Merge branch 'master' of git://github.com/janeczku/calibre-web

pull/819/head
Matt 5 years ago
commit c07cb23ef2

2
.gitattributes vendored

@ -1,4 +1,4 @@
helper.py ident export-subst updater.py ident export-subst
/test export-ignore /test export-ignore
cps/static/css/libs/* linguist-vendored cps/static/css/libs/* linguist-vendored
cps/static/js/libs/* linguist-vendored cps/static/js/libs/* linguist-vendored

@ -1,6 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2016-2019 lemmsh cervinko Kennyl matthazinski 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/>.
import logging import logging
import uploader import uploader
import os import os
@ -19,6 +35,7 @@ logger = logging.getLogger("book_formats")
try: try:
from wand.image import Image from wand.image import Image
from wand import version as ImageVersion from wand import version as ImageVersion
from wand.exceptions import PolicyError
use_generic_pdf_cover = False use_generic_pdf_cover = False
except (ImportError, RuntimeError) as e: except (ImportError, RuntimeError) as e:
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
@ -114,12 +131,18 @@ def pdf_preview(tmp_file_path, tmp_dir):
if use_generic_pdf_cover: if use_generic_pdf_cover:
return None return None
else: else:
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" try:
with Image(filename=tmp_file_path + "[0]", resolution=150) as img: cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
img.compression_quality = 88 with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
img.save(filename=os.path.join(tmp_dir, cover_file_name)) img.compression_quality = 88
return cover_file_name img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
except PolicyError as ex:
logger.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex)
return None
except Exception as ex:
logger.warning('Cannot extract cover image, using default: %s', ex)
return None
def get_versions(): def get_versions():
if not use_generic_pdf_cover: if not use_generic_pdf_cover:

@ -1,3 +1,19 @@
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2016-2019 jkrehm andy29485 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/>.
# Inspired by https://github.com/ChrisTM/Flask-CacheBust # Inspired by https://github.com/ChrisTM/Flask-CacheBust
# Uses query strings so CSS font files are found without having to resort to absolute URLs # Uses query strings so CSS font files are found without having to resort to absolute URLs

@ -1,6 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 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/>.
import argparse import argparse
import os import os
import sys import sys

@ -1,6 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 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/>.
import zipfile import zipfile
import tarfile import tarfile
import os import os

@ -1,5 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2016-2019 Ben Bennett, 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/>.
import os import os
import subprocess import subprocess
import ub import ub

@ -1,6 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 mutschler, cervinko, ok11, jkrehm, nanu-c, Wineliva,
# pjeby, elelay, idalin, 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 sqlalchemy import * from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import * from sqlalchemy.orm import *

@ -1,6 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 lemmsh, Kennyl, Kyosfonica, matthazinski
#
# 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/>.
import zipfile import zipfile
from lxml import etree from lxml import etree
import os import os

@ -1,6 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 lemmsh, cervinko, 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 lxml import etree from lxml import etree
import uploader import uploader

@ -1,7 +1,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 idalin, 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/>.
try: try:
from pydrive.auth import GoogleAuth from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError from pydrive.auth import RefreshError, InvalidConfigError
from apiclient import errors from apiclient import errors
gdrive_support = True gdrive_support = True
except ImportError: except ImportError:
@ -147,7 +166,10 @@ def getDrive(drive=None, gauth=None):
# Save the current credentials to a file # Save the current credentials to a file
return GoogleDrive(gauth) return GoogleDrive(gauth)
if drive.auth.access_token_expired: if drive.auth.access_token_expired:
drive.auth.Refresh() try:
drive.auth.Refresh()
except RefreshError as e:
web.app.logger.error("Google Drive error: " + e.message)
return drive return drive
def listRootFolders(): def listRootFolders():
@ -435,6 +457,10 @@ def getChangeById (drive, change_id):
except (errors.HttpError) as error: except (errors.HttpError) as error:
web.app.logger.info(error.message) web.app.logger.info(error.message)
return None return None
except Exception as e:
web.app.logger.info(e)
return None
# Deletes the local hashes database to force search for new folder names # Deletes the local hashes database to force search for new folder names
def deleteDatabaseOnChange(): def deleteDatabaseOnChange():

@ -1,16 +1,34 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 cervinko, idalin, SiphonSquirrel, ouzklcn, akushsky,
# OzzieIsaacs, bodybybuddha, jkrehm, matthazinski, janeczku
#
# 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/>.
import db import db
import ub import ub
from flask import current_app as app from flask import current_app as app
import logging # import logging
from tempfile import gettempdir from tempfile import gettempdir
import sys import sys
import os import os
import re import re
import unicodedata import unicodedata
from io import BytesIO # from io import BytesIO
import worker import worker
import time import time
from flask import send_from_directory, make_response, redirect, abort from flask import send_from_directory, make_response, redirect, abort
@ -18,16 +36,16 @@ from flask_babel import gettext as _
from flask_login import current_user from flask_login import current_user
from babel.dates import format_datetime from babel.dates import format_datetime
from datetime import datetime from datetime import datetime
import threading # import threading
import shutil import shutil
import requests import requests
import zipfile # import zipfile
try: try:
import gdriveutils as gd import gdriveutils as gd
except ImportError: except ImportError:
pass pass
import web import web
import server # import server
import random import random
import subprocess import subprocess
@ -38,7 +56,7 @@ except ImportError:
use_unidecode = False use_unidecode = False
# Global variables # Global variables
updater_thread = None # updater_thread = None
global_WorkerThread = worker.WorkerThread() global_WorkerThread = worker.WorkerThread()
global_WorkerThread.start() global_WorkerThread.start()
@ -409,6 +427,8 @@ def delete_book(book, calibrepath, book_format):
def get_book_cover(cover_path): def get_book_cover(cover_path):
if ub.config.config_use_google_drive: if ub.config.config_use_google_drive:
try: try:
if not web.is_gdrive_ready():
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
path=gd.get_cover_via_gdrive(cover_path) path=gd.get_cover_via_gdrive(cover_path)
if path: if path:
return redirect(path) return redirect(path)
@ -416,7 +436,7 @@ def get_book_cover(cover_path):
web.app.logger.error(cover_path + '/cover.jpg not found on Google Drive') web.app.logger.error(cover_path + '/cover.jpg not found on Google Drive')
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg") return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
except Exception as e: except Exception as e:
web.app.logger.error("Error Message: "+e.message) web.app.logger.error("Error Message: " + e.message)
web.app.logger.exception(e) web.app.logger.exception(e)
# traceback.print_exc() # traceback.print_exc()
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg") return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg")
@ -468,166 +488,6 @@ def do_download_file(book, book_format, data, headers):
################################## ##################################
class Updater(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.status = 0
def run(self):
try:
self.status = 1
r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True)
r.raise_for_status()
fname = re.findall("filename=(.+)", r.headers['content-disposition'])[0]
self.status = 2
z = zipfile.ZipFile(BytesIO(r.content))
self.status = 3
tmp_dir = gettempdir()
z.extractall(tmp_dir)
self.status = 4
self.update_source(os.path.join(tmp_dir, os.path.splitext(fname)[0]), ub.config.get_main_dir)
self.status = 6
time.sleep(2)
server.Server.setRestartTyp(True)
server.Server.stopServer()
self.status = 7
time.sleep(2)
except requests.exceptions.HTTPError as ex:
logging.getLogger('cps.web').info( u'HTTP Error' + ' ' + str(ex))
self.status = 8
except requests.exceptions.ConnectionError:
logging.getLogger('cps.web').info(u'Connection error')
self.status = 9
except requests.exceptions.Timeout:
logging.getLogger('cps.web').info(u'Timeout while establishing connection')
self.status = 10
except requests.exceptions.RequestException:
self.status = 11
logging.getLogger('cps.web').info(u'General error')
def get_update_status(self):
return self.status
@classmethod
def file_to_list(self, filelist):
return [x.strip() for x in open(filelist, 'r') if not x.startswith('#EXT')]
@classmethod
def one_minus_two(self, one, two):
return [x for x in one if x not in set(two)]
@classmethod
def reduce_dirs(self, delete_files, new_list):
new_delete = []
for filename in delete_files:
parts = filename.split(os.sep)
sub = ''
for part in parts:
sub = os.path.join(sub, part)
if sub == '':
sub = os.sep
count = 0
for song in new_list:
if song.startswith(sub):
count += 1
break
if count == 0:
if sub != '\\':
new_delete.append(sub)
break
return list(set(new_delete))
@classmethod
def reduce_files(self, remove_items, exclude_items):
rf = []
for item in remove_items:
if not item.startswith(exclude_items):
rf.append(item)
return rf
@classmethod
def moveallfiles(self, root_src_dir, root_dst_dir):
change_permissions = True
if sys.platform == "win32" or sys.platform == "darwin":
change_permissions = False
else:
logging.getLogger('cps.web').debug('Update on OS-System : ' + sys.platform)
new_permissions = os.stat(root_dst_dir)
# print new_permissions
for src_dir, __, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
logging.getLogger('cps.web').debug('Create-Dir: '+dst_dir)
if change_permissions:
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if os.path.exists(dst_file):
if change_permissions:
permission = os.stat(dst_file)
logging.getLogger('cps.web').debug('Remove file before copy: '+dst_file)
os.remove(dst_file)
else:
if change_permissions:
permission = new_permissions
shutil.move(src_file, dst_dir)
logging.getLogger('cps.web').debug('Move File '+src_file+' to '+dst_dir)
if change_permissions:
try:
os.chown(dst_file, permission.st_uid, permission.st_gid)
except (Exception) as e:
# ex = sys.exc_info()
old_permissions = os.stat(dst_file)
logging.getLogger('cps.web').debug('Fail change permissions of ' + str(dst_file) + '. Before: '
+ str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: '
+ str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e))
return
def update_source(self, source, destination):
# destination files
old_list = list()
exclude = (
'vendor' + os.sep + 'kindlegen.exe', 'vendor' + os.sep + 'kindlegen', os.sep + 'app.db',
os.sep + 'vendor', os.sep + 'calibre-web.log')
for root, dirs, files in os.walk(destination, topdown=True):
for name in files:
old_list.append(os.path.join(root, name).replace(destination, ''))
for name in dirs:
old_list.append(os.path.join(root, name).replace(destination, ''))
# source files
new_list = list()
for root, dirs, files in os.walk(source, topdown=True):
for name in files:
new_list.append(os.path.join(root, name).replace(source, ''))
for name in dirs:
new_list.append(os.path.join(root, name).replace(source, ''))
delete_files = self.one_minus_two(old_list, new_list)
rf = self.reduce_files(delete_files, exclude)
remove_items = self.reduce_dirs(rf, new_list)
self.moveallfiles(source, destination)
for item in remove_items:
item_path = os.path.join(destination, item[1:])
if os.path.isdir(item_path):
logging.getLogger('cps.web').debug("Delete dir " + item_path)
shutil.rmtree(item_path)
else:
try:
logging.getLogger('cps.web').debug("Delete file " + item_path)
# log_from_thread("Delete file " + item_path)
os.remove(item_path)
except Exception:
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True)
def check_unrar(unrarLocation): def check_unrar(unrarLocation):
@ -654,26 +514,6 @@ def check_unrar(unrarLocation):
return (error, version) return (error, version)
def is_sha1(sha1):
if len(sha1) != 40:
return False
try:
int(sha1, 16)
except ValueError:
return False
return True
def get_current_version_info():
content = {}
content[0] = '$Format:%H$'
content[1] = '$Format:%cI$'
# content[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
# content[1] = '2018-09-09T10:13:08+02:00'
if is_sha1(content[0]) and len(content[1]) > 0:
return {'hash': content[0], 'datetime': content[1]}
return False
def json_serial(obj): def json_serial(obj):
"""JSON serializer for objects not serializable by default json code""" """JSON serializer for objects not serializable by default json code"""

@ -1,3 +1,31 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Flask License
#
# Copyright © 2010 by the Pallets team.
#
# Some rights reserved.
# Redistribution and use in source and binary forms of the software as well as
# documentation, with or without modification, are permitted provided that the
# following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list of conditions
# and the following disclaimer.
# Redistributions in binary form must reproduce the above copyright notice, this list of conditions
# and the following disclaimer in the documentation and/or other materials provided with the distribution.
# Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# http://flask.pocoo.org/snippets/62/ # http://flask.pocoo.org/snippets/62/
try: try:

@ -1,6 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 cervinko, janeczku, 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/>.
class ReverseProxied(object): class ReverseProxied(object):
"""Wrap the application in this middleware and configure the """Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind front-end server to add these headers, to let you quietly bind

@ -1,6 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 janeczku, OzzieIsaacs, andrerfcsantos, idalin
#
# 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 socket import error as SocketError from socket import error as SocketError
import sys import sys
import os import os
@ -20,6 +37,7 @@ except ImportError:
gevent_present = False gevent_present = False
class server: class server:
wsgiserver = None wsgiserver = None
@ -44,11 +62,14 @@ class server:
self.wsgiserver= WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) self.wsgiserver= WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
else: else:
self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
web.py3_gevent_link = self.wsgiserver
self.wsgiserver.serve_forever() self.wsgiserver.serve_forever()
except SocketError: except SocketError:
try: try:
web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...') web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...')
self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
web.py3_gevent_link = self.wsgiserver
self.wsgiserver.serve_forever() self.wsgiserver.serve_forever()
except (OSError, SocketError) as e: except (OSError, SocketError) as e:
web.app.logger.info("Error starting server: %s" % e.strerror) web.app.logger.info("Error starting server: %s" % e.strerror)
@ -91,6 +112,9 @@ class server:
web.helper.global_WorkerThread.stop() web.helper.global_WorkerThread.stop()
sys.exit(1) sys.exit(1)
# ToDo: Somehow caused by circular import under python3 refactor
if sys.version_info > (3, 0):
self.restart = web.py3_restart_Typ
if self.restart == True: if self.restart == True:
web.app.logger.info("Performing restart of Calibre-Web") web.app.logger.info("Performing restart of Calibre-Web")
web.helper.global_WorkerThread.stop() web.helper.global_WorkerThread.stop()
@ -107,12 +131,21 @@ class server:
sys.exit(0) sys.exit(0)
def setRestartTyp(self,starttyp): def setRestartTyp(self,starttyp):
self.restart=starttyp self.restart = starttyp
# ToDo: Somehow caused by circular import under python3 refactor
web.py3_restart_Typ = starttyp
def killServer(self, signum, frame): def killServer(self, signum, frame):
self.stopServer() self.stopServer()
def stopServer(self): def stopServer(self):
# ToDo: Somehow caused by circular import under python3 refactor
if sys.version_info > (3, 0):
if not self.wsgiserver:
if gevent_present:
self.wsgiserver = web.py3_gevent_link
else:
self.wsgiserver = IOLoop.instance()
if self.wsgiserver: if self.wsgiserver:
if gevent_present: if gevent_present:
self.wsgiserver.close() self.wsgiserver.close()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,505 +1,349 @@
/*! normalize.css v1.0.1 | MIT License | git.io/normalize */ /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* ========================================================================== /* Document
HTML5 display definitions
========================================================================== */ ========================================================================== */
/* /**
* Corrects `block` display not defined in IE 6/7/8/9 and Firefox 3. * 1. Correct the line height in all browsers.
*/ * 2. Prevent adjustments of font size after orientation changes in iOS.
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section,
summary {
display: block;
}
/*
* Corrects `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
}
/*
* Prevents modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/ */
audio:not([controls]) { html {
display: none; line-height: 1.15; /* 1 */
height: 0; -webkit-text-size-adjust: 100%; /* 2 */
}
/*
* Addresses styling for `hidden` attribute not present in IE 7/8/9, Firefox 3,
* and Safari 4.
* Known issue: no IE 6 support.
*/
[hidden] {
display: none;
} }
/* ========================================================================== /* Sections
Base
========================================================================== */ ========================================================================== */
/* /**
* 1. Corrects text resizing oddly in IE 6/7 when body `font-size` is set using * Remove the margin in all browsers.
* `em` units.
* 2. Prevents iOS text size adjust after orientation change, without disabling
* user zoom.
*/ */
html { body {
font-size: 100%; /* 1 */ margin: 0;
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
} }
/* /**
* Addresses `font-family` inconsistency between `textarea` and other form * Render the `main` element consistently in IE.
* elements.
*/ */
html, main {
button, display: block;
input,
select,
textarea {
font-family: sans-serif;
} }
/* /**
* Addresses margins handled incorrectly in IE 6/7. * Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/ */
body { h1 {
margin: 0; font-size: 2em;
margin: 0.67em 0;
} }
/* ========================================================================== /* Grouping content
Links
========================================================================== */ ========================================================================== */
/* /**
* Addresses `outline` inconsistency between Chrome and other browsers. * 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/ */
a:focus { hr {
outline: thin dotted; box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
} }
/* /**
* Improves readability when focused and also mouse hovered in all browsers. * 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/ */
a:active, pre {
a:hover { font-family: monospace, monospace; /* 1 */
outline: 0; font-size: 1em; /* 2 */
} }
/* ========================================================================== /* Text-level semantics
Typography
========================================================================== */ ========================================================================== */
/* /**
* Addresses font sizes and margins set differently in IE 6/7. * Remove the gray background on active links in IE 10.
* Addresses font sizes within `section` and `article` in Firefox 4+, Safari 5,
* and Chrome.
*/ */
h1 { a {
font-size: 2em; background-color: transparent;
margin: 0.67em 0;
}
h2 {
font-size: 1.5em;
margin: 0.83em 0;
} }
h3 { /**
font-size: 1.17em; * 1. Remove the bottom border in Chrome 57-
margin: 1em 0; * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
}
h4 {
font-size: 1em;
margin: 1.33em 0;
}
h5 {
font-size: 0.83em;
margin: 1.67em 0;
}
h6 {
font-size: 0.75em;
margin: 2.33em 0;
}
/*
* Addresses styling not present in IE 7/8/9, Safari 5, and Chrome.
*/ */
abbr[title] { abbr[title] {
border-bottom: 1px dotted; border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
} }
/* /**
* Addresses style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. * Add the correct font weight in Chrome, Edge, and Safari.
*/ */
b, b,
strong { strong {
font-weight: bold; font-weight: bolder;
}
blockquote {
margin: 1em 40px;
} }
/* /**
* Addresses styling not present in Safari 5 and Chrome. * 1. Correct the inheritance and scaling of font size in all browsers.
*/ * 2. Correct the odd `em` font sizing in all browsers.
dfn {
font-style: italic;
}
/*
* Addresses styling not present in IE 6/7/8/9.
*/
mark {
background: #ff0;
color: #000;
}
/*
* Addresses margins set differently in IE 6/7.
*/
p,
pre {
margin: 1em 0;
}
/*
* Corrects font family set oddly in IE 6, Safari 4/5, and Chrome.
*/ */
code, code,
kbd, kbd,
pre,
samp { samp {
font-family: monospace, serif; font-family: monospace, monospace; /* 1 */
_font-family: 'courier new', monospace; font-size: 1em; /* 2 */
font-size: 1em;
} }
/* /**
* Improves readability of pre-formatted text in all browsers. * Add the correct font size in all browsers.
*/
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
/*
* Addresses CSS quotes not supported in IE 6/7.
*/
q {
quotes: none;
}
/*
* Addresses `quotes` property not supported in Safari 4.
*/
q:before,
q:after {
content: '';
content: none;
}
/*
* Addresses inconsistent and variable font size in all browsers.
*/ */
small { small {
font-size: 80%; font-size: 80%;
} }
/* /**
* Prevents `sub` and `sup` affecting `line-height` in all browsers. * Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/ */
sub, sub,
sup { sup {
font-size: 75%; font-size: 75%;
line-height: 0; line-height: 0;
position: relative; position: relative;
vertical-align: baseline; vertical-align: baseline;
} }
sup { sub {
top: -0.5em; bottom: -0.25em;
} }
sub { sup {
bottom: -0.25em; top: -0.5em;
} }
/* ========================================================================== /* Embedded content
Lists
========================================================================== */ ========================================================================== */
/* /**
* Addresses margins set differently in IE 6/7. * Remove the border on images inside links in IE 10.
*/ */
dl, img {
menu, border-style: none;
ol,
ul {
margin: 1em 0;
} }
dd { /* Forms
margin: 0 0 0 40px; ========================================================================== */
}
/* /**
* Addresses paddings set differently in IE 6/7. * 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/ */
menu, button,
ol, input,
ul { optgroup,
padding: 0 0 0 40px; select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
} }
/* /**
* Corrects list images handled incorrectly in IE 7. * Show the overflow in IE.
* 1. Show the overflow in Edge.
*/ */
nav ul, button,
nav ol { input { /* 1 */
list-style: none; overflow: visible;
list-style-image: none;
} }
/* ========================================================================== /**
Embedded content * Remove the inheritance of text transform in Edge, Firefox, and IE.
========================================================================== */ * 1. Remove the inheritance of text transform in Firefox.
/*
* 1. Removes border when inside `a` element in IE 6/7/8/9 and Firefox 3.
* 2. Improves image quality when scaled in IE 7.
*/ */
img { button,
border: 0; /* 1 */ select { /* 1 */
-ms-interpolation-mode: bicubic; /* 2 */ text-transform: none;
} }
/* /**
* Corrects overflow displayed oddly in IE 9. * Correct the inability to style clickable types in iOS and Safari.
*/ */
svg:not(:root) { button,
overflow: hidden; [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
} }
/* ========================================================================== /**
Figures * Remove the inner border and padding in Firefox.
========================================================================== */
/*
* Addresses margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
*/ */
figure { button::-moz-focus-inner,
margin: 0; [type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
} }
/* ========================================================================== /**
Forms * Restore the focus styles unset by the previous rule.
========================================================================== */
/*
* Corrects margin displayed oddly in IE 6/7.
*/ */
form { button:-moz-focusring,
margin: 0; [type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
} }
/* /**
* Define consistent border, margin, and padding. * Correct the padding in Firefox.
*/ */
fieldset { fieldset {
border: 1px solid #c0c0c0; padding: 0.35em 0.75em 0.625em;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
} }
/* /**
* 1. Corrects color not being inherited in IE 6/7/8/9. * 1. Correct the text wrapping in Edge and IE.
* 2. Corrects text not wrapping in Firefox 3. * 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Corrects alignment displayed oddly in IE 6/7. * 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/ */
legend { legend {
border: 0; /* 1 */ box-sizing: border-box; /* 1 */
padding: 0; color: inherit; /* 2 */
white-space: normal; /* 2 */ display: table; /* 1 */
*margin-left: -7px; /* 3 */ max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
} }
/* /**
* 1. Corrects font size not being inherited in all browsers. * Add the correct vertical alignment in Chrome, Firefox, and Opera.
* 2. Addresses margins set differently in IE 6/7, Firefox 3+, Safari 5,
* and Chrome.
* 3. Improves appearance and consistency in all browsers.
*/ */
button, progress {
input, vertical-align: baseline;
select,
textarea {
font-size: 100%; /* 1 */
margin: 0; /* 2 */
vertical-align: baseline; /* 3 */
*vertical-align: middle; /* 3 */
} }
/* /**
* Addresses Firefox 3+ setting `line-height` on `input` using `!important` in * Remove the default vertical scrollbar in IE 10+.
* the UA stylesheet.
*/ */
button, textarea {
input { overflow: auto;
line-height: normal;
} }
/* /**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` * 1. Add the correct box sizing in IE 10.
* and `video` controls. * 2. Remove the padding in IE 10.
* 2. Corrects inability to style clickable `input` types in iOS.
* 3. Improves usability and consistency of cursor style between image-type
* `input` and others.
* 4. Removes inner spacing in IE 7 without affecting normal text inputs.
* Known issue: inner spacing remains in IE 6.
*/ */
button, [type="checkbox"],
html input[type="button"], /* 1 */ [type="radio"] {
input[type="reset"], box-sizing: border-box; /* 1 */
input[type="submit"] { padding: 0; /* 2 */
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
*overflow: visible; /* 4 */
} }
/* /**
* Re-set default cursor for disabled elements. * Correct the cursor style of increment and decrement buttons in Chrome.
*/ */
button[disabled], [type="number"]::-webkit-inner-spin-button,
input[disabled] { [type="number"]::-webkit-outer-spin-button {
cursor: default; height: auto;
} }
/* /**
* 1. Addresses box sizing set to content-box in IE 8/9. * 1. Correct the odd appearance in Chrome and Safari.
* 2. Removes excess padding in IE 8/9. * 2. Correct the outline style in Safari.
* 3. Removes excess padding in IE 7.
* Known issue: excess padding remains in IE 6.
*/ */
input[type="checkbox"], [type="search"] {
input[type="radio"] { -webkit-appearance: textfield; /* 1 */
box-sizing: border-box; /* 1 */ outline-offset: -2px; /* 2 */
padding: 0; /* 2 */
*height: 13px; /* 3 */
*width: 13px; /* 3 */
} }
/* /**
* 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. * Remove the inner padding in Chrome and Safari on macOS.
* 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/ */
/*
input[type="search"] { [type="search"]::-webkit-search-decoration {
-webkit-appearance: textfield; -webkit-appearance: none;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box;
} }
*/
/* /**
* Removes inner padding and search cancel button in Safari 5 and Chrome * 1. Correct the inability to style clickable types in iOS and Safari.
* on OS X. * 2. Change font properties to `inherit` in Safari.
*/ */
/* input[type="search"]::-webkit-search-cancel-button, ::-webkit-file-upload-button {
input[type="search"]::-webkit-search-decoration { -webkit-appearance: button; /* 1 */
-webkit-appearance: none; font: inherit; /* 2 */
} */ }
/* Interactive
========================================================================== */
/* /*
* Removes inner padding and border in Firefox 3+. * Add the correct display in Edge, IE 10+, and Firefox.
*/ */
button::-moz-focus-inner, details {
input::-moz-focus-inner { display: block;
border: 0;
padding: 0;
} }
/* /*
* 1. Removes default vertical scrollbar in IE 6/7/8/9. * Add the correct display in all browsers.
* 2. Improves readability and alignment in all browsers.
*/ */
textarea { summary {
overflow: auto; /* 1 */ display: list-item;
vertical-align: top; /* 2 */
} }
/* ========================================================================== /* Misc
Tables
========================================================================== */ ========================================================================== */
/* /**
* Remove most spacing between table cells. * Add the correct display in IE 10+.
*/ */
table { template {
border-collapse: collapse; display: none;
border-spacing: 0;
} }
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

@ -0,0 +1,8 @@
@media (min-device-width: 768px) {
.modal-dialog {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%) !important;
}
}

@ -1,3 +1,19 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018-2019 hexeth
*
* 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/>.
*/
// Move advanced search to side-menu // Move advanced search to side-menu
$( 'a[href*="advanced"]' ).parent().insertAfter( '#nav_new' ); $( 'a[href*="advanced"]' ).parent().insertAfter( '#nav_new' );
$( 'body' ).addClass('blur'); $( 'body' ).addClass('blur');
@ -200,7 +216,7 @@ return $(this).text().replace(/^\s+|^\t+|\t+|\s+$/g, "");
$('.downloadBtn' ).click( function() { $('.downloadBtn' ).click( function() {
$( '.leramslist' ).toggle(); $( '.leramslist' ).toggle();
}); });
$('#sendbtn2' ).click( function() { $('#sendbtn2' ).click( function() {
$( '.sendtokindle-drop' ).toggle(); $( '.sendtokindle-drop' ).toggle();
}); });
@ -231,7 +247,7 @@ return $(this).text().replace(/^\s+|^\t+|\t+|\s+$/g, "");
$( '.readinbrowser-drop' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px"); $( '.readinbrowser-drop' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
} }
} }
if ( $( '#sendbtn2' ).length > 0 ) { if ( $( '#sendbtn2' ).length > 0 ) {
position = $( '#sendbtn2' ).offset().left position = $( '#sendbtn2' ).offset().left
if ( position + $( '.sendtokindle-drop' ).width() > $( window ).width() ) { if ( position + $( '.sendtokindle-drop' ).width() > $( window ).width() ) {

@ -1,3 +1,20 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 jkrehm
*
* 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/>.
*/
/* global _ */ /* global _ */
$(function() { $(function() {

@ -1,6 +1,21 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 idalin<dalin.lin@gmail.com>
*
* 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/>.
*/
/* /*
* Get Metadata from Douban Books api and Google Books api * Get Metadata from Douban Books api and Google Books api
* Created by idalin<dalin.lin@gmail.com>
* Google Books api document: https://developers.google.com/books/docs/v1/using * Google Books api document: https://developers.google.com/books/docs/v1/using
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only) * Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
*/ */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,3 +1,20 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2012-2019 mutschler, janeczku, jkrehm, 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/>.
*/
// Generic control/related handler to show/hide fields based on a checkbox' value // Generic control/related handler to show/hide fields based on a checkbox' value
// e.g. // e.g.
// <input type="checkbox" data-control="stuff-to-show"> // <input type="checkbox" data-control="stuff-to-show">
@ -60,17 +77,17 @@ $(function() {
layoutMode : "fitRows" layoutMode : "fitRows"
}); });
$(".load-more .row").infinitescroll({ var $loadMore = $(".load-more .row").infiniteScroll({
debug: false, debug: false,
navSelector : ".pagination",
// selector for the paged navigation (it will be hidden) // selector for the paged navigation (it will be hidden)
nextSelector : ".pagination a:last", path : ".next",
// selector for the NEXT link (to page 2) // selector for the NEXT link (to page 2)
itemSelector : ".load-more .book", append : ".load-more .book"
animate : true, //animate : true, # ToDo: Reenable function
extraScrollPx: 300 //extraScrollPx: 300
// selector for all items you'll retrieve });
}, function(data) { $loadMore.on( 'append.infiniteScroll', function( event, response, path, data ) {
$(".pagination").addClass("hidden");
$(".load-more .row").isotope( "appended", $(data), null ); $(".load-more .row").isotope( "appended", $(data), null );
}); });
@ -120,7 +137,7 @@ $(function() {
.removeClass("hidden") .removeClass("hidden")
.find("span").html(data.commit); .find("span").html(data.commit);
data.history.reverse().forEach(function(entry) { data.history.forEach(function(entry) {
$("<tr><td>" + entry[0] + "</td><td>" + entry[1] + "</td></tr>").appendTo($("#update_table")); $("<tr><td>" + entry[0] + "</td><td>" + entry[1] + "</td></tr>").appendTo($("#update_table"));
}); });
cssClass = "alert-warning"; cssClass = "alert-warning";
@ -185,6 +202,6 @@ $(function() {
}); });
$(window).resize(function() { $(window).resize(function() {
$(".discover .row").isotope("reLayout"); $(".discover .row").isotope("layout");
}); });
}); });

@ -1,3 +1,20 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 jkrehm, 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/>.
*/
/* global Sortable,sortTrue */ /* global Sortable,sortTrue */
Sortable.create(sortTrue, { Sortable.create(sortTrue, {

@ -1,3 +1,20 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 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/>.
*/
$(function() { $(function() {
$("#domain_submit").click(function(event) { $("#domain_submit").click(function(event) {

@ -0,0 +1,198 @@
/*
* bootstrap-uploadprogress
* github: https://github.com/jakobadam/bootstrap-uploadprogress
*
* Copyright (c) 2015 Jakob Aarøe Dam
* Version 1.0.0
* Licensed under the MIT license.
*/
(function($){
"use strict";
$.support.xhrFileUpload = !!(window.FileReader && window.ProgressEvent);
$.support.xhrFormData = !!window.FormData;
if(!$.support.xhrFileUpload || !$.support.xhrFormData){
// skip decorating form
return;
}
var template = '<div class="modal fade" id="file-progress-modal">\
<div class="modal-dialog">\
<div class="modal-content">\
<div class="modal-header">\
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>\
<h4 class="modal-title">Uploading</h4>\
</div>\
<div class="modal-body">\
<div class="modal-message"></div>\
<div class="progress">\
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0"\
aria-valuemax="100" style="width: 0%;min-width: 2em;">\
0%\
</div>\
</div>\
</div>\
<div class="modal-footer" style="display:none">\
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>\
</div>\
</div>\
</div>\
</div>';
var UploadProgress = function(element, options){
this.options = options;
this.$element = $(element);
};
UploadProgress.prototype = {
constructor: function() {
this.$form = this.$element;
this.$form.on('submit', $.proxy(this.submit, this));
this.$modal = $(this.options.template);
this.$modal_message = this.$modal.find('.modal-message');
this.$modal_title = this.$modal.find('.modal-title');
this.$modal_footer = this.$modal.find('.modal-footer');
this.$modal_bar = this.$modal.find('.progress-bar');
this.$modal.on('hidden.bs.modal', $.proxy(this.reset, this));
},
reset: function(){
this.$modal_title = this.$modal_title.text('Uploading');
this.$modal_footer.hide();
this.$modal_bar.addClass('progress-bar-success');
this.$modal_bar.removeClass('progress-bar-danger');
if(this.xhr){
this.xhr.abort();
}
},
submit: function(e) {
e.preventDefault();
this.$modal.modal({
backdrop: 'static',
keyboard: false
});
// We need the native XMLHttpRequest for the progress event
var xhr = new XMLHttpRequest();
this.xhr = xhr;
xhr.addEventListener('load', $.proxy(this.success, this, xhr));
xhr.addEventListener('error', $.proxy(this.error, this, xhr));
//xhr.addEventListener('abort', function(){});
xhr.upload.addEventListener('progress', $.proxy(this.progress, this));
var form = this.$form;
xhr.open(form.attr('method'), form.attr("action"));
xhr.setRequestHeader('X-REQUESTED-WITH', 'XMLHttpRequest');
var data = new FormData(form.get(0));
xhr.send(data);
},
success: function(xhr) {
if(xhr.status == 0 || xhr.status >= 400){
// HTTP 500 ends up here!?!
return this.error(xhr);
}
this.set_progress(100);
var url;
var content_type = xhr.getResponseHeader('Content-Type');
// make it possible to return the redirect URL in
// a JSON response
if(content_type.indexOf('application/json') !== -1){
var response = $.parseJSON(xhr.responseText);
console.log(response);
url = response.location;
}
else{
url = this.options.redirect_url;
}
window.location.href = url;
},
// handle form error
// we replace the form with the returned one
error: function(xhr){
this.$modal_title.text('Upload failed');
this.$modal_bar.removeClass('progress-bar-success');
this.$modal_bar.addClass('progress-bar-danger');
this.$modal_footer.show();
var content_type = xhr.getResponseHeader('Content-Type');
// Replace the contents of the form, with the returned html
if(xhr.status === 422){
var new_html = $.parseHTML(xhr.responseText);
this.replace_form(new_html);
this.$modal.modal('hide');
}
// Write the error response to the document.
else{
var response_text = xhr.responseText;
if(content_type.indexOf('text/plain') !== -1){
response_text = '<pre>' + response_text + '</pre>';
}
document.write(xhr.responseText);
}
},
set_progress: function(percent){
var txt = percent + '%';
if (percent == 100) {
txt = this.options.uploaded_msg;
}
this.$modal_bar.attr('aria-valuenow', percent);
this.$modal_bar.text(txt);
this.$modal_bar.css('width', percent + '%');
},
progress: function(/*ProgressEvent*/e){
var percent = Math.round((e.loaded / e.total) * 100);
this.set_progress(percent);
},
// replace_form replaces the contents of the current form
// with the form in the html argument.
// We use the id of the current form to find the new form in the html
replace_form: function(html){
var new_form;
var form_id = this.$form.attr('id');
if(form_id !== undefined){
new_form = $(html).find('#' + form_id);
}
else{
new_form = $(html).find('form');
}
// add the filestyle again
new_form.find(':file').filestyle({buttonBefore: true});
this.$form.html(new_form.children());
}
};
$.fn.uploadprogress = function(options, value){
return this.each(function(){
var _options = $.extend({}, $.fn.uploadprogress.defaults, options);
var file_progress = new UploadProgress(this, _options);
file_progress.constructor();
});
};
$.fn.uploadprogress.defaults = {
template: template,
uploaded_msg: "Upload done, processing, please wait..."
//redirect_url: ...
// need to customize stuff? Add here, and change code accordingly.
};
})(window.jQuery);

@ -105,7 +105,7 @@
<div class="col"> <div class="col">
<h2>{{_('Administration')}}</h2> <h2>{{_('Administration')}}</h2>
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div> <div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
<div class="btn btn-default" id="admin_restart"data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div> <div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div>
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div> <div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
</div> </div>
</div> </div>

@ -29,7 +29,7 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}"> <a href="{{ url_for('show_book', book_id=entry.id) }}">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" />
{% endif %} {% endif %}

@ -6,7 +6,7 @@
<div class="col-sm-3 col-lg-3 col-xs-12"> <div class="col-sm-3 col-lg-3 col-xs-12">
<div class="cover"> <div class="cover">
{% if book.has_cover %} {% if book.has_cover %}
<img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" alt="{{ book.title }}"/> <img src="{{ url_for('get_cover', book_id=book.id) }}" alt="{{ book.title }}"/>
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/>
{% endif %} {% endif %}

@ -31,15 +31,19 @@
</label> </label>
</div> </div>
{% else %} {% else %}
{% if show_authenticate_google_drive and g.user.is_authenticated %} {% if show_authenticate_google_drive and g.user.is_authenticated and content.config_use_google_drive %}
<div class="form-group required"> <div class="form-group required">
<a href="{{ url_for('authenticate_google_drive') }}" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a> <a href="{{ url_for('authenticate_google_drive') }}" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>
</div> </div>
{% else %} {% else %}
{% if show_authenticate_google_drive and not g.user.is_authenticated %} {% if show_authenticate_google_drive and g.user.is_authenticated and not content.config_use_google_drive %}
<div >{{_('Please hit submit to continue with setup')}}</div>
{% endif %}
{% if not g.user.is_authenticated %}
<div >{{_('Please finish Google Drive setup after login')}}</div> <div >{{_('Please finish Google Drive setup after login')}}</div>
{% endif %} {% endif %}
{% if not show_authenticate_google_drive %} {% if g.user.is_authenticated %}
{% if not show_authenticate_google_drive %}
<div class="form-group required"> <div class="form-group required">
<label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label> <label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label>
<select name="config_google_drive_folder" id="config_google_drive_folder" class="form-control"> <select name="config_google_drive_folder" id="config_google_drive_folder" class="form-control">
@ -58,6 +62,7 @@
<a href="{{ url_for('watch_gdrive') }}" class="btn btn-primary">Enable watch of metadata.db</a> <a href="{{ url_for('watch_gdrive') }}" class="btn btn-primary">Enable watch of metadata.db</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@ -76,7 +81,7 @@
</div> </div>
<div id="collapsetwo" class="panel-collapse collapse"> <div id="collapsetwo" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<div class="form-group"> <div class="form-group">
<label for="config_port">{{_('Server Port')}}</label> <label for="config_port">{{_('Server Port')}}</label>
<input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required> <input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required>
</div> </div>
@ -88,6 +93,15 @@
<label for="config_keyfile">{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label> <label for="config_keyfile">{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
<input type="text" class="form-control" name="config_keyfile" id="config_keyfile" value="{% if content.config_keyfile != None %}{{ content.config_keyfile }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" name="config_keyfile" id="config_keyfile" value="{% if content.config_keyfile != None %}{{ content.config_keyfile }}{% endif %}" autocomplete="off">
</div> </div>
<div class="form-group">
<label for="config_updater">{{_('Update channel')}}</label>
<select name="config_updater" id="config_updater" class="form-control">
<option value="0" {% if content.config_updatechannel == 0 %}selected{% endif %}>{{_('Stable')}}</option>
<!--option value="1" {% if content.config_updatechannel == 1 %}selected{% endif %}>{{_('Stable (Automatic)')}}</option-->
<option value="2" {% if content.config_updatechannel == 2 %}selected{% endif %}>{{_('Nightly')}}</option>
<!--option-- value="3" {% if content.config_updatechannel == 3 %}selected{% endif %}>{{_('Nightly (Automatic)')}}</option-->
</select>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -5,7 +5,7 @@
<div class="col-sm-3 col-lg-3 col-xs-5"> <div class="col-sm-3 col-lg-3 col-xs-5">
<div class="cover"> <div class="cover">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}

@ -9,7 +9,7 @@
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
</a> </a>
{% endif %} {% endif %}
</div> </div>

@ -10,7 +10,7 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}
@ -55,7 +55,7 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}"/> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}

@ -12,6 +12,7 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> <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 href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/upload.css') }}" rel="stylesheet" media="screen">
{% if g.current_theme == 1 %} {% if g.current_theme == 1 %}
<link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen">
{% endif %} {% endif %}
@ -103,14 +104,6 @@
</div> </div>
{%endif%} {%endif%}
{% endfor %} {% endfor %}
<div id="loader" hidden="true">
<center>
<h3>{{_('Uploading...')}}</h3>
<span>{{_("please don't refresh the page")}}</span>.
<br />
<img src="{{ url_for('static', filename='img/loader.gif') }}">
</center>
</div>
<div class="container-fluid"> <div class="container-fluid">
<div class="row-fluid"> <div class="row-fluid">
{% if g.user.is_authenticated or g.user.is_anonymous %} {% if g.user.is_authenticated or g.user.is_anonymous %}
@ -240,6 +233,7 @@
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script> <script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
{% if g.current_theme == 1 %} {% if g.current_theme == 1 %}
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
@ -247,12 +241,12 @@
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script> <script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
{% endif %} {% endif %}
<script type="text/javascript"> <script type="text/javascript">
$(function () { $(function() {
$("#btn-upload").change(function () { $("#form-upload").uploadprogress({redirect_url: '{{ url_for('index')}}'});
$("#loader").show(); $("#btn-upload").change(function() {
$("#form-upload").submit(); $("#form-upload").submit();
}); });
}); });
</script> </script>
{% block js %}{% endblock %} {% block js %}{% endblock %}

@ -7,6 +7,8 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.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/main.css') }}">

@ -36,7 +36,7 @@
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
</a> </a>
{% endif %} {% endif %}
</div> </div>

@ -16,7 +16,7 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}

@ -16,7 +16,7 @@
{% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %} {% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %}
{% if g.user and g.user.role_admin() and g.allow_registration and not new_user and not profile %} {% if g.user and g.user.role_admin() and g.allow_registration and not new_user and not profile %}
<div class="btn btn-default" id="resend_password"><a href="{{url_for('reset_password', user_id = content.id) }}">{{_('Reset user Password')}}</a></div> <div class="btn btn-default" id="resend_password"><a href="{{url_for('reset_password', user_id = content.id) }}">{{_('Reset user Password')}}</a></div>
{% else %} {% else %}
<div class="form-group"> <div class="form-group">
<label for="password">{{_('Password')}}</label> <label for="password">{{_('Password')}}</label>
<input type="password" class="form-control" name="password" id="password" value="" autocomplete="off"> <input type="password" class="form-control" name="password" id="password" value="" autocomplete="off">
@ -85,10 +85,12 @@
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}> <input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
<label for="show_publisher">{{_('Show publisher selection')}}</label> <label for="show_publisher">{{_('Show publisher selection')}}</label>
</div> </div>
{% if not content.role_anonymous() %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}> <input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
<label for="show_read_and_unread">{{_('Show read and unread')}}</label> <label for="show_read_and_unread">{{_('Show read and unread')}}</label>
</div> </div>
{% endif %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}> <input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
<label for="show_detail_random">{{_('Show random books in detail view')}}</label> <label for="show_detail_random">{{_('Show random books in detail view')}}</label>
@ -155,7 +157,7 @@
{% for entry in downloads %} {% for entry in downloads %}
<div class="col-sm-2"> <div class="col-sm-2">
<a class="pull-left" href="{{ url_for('show_book', book_id=entry.id) }}"> <a class="pull-left" href="{{ url_for('show_book', book_id=entry.id) }}">
<img class="media-object" width="100" src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="..."> <img class="media-object" width="100" src="{{ url_for('get_cover', book_id=entry.id) }}" alt="...">
</a> </a>
</div> </div>
{% endfor %} {% endfor %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,6 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 mutschler, jkrehm, cervinko, janeczku, OzzieIsaacs, csitko
# ok11, issmirnov, idalin
#
# 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 sqlalchemy import * from sqlalchemy import *
from sqlalchemy import exc from sqlalchemy import exc
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
@ -44,8 +61,17 @@ MATURE_CONTENT = 2048
SIDEBAR_PUBLISHER = 4096 SIDEBAR_PUBLISHER = 4096
DEFAULT_PASS = "admin123" DEFAULT_PASS = "admin123"
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083)) try:
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
except ValueError:
print ('Environmentvariable CALIBRE_PORT is set to an invalid value: ' +
os.environ.get("CALIBRE_PORT", 8083) + ', faling back to default (8083)')
DEFAULT_PORT = 8083
UPDATE_STABLE = 0
AUTO_UPDATE_STABLE = 1
UPDATE_NIGHTLY = 2
AUTO_UPDATE_NIGHTLY = 4
class UserBase: class UserBase:
@ -166,7 +192,6 @@ class User(UserBase, Base):
sidebar_view = Column(Integer, default=1) sidebar_view = Column(Integer, default=1)
default_language = Column(String(3), default="all") default_language = Column(String(3), default="all")
mature_content = Column(Boolean, default=True) mature_content = Column(Boolean, default=True)
# theme = Column(Integer, default=0)
# Class for anonymous user is derived from User base and completly overrides methods and properties for the # Class for anonymous user is derived from User base and completly overrides methods and properties for the
@ -259,7 +284,7 @@ class Downloads(Base):
def __repr__(self): def __repr__(self):
return '<Download %r' % self.book_id return '<Download %r' % self.book_id
# Baseclass representing allowed domains for registration # Baseclass representing allowed domains for registration
class Registration(Base): class Registration(Base):
__tablename__ = 'registration' __tablename__ = 'registration'
@ -313,6 +338,7 @@ class Settings(Base):
config_calibre = Column(String) config_calibre = Column(String)
config_rarfile_location = Column(String) config_rarfile_location = Column(String)
config_theme = Column(Integer, default=0) config_theme = Column(Integer, default=0)
config_updatechannel = Column(Integer, default=0)
def __repr__(self): def __repr__(self):
pass pass
@ -387,11 +413,16 @@ class Config:
self.config_logfile = data.config_logfile self.config_logfile = data.config_logfile
self.config_rarfile_location = data.config_rarfile_location self.config_rarfile_location = data.config_rarfile_location
self.config_theme = data.config_theme self.config_theme = data.config_theme
self.config_updatechannel = data.config_updatechannel
@property @property
def get_main_dir(self): def get_main_dir(self):
return self.config_main_dir return self.config_main_dir
@property
def get_update_channel(self):
return self.config_updatechannel
def get_config_certfile(self): def get_config_certfile(self):
if cli.certfilepath: if cli.certfilepath:
return cli.certfilepath return cli.certfilepath
@ -535,6 +566,8 @@ class Config:
# everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding # everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding
# rows with SQL commands # rows with SQL commands
def migrate_Database(): def migrate_Database():
if not engine.dialect.has_table(engine.connect(), "book_read_link"):
ReadBook.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "bookmark"): if not engine.dialect.has_table(engine.connect(), "bookmark"):
Bookmark.__table__.create(bind=engine) Bookmark.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "registration"): if not engine.dialect.has_table(engine.connect(), "registration"):
@ -667,6 +700,12 @@ def migrate_Database():
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_theme` INTEGER DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_theme` INTEGER DEFAULT 0")
session.commit() session.commit()
try:
session.query(exists().where(Settings.config_updatechannel)).scalar()
except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_updatechannel` INTEGER DEFAULT 0")
session.commit()
# Remove login capability of user Guest # Remove login capability of user Guest

@ -0,0 +1,514 @@
#!/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/>.
import threading
import zipfile
import requests
import re
import logging
import server
import time
from io import BytesIO
import os
import sys
import shutil
from ub import config, UPDATE_STABLE
from tempfile import gettempdir
import datetime
import json
from flask_babel import gettext as _
from babel.dates import format_datetime
import web
def is_sha1(sha1):
if len(sha1) != 40:
return False
try:
int(sha1, 16)
except ValueError:
return False
return True
class Updater(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.status = 0
self.updateIndex = None
def get_current_version_info(self):
if config.get_update_channel == UPDATE_STABLE:
return self._stable_version_info()
else:
return self._nightly_version_info()
def get_available_updates(self, request_method):
if config.get_update_channel == UPDATE_STABLE:
return self._stable_available_updates(request_method)
else:
return self._nightly_available_updates(request_method)
def run(self):
try:
self.status = 1
r = requests.get(self._get_request_path(), stream=True)
r.raise_for_status()
self.status = 2
z = zipfile.ZipFile(BytesIO(r.content))
self.status = 3
tmp_dir = gettempdir()
z.extractall(tmp_dir)
foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1]
if not os.path.isdir(foldername):
self.status = 11
logging.getLogger('cps.web').info(u'Extracted contents of zipfile not found in temp folder')
return
self.status = 4
self.update_source(foldername, config.get_main_dir)
self.status = 6
time.sleep(2)
server.Server.setRestartTyp(True)
server.Server.stopServer()
self.status = 7
time.sleep(2)
except requests.exceptions.HTTPError as ex:
logging.getLogger('cps.web').info( u'HTTP Error' + ' ' + str(ex))
self.status = 8
except requests.exceptions.ConnectionError:
logging.getLogger('cps.web').info(u'Connection error')
self.status = 9
except requests.exceptions.Timeout:
logging.getLogger('cps.web').info(u'Timeout while establishing connection')
self.status = 10
except requests.exceptions.RequestException:
self.status = 11
logging.getLogger('cps.web').info(u'General error')
def get_update_status(self):
return self.status
@classmethod
def file_to_list(self, filelist):
return [x.strip() for x in open(filelist, 'r') if not x.startswith('#EXT')]
@classmethod
def one_minus_two(self, one, two):
return [x for x in one if x not in set(two)]
@classmethod
def reduce_dirs(self, delete_files, new_list):
new_delete = []
for filename in delete_files:
parts = filename.split(os.sep)
sub = ''
for part in parts:
sub = os.path.join(sub, part)
if sub == '':
sub = os.sep
count = 0
for song in new_list:
if song.startswith(sub):
count += 1
break
if count == 0:
if sub != '\\':
new_delete.append(sub)
break
return list(set(new_delete))
@classmethod
def reduce_files(self, remove_items, exclude_items):
rf = []
for item in remove_items:
if not item.startswith(exclude_items):
rf.append(item)
return rf
@classmethod
def moveallfiles(self, root_src_dir, root_dst_dir):
change_permissions = True
if sys.platform == "win32" or sys.platform == "darwin":
change_permissions = False
else:
logging.getLogger('cps.web').debug('Update on OS-System : ' + sys.platform)
new_permissions = os.stat(root_dst_dir)
# print new_permissions
for src_dir, __, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
logging.getLogger('cps.web').debug('Create-Dir: '+dst_dir)
if change_permissions:
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if os.path.exists(dst_file):
if change_permissions:
permission = os.stat(dst_file)
logging.getLogger('cps.web').debug('Remove file before copy: '+dst_file)
os.remove(dst_file)
else:
if change_permissions:
permission = new_permissions
shutil.move(src_file, dst_dir)
logging.getLogger('cps.web').debug('Move File '+src_file+' to '+dst_dir)
if change_permissions:
try:
os.chown(dst_file, permission.st_uid, permission.st_gid)
except (Exception) as e:
# ex = sys.exc_info()
old_permissions = os.stat(dst_file)
logging.getLogger('cps.web').debug('Fail change permissions of ' + str(dst_file) + '. Before: '
+ str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: '
+ str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e))
return
def update_source(self, source, destination):
# destination files
old_list = list()
exclude = (
os.sep + 'app.db', os.sep + 'calibre-web.log1', os.sep + 'calibre-web.log2', os.sep + 'gdrive.db',
os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep +'client_secrets.json',
os.sep + 'gdrive_credentials', os.sep + 'settings.yaml')
for root, dirs, files in os.walk(destination, topdown=True):
for name in files:
old_list.append(os.path.join(root, name).replace(destination, ''))
for name in dirs:
old_list.append(os.path.join(root, name).replace(destination, ''))
# source files
new_list = list()
for root, dirs, files in os.walk(source, topdown=True):
for name in files:
new_list.append(os.path.join(root, name).replace(source, ''))
for name in dirs:
new_list.append(os.path.join(root, name).replace(source, ''))
delete_files = self.one_minus_two(old_list, new_list)
rf = self.reduce_files(delete_files, exclude)
remove_items = self.reduce_dirs(rf, new_list)
self.moveallfiles(source, destination)
for item in remove_items:
item_path = os.path.join(destination, item[1:])
if os.path.isdir(item_path):
logging.getLogger('cps.web').debug("Delete dir " + item_path)
shutil.rmtree(item_path, ignore_errors=True)
else:
try:
logging.getLogger('cps.web').debug("Delete file " + item_path)
# log_from_thread("Delete file " + item_path)
os.remove(item_path)
except Exception:
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True)
def _nightly_version_info(self):
content = {}
content[0] = '$Format:%H$'
content[1] = '$Format:%cI$'
# content[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
# content[1] = '2018-09-09T10:13:08+02:00'
if is_sha1(content[0]) and len(content[1]) > 0:
return {'version': content[0], 'datetime': content[1]}
return False
def _stable_version_info(self):
return {'version': '0.6.1'} # Current version
def _nightly_available_updates(self, request_method):
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
if request_method == "GET":
repository_url = 'https://api.github.com/repos/janeczku/calibre-web'
status, commit = self._load_remote_data(repository_url +'/git/refs/heads/master')
parents = []
if status['message'] != '':
return json.dumps(status)
if 'object' not in commit:
status['message'] = _(u'Unexpected data while reading update information')
return json.dumps(status)
if commit['object']['sha'] == status['current_commit_hash']:
status.update({
'update': False,
'success': True,
'message': _(u'No update available. You already have the latest version installed')
})
return json.dumps(status)
# a new update is available
status['update'] = True
try:
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'])
r.raise_for_status()
update_data = r.json()
except requests.exceptions.HTTPError as e:
status['error'] = _(u'HTTP Error') + ' ' + str(e)
except requests.exceptions.ConnectionError:
status['error'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['error'] = _(u'Timeout while establishing connection')
except requests.exceptions.RequestException:
status['error'] = _(u'General error')
if status['message'] != '':
return json.dumps(status)
if 'committer' in update_data and 'message' in update_data:
status['success'] = True
status['message'] = _(
u'A new update is available. Click on the button below to update to the latest version.')
new_commit_date = datetime.datetime.strptime(
update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parents.append(
[
format_datetime(new_commit_date, format='short', locale=web.get_locale()),
update_data['message'],
update_data['sha']
]
)
# it only makes sense to analyze the parents if we know the current commit hash
if status['current_commit_hash'] != '':
try:
parent_commit = update_data['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except IndexError:
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']:
try:
r = requests.get(parent_commit['url'])
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=web.get_locale())
parents.append([parent_commit_date,
parent_data['message'].replace('\r\n','<p>').replace('\n','<p>')])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
else:
status['success'] = False
status['message'] = _(u'Could not fetch update information')
# a new update is available
status['update'] = True
if 'body' in commit:
status['success'] = True
status['message'] = _(
u'A new update is available. Click on the button below to update to the latest version.')
new_commit_date = datetime.datetime.strptime(
commit['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parents.append(
[
format_datetime(new_commit_date, format='short', locale=web.get_locale()),
commit['message'],
commit['sha']
]
)
# it only makes sense to analyze the parents if we know the current commit hash
if status['current_commit_hash'] != '':
try:
parent_commit = commit['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except IndexError:
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if commit['sha'] != status['current_commit_hash']:
try:
r = requests.get(parent_commit['url'])
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=web.get_locale())
parents.append([parent_commit_date, parent_data['message'], parent_data['sha']])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
status['history'] = parents[::-1]
return json.dumps(status)
return ''
def _stable_available_updates(self, request_method):
if request_method == "GET":
parents = []
# repository_url = 'https://api.github.com/repos/flatpak/flatpak/releases' # test URL
repository_url = 'https://api.github.com/repos/janeczku/calibre-web/releases'
status, commit = self._load_remote_data(repository_url)
if status['message'] != '':
return json.dumps(status)
if not commit:
status['success'] = True
status['message'] = _(u'No release information available')
return json.dumps(status)
version = status['current_commit_hash']
current_version = status['current_commit_hash'].split('.')
# we are already on newest version, no update available
if 'tag_name' not in commit[0]:
status['message'] = _(u'Unexpected data while reading update information')
return json.dumps(status)
if commit[0]['tag_name'] == version:
status.update({
'update': False,
'success': True,
'message': _(u'No update available. You already have the latest version installed')
})
return json.dumps(status)
i = len(commit) - 1
while i >= 0:
if 'tag_name' not in commit[i] or 'body' not in commit[i]:
status['message'] = _(u'Unexpected data while reading update information')
return json.dumps(status)
major_version_update = int(commit[i]['tag_name'].split('.')[0])
minor_version_update = int(commit[i]['tag_name'].split('.')[1])
patch_version_update = int(commit[i]['tag_name'].split('.')[2])
# Check if major versions are identical search for newest nonenqual commit and update to this one
if major_version_update == int(current_version[0]):
if (minor_version_update == int(current_version[1]) and
patch_version_update > int(current_version[2])) or \
minor_version_update > int(current_version[1]):
parents.append([commit[i]['tag_name'],commit[i]['body'].replace('\r\n', '<p>')])
i -= 1
continue
if major_version_update < int(current_version[0]):
i -= 1
continue
if major_version_update > int(current_version[0]):
# found update update to last version before major update, unless current version is on last version
# before major update
if commit[i+1]['tag_name'].split('.')[1] == current_version[1]:
parents.append([commit[i]['tag_name'],
commit[i]['body'].replace('\r\n', '<p>').replace('\n', '<p>')])
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[i]['tag_name']),
'history': parents
})
self.updateFile = commit[i]['zipball_url']
else:
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[i]['tag_name']),
'history': parents
})
self.updateFile = commit[i +1]['zipball_url']
break
if i == -1:
status.update({
'update': True,
'success': True,
'message': _(
u'A new update is available. Click on the button below to update to the latest version.'),
'history': parents
})
self.updateFile = commit[0]['zipball_url']
return json.dumps(status)
def _get_request_path(self):
if config.get_update_channel == UPDATE_STABLE:
return self.updateFile
else:
return 'https://api.github.com/repos/janeczku/calibre-web/zipball/master'
def _load_remote_data(self, repository_url):
status = {
'update': False,
'success': False,
'message': '',
'current_commit_hash': ''
}
commit = None
version = self.get_current_version_info()
if version is False:
status['current_commit_hash'] = _(u'Unknown')
else:
status['current_commit_hash'] = version['version']
try:
r = requests.get(repository_url)
commit = r.json()
r.raise_for_status()
except requests.exceptions.HTTPError as e:
if commit:
if 'message' in commit:
status['message'] = _(u'HTTP Error') + ': ' + commit['message']
else:
status['message'] = _(u'HTTP Error') + ': ' + str(e)
except requests.exceptions.ConnectionError:
status['message'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['message'] = _(u'Timeout while establishing connection')
except requests.exceptions.RequestException:
status['message'] = _(u'General error')
return status, commit
updater_thread = Updater()

@ -1,6 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 lemmsh, 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/>.
import os import os
from tempfile import gettempdir from tempfile import gettempdir
import hashlib import hashlib

@ -1,6 +1,26 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler
#
# 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/>.
import mimetypes import mimetypes
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
@ -57,6 +77,8 @@ from redirect import redirect_back
import time import time
import server import server
from reverseproxy import ReverseProxied from reverseproxy import ReverseProxied
from updater import updater_thread
import hashlib
try: try:
from googleapiclient.errors import HttpError from googleapiclient.errors import HttpError
@ -111,6 +133,9 @@ except ImportError:
# Global variables # Global variables
current_milli_time = lambda: int(round(time.time() * 1000)) current_milli_time = lambda: int(round(time.time() * 1000))
gdrive_watch_callback_token = 'target=calibreweb-watch_files' gdrive_watch_callback_token = 'target=calibreweb-watch_files'
# ToDo: Somehow caused by circular import under python3 refactor
py3_gevent_link = None
py3_restart_Typ = False
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
'fb2', 'html', 'rtf', 'odt'} 'fb2', 'html', 'rtf', 'odt'}
EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz'} EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz'}
@ -937,11 +962,9 @@ def get_email_status_json():
# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name; # example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/ # from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
def check_valid_domain(domain_text): def check_valid_domain(domain_text):
# result = session.query(Notification).from_statement(text(sql)).params(id=5).all()
#ToDo: check possible SQL injection
domain_text = domain_text.split('@',1)[-1].lower() domain_text = domain_text.split('@',1)[-1].lower()
sql = "SELECT * FROM registration WHERE '%s' LIKE domain;" % domain_text sql = "SELECT * FROM registration WHERE :domain LIKE domain;"
result = ub.session.query(ub.Registration).from_statement(text(sql)).all() result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all()
return len(result) return len(result)
@ -1139,127 +1162,7 @@ def get_matching_tags():
@app.route("/get_update_status", methods=['GET']) @app.route("/get_update_status", methods=['GET'])
@login_required_if_no_ano @login_required_if_no_ano
def get_update_status(): def get_update_status():
status = { return updater_thread.get_available_updates(request.method)
'update': False,
'success': False,
'message': '',
'current_commit_hash': ''
}
parents = []
repository_url = 'https://api.github.com/repos/janeczku/calibre-web'
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
if request.method == "GET":
version = helper.get_current_version_info()
if version is False:
status['current_commit_hash'] = _(u'Unknown')
else:
status['current_commit_hash'] = version['hash']
try:
r = requests.get(repository_url + '/git/refs/heads/master')
r.raise_for_status()
commit = r.json()
except requests.exceptions.HTTPError as e:
status['message'] = _(u'HTTP Error') + ' ' + str(e)
except requests.exceptions.ConnectionError:
status['message'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['message'] = _(u'Timeout while establishing connection')
except requests.exceptions.RequestException:
status['message'] = _(u'General error')
if status['message'] != '':
return json.dumps(status)
if 'object' not in commit:
status['message'] = _(u'Unexpected data while reading update information')
return json.dumps(status)
if commit['object']['sha'] == status['current_commit_hash']:
status.update({
'update': False,
'success': True,
'message': _(u'No update available. You already have the latest version installed')
})
return json.dumps(status)
# a new update is available
status['update'] = True
try:
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'])
r.raise_for_status()
update_data = r.json()
except requests.exceptions.HTTPError as e:
status['error'] = _(u'HTTP Error') + ' ' + str(e)
except requests.exceptions.ConnectionError:
status['error'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['error'] = _(u'Timeout while establishing connection')
except requests.exceptions.RequestException:
status['error'] = _(u'General error')
if status['message'] != '':
return json.dumps(status)
if 'committer' in update_data and 'message' in update_data:
status['success'] = True
status['message'] = _(u'A new update is available. Click on the button below to update to the latest version.')
new_commit_date = datetime.datetime.strptime(
update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parents.append(
[
format_datetime(new_commit_date, format='short', locale=get_locale()),
update_data['message'],
update_data['sha']
]
)
# it only makes sense to analyze the parents if we know the current commit hash
if status['current_commit_hash'] != '':
try:
parent_commit = update_data['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except IndexError:
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']:
try:
r = requests.get(parent_commit['url'])
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=get_locale())
parents.append([parent_commit_date, parent_data['message'], parent_data['sha']])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
else:
status['success'] = False
status['message'] = _(u'Could not fetch update information')
status['history'] = parents
return json.dumps(status)
@app.route("/get_updater_status", methods=['GET', 'POST']) @app.route("/get_updater_status", methods=['GET', 'POST'])
@ -1284,12 +1187,12 @@ def get_updater_status():
"11": _(u'Update failed:') + u' ' + _(u'General error') "11": _(u'Update failed:') + u' ' + _(u'General error')
} }
status['text'] = text status['text'] = text
helper.updater_thread = helper.Updater() # helper.updater_thread = helper.Updater()
helper.updater_thread.start() updater_thread.start()
status['status'] = helper.updater_thread.get_update_status() status['status'] = updater_thread.get_update_status()
elif request.method == "GET": elif request.method == "GET":
try: try:
status['status'] = helper.updater_thread.get_update_status() status['status'] = updater_thread.get_update_status()
except AttributeError: except AttributeError:
# thread is not active, occours after restart on update # thread is not active, occours after restart on update
status['status'] = 7 status['status'] = 7
@ -1425,7 +1328,7 @@ def author_list():
@app.route("/author/<int:book_id>", defaults={'page': 1}) @app.route("/author/<int:book_id>", defaults={'page': 1})
@app.route("/author/<int:book_id>/<int:page>'") @app.route("/author/<int:book_id>/<int:page>")
@login_required_if_no_ano @login_required_if_no_ano
def author(book_id, page): def author(book_id, page):
entries, __, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id), entries, __, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id),
@ -1821,7 +1724,12 @@ def delete_book(book_id, book_format):
@login_required @login_required
@admin_required @admin_required
def authenticate_google_drive(): def authenticate_google_drive():
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl() try:
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
except gdriveutils.InvalidConfigError:
flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'),
category="error")
return redirect(url_for('index'))
return redirect(authUrl) return redirect(authUrl)
@ -1909,7 +1817,7 @@ def on_received_watch_confirmation():
app.logger.debug(response) app.logger.debug(response)
if response: if response:
dbpath = os.path.join(config.config_calibre_dir, "metadata.db") dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
if not response['deleted'] and response['file']['title'] == 'metadata.db' and response['file']['md5Checksum'] != md5(dbpath): if not response['deleted'] and response['file']['title'] == 'metadata.db' and response['file']['md5Checksum'] != hashlib.md5(dbpath):
tmpDir = tempfile.gettempdir() tmpDir = tempfile.gettempdir()
app.logger.info('Database file updated') app.logger.info('Database file updated')
copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time()))) copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time())))
@ -1957,15 +1865,6 @@ def shutdown():
abort(404) abort(404)
@app.route("/update")
@login_required
@admin_required
def update():
helper.updater_thread = helper.Updater()
flash(_(u"Update done"), category="info")
return abort(404)
@app.route("/search", methods=["GET"]) @app.route("/search", methods=["GET"])
@login_required_if_no_ano @login_required_if_no_ano
def search(): def search():
@ -2128,10 +2027,11 @@ def advanced_search():
series=series, title=_(u"search"), cc=cc, page="advsearch") series=series, title=_(u"search"), cc=cc, page="advsearch")
@app.route("/cover/<path:cover_path>") @app.route("/cover/<book_id>")
@login_required_if_no_ano @login_required_if_no_ano
def get_cover(cover_path): def get_cover(book_id):
return helper.get_book_cover(cover_path) book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
return helper.get_book_cover(book.path)
@app.route("/show/<book_id>/<book_format>") @app.route("/show/<book_id>/<book_format>")
@ -2153,10 +2053,10 @@ def serve_book(book_id, book_format):
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format) return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
@app.route("/opds/thumb_240_240/<path:book_id>") @app.route("/opds/thumb_240_240/<book_id>")
@app.route("/opds/cover_240_240/<path:book_id>") @app.route("/opds/cover_240_240/<book_id>")
@app.route("/opds/cover_90_90/<path:book_id>") @app.route("/opds/cover_90_90/<book_id>")
@app.route("/opds/cover/<path:book_id>") @app.route("/opds/cover/<book_id>")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_get_cover(book_id): def feed_get_cover(book_id):
book = db.session.query(db.Books).filter(db.Books.id == book_id).first() book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
@ -2333,6 +2233,7 @@ def register():
content.password = generate_password_hash(password) content.password = generate_password_hash(password)
content.role = config.config_default_role content.role = config.config_default_role
content.sidebar_view = config.config_default_show content.sidebar_view = config.config_default_show
content.mature_content = bool(config.config_default_show & ub.MATURE_CONTENT)
try: try:
ub.session.add(content) ub.session.add(content)
ub.session.commit() ub.session.commit()
@ -2861,20 +2762,23 @@ def profile():
@login_required @login_required
@admin_required @admin_required
def admin(): def admin():
version = helper.get_current_version_info() version = updater_thread.get_current_version_info()
if version is False: if version is False:
commit = _(u'Unknown') commit = _(u'Unknown')
else: else:
commit = version['datetime'] if 'datetime' in version:
commit = version['datetime']
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
form_date = datetime.datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S") tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
if len(commit) > 19: # check if string has timezone form_date = datetime.datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S")
if commit[19] == '+': if len(commit) > 19: # check if string has timezone
form_date -= datetime.timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) if commit[19] == '+':
elif commit[19] == '-': form_date -= datetime.timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
form_date += datetime.timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) elif commit[19] == '-':
commit = format_datetime(form_date - tz, format='short', locale=get_locale()) form_date += datetime.timedelta(hours=int(commit[20:22]), minutes=int(commit[23:]))
commit = format_datetime(form_date - tz, format='short', locale=get_locale())
else:
commit = version['version']
content = ub.session.query(ub.User).all() content = ub.session.query(ub.User).all()
settings = ub.session.query(ub.Settings).first() settings = ub.session.query(ub.Settings).first()
@ -3010,6 +2914,8 @@ def configuration_helper(origin):
content.config_calibre_dir = to_save["config_calibre_dir"] content.config_calibre_dir = to_save["config_calibre_dir"]
db_change = True db_change = True
# Google drive setup # Google drive setup
if not os.path.isfile(os.path.join(config.get_main_dir, 'settings.yaml')):
content.config_use_google_drive = False
if "config_use_google_drive" in to_save and not content.config_use_google_drive and not gdriveError: if "config_use_google_drive" in to_save and not content.config_use_google_drive and not gdriveError:
if filedata: if filedata:
if filedata['web']['redirect_uris'][0].endswith('/'): if filedata['web']['redirect_uris'][0].endswith('/'):
@ -3099,6 +3005,8 @@ def configuration_helper(origin):
content.config_goodreads_api_key = to_save["config_goodreads_api_key"] content.config_goodreads_api_key = to_save["config_goodreads_api_key"]
if "config_goodreads_api_secret" in to_save: if "config_goodreads_api_secret" in to_save:
content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"] content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"]
if "config_updater" in to_save:
content.config_updatechannel = int(to_save["config_updater"])
if "config_log_level" in to_save: if "config_log_level" in to_save:
content.config_log_level = int(to_save["config_log_level"]) content.config_log_level = int(to_save["config_log_level"])
if content.config_logfile != to_save["config_logfile"]: if content.config_logfile != to_save["config_logfile"]:
@ -3129,7 +3037,8 @@ def configuration_helper(origin):
gdrive=gdriveutils.gdrive_support, goodreads=goodreads_support, gdrive=gdriveutils.gdrive_support, goodreads=goodreads_support,
rarfile_support=rar_support, title=_(u"Basic Configuration")) rarfile_support=rar_support, title=_(u"Basic Configuration"))
try: try:
if content.config_use_google_drive and is_gdrive_ready() and not os.path.exists(config.config_calibre_dir + "/metadata.db"): if content.config_use_google_drive and is_gdrive_ready() and not \
os.path.exists(os.path.join(content.config_calibre_dir, "metadata.db")):
gdriveutils.downloadFile(None, "metadata.db", config.config_calibre_dir + "/metadata.db") gdriveutils.downloadFile(None, "metadata.db", config.config_calibre_dir + "/metadata.db")
if db_change: if db_change:
if config.db_configured: if config.db_configured:
@ -3161,7 +3070,7 @@ def configuration_helper(origin):
app.logger.info('Reboot required, restarting') app.logger.info('Reboot required, restarting')
if origin: if origin:
success = True success = True
if is_gdrive_ready() and gdriveutils.gdrive_support == True and config.config_use_google_drive == True: if is_gdrive_ready() and gdriveutils.gdrive_support == True: # and config.config_use_google_drive == True:
gdrivefolders=gdriveutils.listRootFolders() gdrivefolders=gdriveutils.listRootFolders()
else: else:
gdrivefolders=list() gdrivefolders=list()
@ -3379,6 +3288,11 @@ def edit_user(user_id):
elif "show_sorted" not in to_save and content.show_sorted(): elif "show_sorted" not in to_save and content.show_sorted():
content.sidebar_view -= ub.SIDEBAR_SORTED content.sidebar_view -= ub.SIDEBAR_SORTED
if "show_publisher" in to_save and not content.show_publisher():
content.sidebar_view += ub.SIDEBAR_PUBLISHER
elif "show_publisher" not in to_save and content.show_publisher():
content.sidebar_view -= ub.SIDEBAR_PUBLISHER
if "show_hot" in to_save and not content.show_hot_books(): if "show_hot" in to_save and not content.show_hot_books():
content.sidebar_view += ub.SIDEBAR_HOT content.sidebar_view += ub.SIDEBAR_HOT
elif "show_hot" not in to_save and content.show_hot_books(): elif "show_hot" not in to_save and content.show_hot_books():
@ -3948,19 +3862,13 @@ def upload():
for author in db_book.authors: for author in db_book.authors:
author_names.append(author.name) author_names.append(author.name)
if len(request.files.getlist("btn-upload")) < 2: if len(request.files.getlist("btn-upload")) < 2:
cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.
datatype.notin_(db.cc_exceptions)).all()
if current_user.role_edit() or current_user.role_admin(): if current_user.role_edit() or current_user.role_admin():
return render_title_template('book_edit.html', book=book, authors=author_names, resp = {"location": url_for('edit_book', book_id=db_book.id)}
cc=cc, title=_(u"edit metadata"), page="upload") return Response(json.dumps(resp), mimetype='application/json')
book_in_shelfs = [] else:
kindle_list = helper.check_send_to_kindle(book) resp = {"location": url_for('show_book', book_id=db_book.id)}
reader_list = helper.check_read_formats(book) return Response(json.dumps(resp), mimetype='application/json')
return Response(json.dumps({"location": url_for("index")}), mimetype='application/json')
return render_title_template('detail.html', entry=book, cc=cc,
title=book.title, books_shelfs=book_in_shelfs, kindle_list=kindle_list,
reader_list=reader_list, page="upload")
return redirect(url_for("index"))
@app.route("/admin/book/convert/<int:book_id>", methods=['POST']) @app.route("/admin/book/convert/<int:book_id>", methods=['POST'])

@ -1,6 +1,22 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, bodybybuddha, janeczku
#
# 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 __future__ import print_function from __future__ import print_function
import smtplib import smtplib
import threading import threading

File diff suppressed because it is too large Load Diff

@ -4,7 +4,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.* *This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
![screenshot](https://raw.githubusercontent.com/janeczku/docker-calibre-web/master/screenshot.png) ![Main screen](../../wiki/images/main_screen.png)
## Features ## Features
@ -12,17 +12,17 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup - full graphical setup
- User management with fine grained per-user permissions - User management with fine grained per-user permissions
- Admin interface - Admin interface
- User Interface in dutch, english, french, german, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish - User Interface in dutch, english, french, german, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, ukrainian
- OPDS feed for eBook reader apps - OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language - Filter and search by titles, authors, tags, series and language
- Create custom book collection (shelves) - Create custom book collection (shelves)
- Support for editing eBook metadata and deleting eBooks from Calibre library - Support for editing eBook metadata and deleting eBooks from Calibre library
- Support for converting eBooks from EPUB to Kindle format (mobi/azw) - Support for converting eBooks through Calibre binaries
- Restrict eBook download to logged-in users - Restrict eBook download to logged-in users
- Support for public user registration - Support for public user registration
- Send eBooks to Kindle devices with the click of a button - Send eBooks to Kindle devices with the click of a button
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz) - Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz)
- Upload new books in PDF, epub, fb2 format - Upload new books in many formats
- Support for Calibre custom columns - Support for Calibre custom columns
- Ability to hide content based on categories for certain users - Ability to hide content based on categories for certain users
- Self update capability - Self update capability
@ -33,80 +33,28 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
1. Install dependencies by running `pip install --target vendor -r requirements.txt`. 1. Install dependencies by running `pip install --target vendor -r requirements.txt`.
2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window) 2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog 3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button 4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
optionally a google drive can be used to host the calibre library (-> Using Google Drive integration) Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
5. Go to Login page 5. Go to Login page
**Default admin login:** **Default admin login:**\
*Username:* admin *Username:* admin\
*Password:* admin123 *Password:* admin123
**Issues with Ubuntu:** **Issues with Ubuntu:**
Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead. Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
## Runtime Configuration Options
The configuration can be changed as admin in the admin panel under "Configuration"
Server Port:
Changes the port Calibre-Web is listening, changes take effect after pressing submit button
Enable public registration:
Tick to enable public user registration.
Enable anonymous browsing:
Tick to allow not logged in users to browse the catalog, anonymous user permissions can be set as admin ("Guest" user)
Enable uploading:
Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick library to be installed.
Enable remote login ("magic link"):
Tick to enable remote login, i.e. a link that allows user to log in via a different device.
## Requirements ## Requirements
Python 2.7+ Python 2.7+, python 3.x+
Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature: 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](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.
## Using Google Drive integration [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.
Calibre Calibre library (metadata.db) can be located on a Google Drive. Additional optional dependencys are necessary to get this work. Please install all optional requirements by executing `pip install --target vendor -r optional-requirements.txt` \*** DEPRECATED \*** Support will be removed in future releases
To use google drive integration, you have to use the google developer console to create a new app. https://console.developers.google.com [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.
Once a project has been created, we need to create a client ID and a client secret that will be used to enable the OAuth request with google, and enable the Drive API. To do this, follow the steps below: -
1. Open project in developer console
2. Click Enable API, and enable google drive
3. Now on the sidebar, click Credentials
4. Click Create Credentials and OAuth Client ID
5. Select Web Application and then next
6. Give the Credentials a name and enter your callback, which will be CALIBRE_WEB_URL/gdrive/callback
7. Click save
8. Download json file and place it in `calibre-web` directory, with the name `client_secrets.json`
The Drive API should now be setup and ready to use, so we need to integrate it into Calibre-Web. This is done as below: -
1. Open config page
2. Enter the location that will be used to store the metadata.db file locally, and to temporary store uploaded books and other temporary files for upload ("Location of Calibre database")
2. Tick Use Google Drive
3. Click the "Submit" button
4. Now select Authenticate Google Drive
5. This should redirect you to Google. After allowing it to use your Drive, it redirects you back to the config page
6. Select the folder that is the root of your calibre library on Gdrive ("Google drive Calibre folder")
7. Click the "Submit" button
8. Google Drive should now be connected and be used to get images and download Epubs. The metadata.db is stored in the calibre library location
### Optional
If your Calibre-Web is using https, it is possible to add a "watch" to the drive. This will inform us if the metadata.db file is updated and allow us to update our calibre library accordingly.
Additionally the public adress your server uses (e.g.https://example.com) has to be verified in the Google developer console. After this is done, please wait a few minutes.
9. Open config page
10. Click enable watch of metadata.db
11. Note that this expires after a week, so will need to be manually refresh
## Docker images ## Docker images
@ -122,94 +70,6 @@ Pre-built Docker images based on Alpine Linux are available in these Docker Hub
**aarch64** **aarch64**
+ **linuxserver.io** at [lsioarmhf/calibre-web-aarch64](https://hub.docker.com/r/lsioarmhf/calibre-web-aarch64) + **linuxserver.io** at [lsioarmhf/calibre-web-aarch64](https://hub.docker.com/r/lsioarmhf/calibre-web-aarch64)
## Reverse Proxy # Wiki
Reverse proxy configuration examples for apache and nginx to use Calibre-Web: For further informations, How To's and FAQ please check the ![Wiki](../../wiki)
nginx configuration for a local server listening on port 8080, mapping Calibre-Web to /calibre:
```
http {
upstream calibre {
server 127.0.0.1:8083;
}
server {
client_max_body_size 20M;
location /calibre {
proxy_bind $server_adress;
proxy_pass http://127.0.0.1:8083;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /calibre;
}
}
}
```
*Note: If using SSL in your reverse proxy on a non-standard port (e.g.12345), the following proxy_redirect line may be required:*
```
proxy_redirect http://$host/ https://$host:12345/;
```
Apache 2.4 configuration for a local server listening on port 443, mapping Calibre-Web to /calibre-web:
The following modules have to be activated: headers, proxy, rewrite.
```
Listen 443
<VirtualHost *:443>
SSLEngine on
SSLProxyEngine on
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
SSLCertificateFile "C:\Apache24\conf\ssl\test.crt"
SSLCertificateKeyFile "C:\Apache24\conf\ssl\test.key"
<Location "/calibre-web" >
RequestHeader set X-SCRIPT-NAME /calibre-web
RequestHeader set X-SCHEME https
ProxyPass http://localhost:8083/
ProxyPassReverse http://localhost:8083/
</Location>
</VirtualHost>
```
## (Optional) SSL Configuration
For configuration of calibre-web as SSL Server go to the Config page in the Admin section. Enter the certfile- and keyfile-location, optionally change port to 443 and press submit.
Afterwards the server can only be accessed via SSL. In case of a misconfiguration (wrong/invalid files) both files can be overridden via command line options
-c [certfile location] -k [keyfile location]
By using "" as file locations the server runs as non SSL server again. The correct file path can than be entered on the Config page. After the next restart without command line options the changed file paths are applied.
## Start Calibre-Web as service under Linux
Create a file "cps.service" as root in the folder /etc/systemd/system with the following content:
```[Unit]
Description=Calibre-Web
[Service]
Type=simple
User=[Username]
ExecStart=[path to python] [/PATH/TO/cps.py]
WorkingDirectory=[/PATH/TO/cps.py]
[Install]
WantedBy=multi-user.target
```
Replace the user and ExecStart with your user and foldernames.
`sudo systemctl enable cps.service`
enables the service.
## Command line options
Starting the script with `-h` lists all supported command line options
Currently supported are 2 options, which are both useful for running multiple instances of Calibre-Web
`"-p path"` allows to specify the location of the settings database
`"-g path"` allows to specify the location of the google-drive database
`"-c path"` allows to specify the location of SSL certfile, works only in combination with keyfile
`"-k path"` allows to specify the location of SSL keyfile, works only in combination with certfile

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save