mirror of
https://github.com/janeczku/calibre-web
synced 2024-12-29 03:20:30 +00:00
commit
dec50b5682
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -1,3 +1,4 @@
|
||||
web.py ident export-subst
|
||||
helper.py ident export-subst
|
||||
/test export-ignore
|
||||
cps/static/css/libs/* linguist-vendored
|
||||
cps/static/js/libs/* linguist-vendored
|
||||
|
@ -123,9 +123,11 @@ def pdf_preview(tmp_file_path, tmp_dir):
|
||||
|
||||
def get_versions():
|
||||
if not use_generic_pdf_cover:
|
||||
IVersion=ImageVersion.MAGICK_VERSION
|
||||
IVersion = ImageVersion.MAGICK_VERSION
|
||||
WVersion = ImageVersion.VERSION
|
||||
else:
|
||||
IVersion = _(u'not installed')
|
||||
WVersion = _(u'not installed')
|
||||
if use_pdf_meta:
|
||||
PVersion='v'+PyPdfVersion
|
||||
else:
|
||||
@ -134,4 +136,4 @@ def get_versions():
|
||||
XVersion = 'v'+'.'.join(map(str, lxmlversion))
|
||||
else:
|
||||
XVersion = _(u'not installed')
|
||||
return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion}
|
||||
return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion, 'Wand Version': WVersion}
|
||||
|
@ -44,5 +44,9 @@ if args.k:
|
||||
print("Keyfilepath is invalid. Exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
if (args.k and not args.c) or (not args.k and args.c):
|
||||
print("Certfile and Keyfile have to be used together. Exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
if args.k is "":
|
||||
keyfilepath = ""
|
||||
|
@ -45,5 +45,5 @@ def versioncheck():
|
||||
elif ub.config.config_ebookconverter == 2:
|
||||
return versionCalibre()
|
||||
else:
|
||||
return {'ebook_converter':''}
|
||||
return {'ebook_converter':_(u'not configured')}
|
||||
|
||||
|
@ -149,19 +149,19 @@ def getDrive(drive=None, gauth=None):
|
||||
drive.auth.Refresh()
|
||||
return drive
|
||||
|
||||
def listRootFolders(drive=None):
|
||||
drive = getDrive(drive)
|
||||
def listRootFolders():
|
||||
drive = getDrive(Gdrive.Instance().drive)
|
||||
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
|
||||
fileList = drive.ListFile({'q': folder}).GetList()
|
||||
return fileList
|
||||
|
||||
|
||||
def getEbooksFolder(drive=None):
|
||||
def getEbooksFolder(drive):
|
||||
return getFolderInFolder('root',config.config_google_drive_folder,drive)
|
||||
|
||||
|
||||
def getFolderInFolder(parentId, folderName,drive=None):
|
||||
drive = getDrive(drive)
|
||||
def getFolderInFolder(parentId, folderName, drive):
|
||||
# drive = getDrive(drive)
|
||||
query=""
|
||||
if folderName:
|
||||
query = "title = '%s' and " % folderName.replace("'", "\\'")
|
||||
@ -190,7 +190,6 @@ def getEbooksFolderId(drive=None):
|
||||
|
||||
|
||||
def getFile(pathId, fileName, drive):
|
||||
# drive = getDrive(Gdrive.Instance().drive)
|
||||
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'"))
|
||||
|
||||
fileList = drive.ListFile({'q': metaDataFile}).GetList()
|
||||
@ -200,8 +199,8 @@ def getFile(pathId, fileName, drive):
|
||||
return fileList[0]
|
||||
|
||||
|
||||
def getFolderId(path, drive=None):
|
||||
drive = getDrive(drive)
|
||||
def getFolderId(path, drive):
|
||||
# drive = getDrive(drive)
|
||||
currentFolderId = getEbooksFolderId(drive)
|
||||
sqlCheckPath = path if path[-1] == '/' else path + '/'
|
||||
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
|
||||
@ -249,7 +248,7 @@ def getFileFromEbooksFolder(path, fileName):
|
||||
return None
|
||||
|
||||
|
||||
def copyDriveFileRemote(drive, origin_file_id, copy_title):
|
||||
'''def copyDriveFileRemote(drive, origin_file_id, copy_title):
|
||||
drive = getDrive(drive)
|
||||
copied_file = {'title': copy_title}
|
||||
try:
|
||||
@ -258,7 +257,7 @@ def copyDriveFileRemote(drive, origin_file_id, copy_title):
|
||||
return drive.CreateFile({'id': file_data['id']})
|
||||
except errors.HttpError as error:
|
||||
print ('An error occurred: %s' % error)
|
||||
return None
|
||||
return None'''
|
||||
|
||||
|
||||
# Download metadata.db from gdrive
|
||||
@ -347,7 +346,6 @@ def uploadFileToEbooksFolder(destFile, f):
|
||||
|
||||
def watchChange(drive, channel_id, channel_type, channel_address,
|
||||
channel_token=None, expiration=None):
|
||||
# drive = getDrive(drive)
|
||||
# Watch for all changes to a user's Drive.
|
||||
# Args:
|
||||
# service: Drive API service instance.
|
||||
@ -390,8 +388,6 @@ def watchFile(drive, file_id, channel_id, channel_type, channel_address,
|
||||
Raises:
|
||||
apiclient.errors.HttpError: if http request to create channel fails.
|
||||
"""
|
||||
# drive = getDrive(drive)
|
||||
|
||||
body = {
|
||||
'id': channel_id,
|
||||
'type': channel_type,
|
||||
@ -413,8 +409,6 @@ def stopChannel(drive, channel_id, resource_id):
|
||||
Raises:
|
||||
apiclient.errors.HttpError: if http request to create channel fails.
|
||||
"""
|
||||
# drive = getDrive(drive)
|
||||
# service=drive.auth.service
|
||||
body = {
|
||||
'id': channel_id,
|
||||
'resourceId': resource_id
|
||||
@ -423,7 +417,6 @@ def stopChannel(drive, channel_id, resource_id):
|
||||
|
||||
|
||||
def getChangeById (drive, change_id):
|
||||
# drive = getDrive(drive)
|
||||
# Print a single Change resource information.
|
||||
#
|
||||
# Args:
|
||||
@ -454,11 +447,13 @@ def updateDatabaseOnEdit(ID,newPath):
|
||||
storedPathName.path = newPath
|
||||
session.commit()
|
||||
|
||||
|
||||
# Deletes the hashes in database of deleted book
|
||||
def deleteDatabaseEntry(ID):
|
||||
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
|
||||
session.commit()
|
||||
|
||||
|
||||
# Gets cover file from gdrive
|
||||
def get_cover_via_gdrive(cover_path):
|
||||
df = getFileFromEbooksFolder(cover_path, 'cover.jpg')
|
||||
|
182
cps/helper.py
Executable file → Normal file
182
cps/helper.py
Executable file → Normal file
@ -13,9 +13,10 @@ import unicodedata
|
||||
from io import BytesIO
|
||||
import worker
|
||||
import time
|
||||
|
||||
from flask import send_from_directory, make_response, redirect, abort
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import current_user
|
||||
from babel.dates import format_datetime
|
||||
import threading
|
||||
import shutil
|
||||
import requests
|
||||
@ -73,11 +74,14 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
||||
# read settings and append converter task to queue
|
||||
if kindle_mail:
|
||||
settings = ub.get_mail_settings()
|
||||
text = _(u"Convert: %s" % book.title)
|
||||
settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail
|
||||
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
|
||||
# text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title)
|
||||
else:
|
||||
text = _(u"Convert to %(format)s: %(book)s", format=new_book_format, book=book.title)
|
||||
settings['old_book_format'] = u'EPUB'
|
||||
settings['new_book_format'] = u'MOBI'
|
||||
settings = dict()
|
||||
text = (u"%s -> %s: %s" % (old_book_format, new_book_format, book.title))
|
||||
settings['old_book_format'] = old_book_format
|
||||
settings['new_book_format'] = new_book_format
|
||||
global_WorkerThread.add_convert(file_path, book.id, user_id, text, settings, kindle_mail)
|
||||
return None
|
||||
else:
|
||||
@ -88,7 +92,8 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
|
||||
|
||||
def send_test_mail(kindle_mail, user_name):
|
||||
global_WorkerThread.add_email(_(u'Calibre-Web test e-mail'),None, None, ub.get_mail_settings(),
|
||||
kindle_mail, user_name, _(u"Test e-mail"))
|
||||
kindle_mail, user_name, _(u"Test e-mail"),
|
||||
_(u'This e-mail has been sent via Calibre-Web.'))
|
||||
return
|
||||
|
||||
|
||||
@ -104,7 +109,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
|
||||
text += "Sincerely\r\n\r\n"
|
||||
text += "Your Calibre-Web team"
|
||||
global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, ub.get_mail_settings(),
|
||||
e_mail, user_name, _(u"Registration e-mail for user: %s" % user_name),text)
|
||||
e_mail, user_name, _(u"Registration e-mail for user: %(name)s", name=user_name), text)
|
||||
return
|
||||
|
||||
|
||||
@ -132,7 +137,7 @@ def send_mail(book_id, kindle_mail, calibrepath, user_id):
|
||||
if 'mobi' in formats:
|
||||
result = formats['mobi']
|
||||
elif 'epub' in formats:
|
||||
# returns None if sucess, otherwise errormessage
|
||||
# returns None if success, otherwise errormessage
|
||||
return convert_book_format(book_id, calibrepath, u'epub', u'mobi', user_id, kindle_mail)
|
||||
elif 'pdf' in formats:
|
||||
result = formats['pdf'] # worker.get_attachment()
|
||||
@ -140,7 +145,8 @@ def send_mail(book_id, kindle_mail, calibrepath, user_id):
|
||||
return _(u"Could not find any formats suitable for sending by e-mail")
|
||||
if result:
|
||||
global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(),
|
||||
kindle_mail, user_id, _(u"E-Mail: %s" % book.title))
|
||||
kindle_mail, user_id, _(u"E-mail: %(book)s", book=book.title),
|
||||
_(u'This e-mail has been sent via Calibre-Web.'))
|
||||
else:
|
||||
return _(u"The requested file could not be read. Maybe wrong permissions?")
|
||||
|
||||
@ -177,13 +183,18 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||
|
||||
def get_sorted_author(value):
|
||||
try:
|
||||
regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"]
|
||||
combined = "(" + ")|(".join(regexes) + ")"
|
||||
value = value.split(" ")
|
||||
if re.match(combined, value[-1].upper()):
|
||||
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
|
||||
if ',' not in value:
|
||||
regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"]
|
||||
combined = "(" + ")|(".join(regexes) + ")"
|
||||
value = value.split(" ")
|
||||
if re.match(combined, value[-1].upper()):
|
||||
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
|
||||
elif len(value) == 1:
|
||||
value2 = value[0]
|
||||
else:
|
||||
value2 = value[-1] + ", " + " ".join(value[:-1])
|
||||
else:
|
||||
value2 = value[-1] + ", " + " ".join(value[:-1])
|
||||
value2 = value
|
||||
except Exception:
|
||||
web.app.logger.error("Sorting author " + str(value) + "failed")
|
||||
value2 = value
|
||||
@ -235,18 +246,18 @@ def update_dir_structure_file(book_id, calibrepath):
|
||||
path = new_title_path
|
||||
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
|
||||
except OSError as ex:
|
||||
web.app.logger.error("Rename title from: " + path + " to " + new_title_path)
|
||||
web.app.logger.error(ex, exc_info=True)
|
||||
return _('Rename title from: "%s" to "%s" failed with error: %s' % (path, new_title_path, str(ex)))
|
||||
web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex))
|
||||
web.app.logger.debug(ex, exc_info=True)
|
||||
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_title_path, error=str(ex))
|
||||
if authordir != new_authordir:
|
||||
try:
|
||||
new_author_path = os.path.join(os.path.join(calibrepath, new_authordir), os.path.basename(path))
|
||||
os.renames(path, new_author_path)
|
||||
localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
|
||||
except OSError as ex:
|
||||
web.app.logger.error("Rename author from: " + path + " to " + new_author_path)
|
||||
web.app.logger.error(ex, exc_info=True)
|
||||
return _('Rename author from: "%s" to "%s" failed with error: %s' % (path, new_title_path, str(ex)))
|
||||
web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex))
|
||||
web.app.logger.debug(ex, exc_info=True)
|
||||
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_author_path, error=str(ex))
|
||||
return False
|
||||
|
||||
|
||||
@ -260,7 +271,6 @@ def update_dir_structure_gdrive(book_id):
|
||||
new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")"
|
||||
|
||||
if titledir != new_titledir:
|
||||
# print (titledir)
|
||||
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
|
||||
if gFile:
|
||||
gFile['title'] = new_titledir
|
||||
@ -269,7 +279,7 @@ def update_dir_structure_gdrive(book_id):
|
||||
book.path = book.path.split('/')[0] + '/' + new_titledir
|
||||
gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected
|
||||
else:
|
||||
error = _(u'File %s not found on Google Drive' % book.path) # file not found
|
||||
error = _(u'File %(file)s not found on Google Drive', file= book.path) # file not found
|
||||
|
||||
if authordir != new_authordir:
|
||||
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
|
||||
@ -278,7 +288,7 @@ def update_dir_structure_gdrive(book_id):
|
||||
book.path = new_authordir + '/' + book.path.split('/')[1]
|
||||
gd.updateDatabaseOnEdit(gFile['id'], book.path)
|
||||
else:
|
||||
error = _(u'File %s not found on Google Drive' % authordir) # file not found
|
||||
error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
|
||||
return error
|
||||
|
||||
|
||||
@ -296,7 +306,7 @@ def delete_book_gdrive(book, book_format):
|
||||
gd.deleteDatabaseEntry(gFile['id'])
|
||||
gFile.Trash()
|
||||
else:
|
||||
error =_(u'Book path %s not found on Google Drive' % book.path) # file not found
|
||||
error =_(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
|
||||
return error
|
||||
|
||||
def generate_random_password():
|
||||
@ -367,7 +377,11 @@ def do_download_file(book, book_format, data, headers):
|
||||
else:
|
||||
abort(404)
|
||||
else:
|
||||
response = make_response(send_from_directory(os.path.join(ub.config.config_calibre_dir, book.path), data.name + "." + book_format))
|
||||
filename = os.path.join(ub.config.config_calibre_dir, book.path)
|
||||
if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)):
|
||||
# ToDo: improve error handling
|
||||
web.app.logger.error('File not found: %s' % os.path.join(filename, data.name + "." + book_format))
|
||||
response = make_response(send_from_directory(filename, data.name + "." + book_format))
|
||||
response.headers = headers
|
||||
return response
|
||||
|
||||
@ -381,25 +395,37 @@ class Updater(threading.Thread):
|
||||
self.status = 0
|
||||
|
||||
def run(self):
|
||||
self.status = 1
|
||||
r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True)
|
||||
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 = 5
|
||||
db.session.close()
|
||||
db.engine.dispose()
|
||||
ub.session.close()
|
||||
ub.engine.dispose()
|
||||
self.status = 6
|
||||
server.Server.setRestartTyp(True)
|
||||
server.Server.stopServer()
|
||||
self.status = 7
|
||||
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
|
||||
@ -523,7 +549,7 @@ class Updater(threading.Thread):
|
||||
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
|
||||
shutil.rmtree(source, ignore_errors=True)
|
||||
|
||||
|
||||
|
||||
def check_unrar(unrarLocation):
|
||||
error = False
|
||||
if os.path.exists(unrarLocation):
|
||||
@ -547,3 +573,67 @@ def check_unrar(unrarLocation):
|
||||
error=True
|
||||
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 render_task_status(tasklist):
|
||||
#helper function to apply localize status information in tasklist entries
|
||||
renderedtasklist=list()
|
||||
|
||||
for task in tasklist:
|
||||
if task['user'] == current_user.nickname or current_user.role_admin():
|
||||
if task['formStarttime']:
|
||||
task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale())
|
||||
task['formStarttime'] = ""
|
||||
else:
|
||||
if 'starttime' not in task:
|
||||
task['starttime'] = ""
|
||||
|
||||
# localize the task status
|
||||
if isinstance( task['stat'], int ):
|
||||
if task['stat'] == worker.STAT_WAITING:
|
||||
task['status'] = _(u'Waiting')
|
||||
elif task['stat'] == worker.STAT_FAIL:
|
||||
task['status'] = _(u'Failed')
|
||||
elif task['stat'] == worker.STAT_STARTED:
|
||||
task['status'] = _(u'Started')
|
||||
elif task['stat'] == worker.STAT_FINISH_SUCCESS:
|
||||
task['status'] = _(u'Finished')
|
||||
else:
|
||||
task['status'] = _(u'Unknown Status')
|
||||
|
||||
# localize the task type
|
||||
if isinstance( task['taskType'], int ):
|
||||
if task['taskType'] == worker.TASK_EMAIL:
|
||||
task['taskMessage'] = _(u'E-mail: ') + task['taskMess']
|
||||
elif task['taskType'] == worker.TASK_CONVERT:
|
||||
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
|
||||
elif task['taskType'] == worker.TASK_UPLOAD:
|
||||
task['taskMessage'] = _(u'Upload: ') + task['taskMess']
|
||||
elif task['taskType'] == worker.TASK_CONVERT_ANY:
|
||||
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
|
||||
else:
|
||||
task['taskMessage'] = _(u'Unknown Task: ') + task['taskMess']
|
||||
|
||||
renderedtasklist.append(task)
|
||||
|
||||
return renderedtasklist
|
||||
|
39
cps/reverseproxy.py
Normal file
39
cps/reverseproxy.py
Normal file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class ReverseProxied(object):
|
||||
"""Wrap the application in this middleware and configure the
|
||||
front-end server to add these headers, to let you quietly bind
|
||||
this to a URL other than / and to an HTTP scheme that is
|
||||
different than what is used locally.
|
||||
|
||||
Code courtesy of: http://flask.pocoo.org/snippets/35/
|
||||
|
||||
In nginx:
|
||||
location /myprefix {
|
||||
proxy_pass http://127.0.0.1:8083;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_set_header X-Script-Name /myprefix;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
self.app = application
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
|
||||
if script_name:
|
||||
environ['SCRIPT_NAME'] = script_name
|
||||
path_info = environ.get('PATH_INFO', '')
|
||||
if path_info and path_info.startswith(script_name):
|
||||
environ['PATH_INFO'] = path_info[len(script_name):]
|
||||
|
||||
scheme = environ.get('HTTP_X_SCHEME', '')
|
||||
if scheme:
|
||||
environ['wsgi.url_scheme'] = scheme
|
||||
servr = environ.get('HTTP_X_FORWARDED_SERVER', '')
|
||||
if servr:
|
||||
environ['HTTP_HOST'] = servr
|
||||
return self.app(environ, start_response)
|
@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from socket import error as SocketError
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
import web
|
||||
|
||||
try:
|
||||
from gevent.pywsgi import WSGIServer
|
||||
from gevent.pool import Pool
|
||||
@ -17,8 +19,6 @@ except ImportError:
|
||||
from tornado import version as tornadoVersion
|
||||
gevent_present = False
|
||||
|
||||
import web
|
||||
|
||||
|
||||
class server:
|
||||
|
||||
@ -26,7 +26,8 @@ class server:
|
||||
restart= False
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
signal.signal(signal.SIGINT, self.killServer)
|
||||
signal.signal(signal.SIGTERM, self.killServer)
|
||||
|
||||
def start_gevent(self):
|
||||
try:
|
||||
@ -39,11 +40,16 @@ class server:
|
||||
else:
|
||||
self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
|
||||
self.wsgiserver.serve_forever()
|
||||
|
||||
except SocketError:
|
||||
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.serve_forever()
|
||||
try:
|
||||
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.serve_forever()
|
||||
except (OSError, SocketError) as e:
|
||||
web.app.logger.info("Error starting server: %s" % e.strerror)
|
||||
print("Error starting server: %s" % e.strerror)
|
||||
web.helper.global_WorkerThread.stop()
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
web.app.logger.info("Unknown error while starting gevent")
|
||||
|
||||
@ -53,20 +59,27 @@ class server:
|
||||
# leave subprocess out to allow forking for fetchers and processors
|
||||
self.start_gevent()
|
||||
else:
|
||||
web.app.logger.info('Starting Tornado server')
|
||||
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
|
||||
ssl={"certfile": web.ub.config.get_config_certfile(),
|
||||
"keyfile": web.ub.config.get_config_keyfile()}
|
||||
else:
|
||||
ssl=None
|
||||
# Max Buffersize set to 200MB
|
||||
http_server = HTTPServer(WSGIContainer(web.app),
|
||||
max_buffer_size = 209700000,
|
||||
ssl_options=ssl)
|
||||
http_server.listen(web.ub.config.config_port)
|
||||
self.wsgiserver=IOLoop.instance()
|
||||
self.wsgiserver.start() # wait for stop signal
|
||||
self.wsgiserver.close(True)
|
||||
try:
|
||||
web.app.logger.info('Starting Tornado server')
|
||||
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
|
||||
ssl={"certfile": web.ub.config.get_config_certfile(),
|
||||
"keyfile": web.ub.config.get_config_keyfile()}
|
||||
else:
|
||||
ssl=None
|
||||
# Max Buffersize set to 200MB
|
||||
http_server = HTTPServer(WSGIContainer(web.app),
|
||||
max_buffer_size = 209700000,
|
||||
ssl_options=ssl)
|
||||
http_server.listen(web.ub.config.config_port)
|
||||
self.wsgiserver=IOLoop.instance()
|
||||
self.wsgiserver.start()
|
||||
# wait for stop signal
|
||||
self.wsgiserver.close(True)
|
||||
except SocketError as e:
|
||||
web.app.logger.info("Error starting server: %s" % e.strerror)
|
||||
print("Error starting server: %s" % e.strerror)
|
||||
web.helper.global_WorkerThread.stop()
|
||||
sys.exit(1)
|
||||
|
||||
if self.restart == True:
|
||||
web.app.logger.info("Performing restart of Calibre-Web")
|
||||
@ -86,6 +99,9 @@ class server:
|
||||
def setRestartTyp(self,starttyp):
|
||||
self.restart=starttyp
|
||||
|
||||
def killServer(self, signum, frame):
|
||||
self.stopServer()
|
||||
|
||||
def stopServer(self):
|
||||
if gevent_present:
|
||||
self.wsgiserver.close()
|
||||
|
@ -5,6 +5,22 @@
|
||||
src: local('Grand Hotel'), local('GrandHotel-Regular'), url("fonts/GrandHotel-Regular.ttf") format('truetype');
|
||||
}
|
||||
|
||||
html.http-error {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.http-error body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
.http-error body > div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
|
||||
body { margin-bottom: 40px;}
|
||||
a{color: #45b29d}a:hover{color: #444;}
|
||||
|
@ -142,6 +142,17 @@ var languages = new Bloodhound({
|
||||
}
|
||||
});
|
||||
|
||||
var publishers = new Bloodhound({
|
||||
name: "publisher",
|
||||
datumTokenizer: function datumTokenizer(datum) {
|
||||
return [datum.name];
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||
remote: {
|
||||
url: getPath() + "/get_publishers_json?q=%QUERY"
|
||||
}
|
||||
});
|
||||
|
||||
function sourceSplit(query, cb, split, source) {
|
||||
var bhAdapter = source.ttAdapter();
|
||||
|
||||
@ -224,6 +235,20 @@ promiseLanguages.done(function() {
|
||||
);
|
||||
});
|
||||
|
||||
var promisePublishers = publishers.initialize();
|
||||
promisePublishers.done(function() {
|
||||
$("#publisher").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "publishers",
|
||||
displayKey: "name",
|
||||
source: publishers.ttAdapter()
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#search").on("change input.typeahead:selected", function() {
|
||||
var form = $("form").serialize();
|
||||
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) {
|
||||
|
@ -104,18 +104,39 @@ $(function() {
|
||||
var $this = $(this);
|
||||
var buttonText = $this.html();
|
||||
$this.html("...");
|
||||
$("#update_error").addClass("hidden")
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../get_update_status",
|
||||
success: function success(data) {
|
||||
$this.html(buttonText);
|
||||
if (data.status === true) {
|
||||
$("#check_for_update").addClass("hidden");
|
||||
$("#perform_update").removeClass("hidden");
|
||||
$("#update_info")
|
||||
.removeClass("hidden")
|
||||
.find("span").html(data.commit);
|
||||
|
||||
var cssClass = '';
|
||||
var message = ''
|
||||
|
||||
if (data.success === true) {
|
||||
if (data.update === true) {
|
||||
$("#check_for_update").addClass("hidden");
|
||||
$("#perform_update").removeClass("hidden");
|
||||
$("#update_info")
|
||||
.removeClass("hidden")
|
||||
.find("span").html(data.commit);
|
||||
|
||||
data.history.reverse().forEach(function(entry, index) {
|
||||
$("<tr><td>" + entry[0] + "</td><td>" + entry[1] + "</td></tr>").appendTo($("#update_table"));
|
||||
});
|
||||
cssClass = 'alert-warning'
|
||||
} else {
|
||||
cssClass = 'alert-success'
|
||||
}
|
||||
} else {
|
||||
cssClass = 'alert-danger'
|
||||
}
|
||||
|
||||
message = '<div class="alert ' + cssClass
|
||||
+ ' fade in"><a href="#" class="close" data-dismiss="alert">×</a>' + data.message + '</div>';
|
||||
|
||||
$(message).insertAfter($("#update_table"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,105 +1,140 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="discover">
|
||||
<h2>{{_('User list')}}</h2>
|
||||
<table class="table table-striped" id="table_user">
|
||||
<tr>
|
||||
<th>{{_('Nickname')}}</th>
|
||||
<th>{{_('E-mail')}}</th>
|
||||
<th>{{_('Kindle')}}</th>
|
||||
<th>{{_('DLS')}}</th>
|
||||
<th class="hidden-xs">{{_('Admin')}}</th>
|
||||
<th class="hidden-xs">{{_('Download')}}</th>
|
||||
<th class="hidden-xs">{{_('Upload')}}</th>
|
||||
<th class="hidden-xs">{{_('Edit')}}</th>
|
||||
</tr>
|
||||
{% for user in content %}
|
||||
{% if not user.role_anonymous() or config.config_anonbrowse %}
|
||||
<tr>
|
||||
<td><a href="{{url_for('edit_user', user_id=user.id)}}">{{user.nickname}}</a></td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>{{user.kindle_mail}}</td>
|
||||
<td>{{user.downloads.count()}}</td>
|
||||
<td class="hidden-xs">{% if user.role_admin() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
<td class="hidden-xs">{% if user.role_download() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
<td class="hidden-xs">{% if user.role_upload() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
<td class="hidden-xs">{% if user.role_edit() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="btn btn-default" id="admin_new_user"><a href="{{url_for('new_user')}}">{{_('Add new user')}}</a></div>
|
||||
<h2>{{_('SMTP e-mail server settings')}}</h2>
|
||||
<table class="table table-striped" id="table_email">
|
||||
<tr>
|
||||
<th>{{_('SMTP hostname')}}</th>
|
||||
<th>{{_('SMTP port')}}</th>
|
||||
<th>{{_('SSL')}}</th>
|
||||
<th>{{_('SMTP login')}}</th>
|
||||
<th class="hidden-xs">{{_('From mail')}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{email.mail_server}}</td>
|
||||
<td>{{email.mail_port}}</td>
|
||||
<td>{% if email.mail_use_ssl %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
<td>{{email.mail_login}}</td>
|
||||
<td class="hidden-xs">{{email.mail_from}}</td>
|
||||
</table>
|
||||
<div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div>
|
||||
<div id="container">
|
||||
<h2>{{_('Configuration')}}</h2>
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="Row">
|
||||
<div class="col-xs-6 col-sm-6">{{_('Calibre DB dir')}}</div>
|
||||
<div class="col-xs-6 col-sm-6">{{config.config_calibre_dir}}</div>
|
||||
</div>
|
||||
<div class="Row">
|
||||
<div class="col-xs-6 col-sm-6">{{_('Log level')}}</div>
|
||||
<div class="col-xs-6 col-sm-6">{{config.get_Log_Level()}}</div>
|
||||
</div>
|
||||
<div class="Row">
|
||||
<div class="col-xs-6 col-sm-6">{{_('Port')}}</div>
|
||||
<div class="col-xs-6 col-sm-6">{{config.config_port}}</div>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>{{_('User list')}}</h2>
|
||||
<table class="table table-striped" id="table_user">
|
||||
<tr>
|
||||
<th>{{_('Nickname')}}</th>
|
||||
<th>{{_('E-mail')}}</th>
|
||||
<th>{{_('Kindle')}}</th>
|
||||
<th>{{_('DLS')}}</th>
|
||||
<th class="hidden-xs">{{_('Admin')}}</th>
|
||||
<th class="hidden-xs">{{_('Download')}}</th>
|
||||
<th class="hidden-xs">{{_('Upload')}}</th>
|
||||
<th class="hidden-xs">{{_('Edit')}}</th>
|
||||
</tr>
|
||||
{% for user in content %}
|
||||
{% if not user.role_anonymous() or config.config_anonbrowse %}
|
||||
<tr>
|
||||
<td><a href="{{url_for('edit_user', user_id=user.id)}}">{{user.nickname}}</a></td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>{{user.kindle_mail}}</td>
|
||||
<td>{{user.downloads.count()}}</td>
|
||||
<td class="hidden-xs">{% if user.role_admin() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
<td class="hidden-xs">{% if user.role_download() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
<td class="hidden-xs">{% if user.role_upload() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
<td class="hidden-xs">{% if user.role_edit() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="btn btn-default" id="admin_new_user"><a href="{{url_for('new_user')}}">{{_('Add new user')}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="Row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Books per page')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{{config.config_books_per_page}}</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>{{_('SMTP e-mail server settings')}}</h2>
|
||||
<table class="table table-striped" id="table_email">
|
||||
<tr>
|
||||
<th>{{_('SMTP hostname')}}</th>
|
||||
<th>{{_('SMTP port')}}</th>
|
||||
<th>{{_('SSL')}}</th>
|
||||
<th>{{_('SMTP login')}}</th>
|
||||
<th class="hidden-xs">{{_('From mail')}}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{email.mail_server}}</td>
|
||||
<td>{{email.mail_port}}</td>
|
||||
<td>{% if email.mail_use_ssl %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
|
||||
<td>{{email.mail_login}}</td>
|
||||
<td class="hidden-xs">{{email.mail_from}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div>
|
||||
</div>
|
||||
<div class="Row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Uploading')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{% if config.config_uploading %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>{{_('Configuration')}}</h2>
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-6">{{_('Calibre DB dir')}}</div>
|
||||
<div class="col-xs-6 col-sm-6">{{config.config_calibre_dir}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-6">{{_('Log level')}}</div>
|
||||
<div class="col-xs-6 col-sm-6">{{config.get_Log_Level()}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-6">{{_('Port')}}</div>
|
||||
<div class="col-xs-6 col-sm-6">{{config.config_port}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Books per page')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{{config.config_books_per_page}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Uploading')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{% if config.config_uploading %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Anonymous browsing')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{% if config.config_anonbrowse %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Public registration')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{% if config.config_public_reg %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Remote login')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{% if config.config_remote_login %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-default"><a id="basic_config" href="{{url_for('configuration')}}">{{_('Basic Configuration')}}</a></div>
|
||||
<div class="btn btn-default"><a id="view_config" href="{{url_for('view_configuration')}}">{{_('UI Configuration')}}</a></div>
|
||||
</div>
|
||||
<div class="Row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Anonymous browsing')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{% if config.config_anonbrowse %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>{{_('Administration')}}</h2>
|
||||
<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_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
|
||||
</div>
|
||||
<div class="Row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Public registration')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{% if config.config_public_reg %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
|
||||
</div>
|
||||
<div class="Row">
|
||||
<div class="col-xs-6 col-sm-7">{{_('Remote login')}}</div>
|
||||
<div class="col-xs-6 col-sm-5">{% if config.config_remote_login %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>{{_('Update')}}</h2>
|
||||
<table class="table table-striped" id="update_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-xs-3">{{_('Version')}}</th>
|
||||
<th class="col-xl-8">{{_('Details')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr id="current_version">
|
||||
<td>{{commit}} </td>
|
||||
<td><i>{{_('Current version')}}</i></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
|
||||
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>
|
||||
<div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#UpdateprogressDialog">{{_('Perform Update')}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12">
|
||||
<p></p>
|
||||
<div class="btn btn-default"><a href="{{url_for('configuration')}}">{{_('Basic Configuration')}}</a></div>
|
||||
<div class="btn btn-default"><a href="{{url_for('view_configuration')}}">{{_('UI Configuration')}}</a></div>
|
||||
</div>
|
||||
<h2>{{_('Administration')}}</h2>
|
||||
<div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div>
|
||||
<div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div>
|
||||
<p></p>
|
||||
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
|
||||
<div class="btn btn-default" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div>
|
||||
<div class="btn btn-default" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
|
||||
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>
|
||||
<div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#UpdateprogressDialog">{{_('Perform Update')}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div id="RestartDialog" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
@ -130,7 +165,6 @@
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="UpdateprogressDialog" class="modal fade" role="dialog">
|
||||
|
@ -36,7 +36,9 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
|
||||
|
@ -6,9 +6,9 @@
|
||||
<div class="col-sm-3 col-lg-3 col-xs-12">
|
||||
<div class="cover">
|
||||
{% if book.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" />
|
||||
<img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" alt="{{ book.title }}"/>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" />
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if g.user.role_delete_books() %}
|
||||
@ -26,7 +26,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if display_convertbtn and conversion_formats|length > 0 %}
|
||||
{% if source_formats|length > 0 and conversion_formats|length > 0 %}
|
||||
<div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4>
|
||||
<form class="padded-bottom" action="{{ url_for('convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
|
||||
<div class="form-group">
|
||||
@ -34,8 +34,8 @@
|
||||
<label class="control-label" for="book_format_from">{{_('Convert from:')}}</label>
|
||||
<select class="form-control" name="book_format_from" id="book_format_from">
|
||||
<option disabled selected value> -- {{_('select an option')}} -- </option>
|
||||
{% for file in book.data %}
|
||||
<option>{{file.format}} </option>
|
||||
{% for format in source_formats %}
|
||||
<option>{{format|upper}} </option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label class="control-label" for="book_format_to">{{_('Convert to:')}}</label>
|
||||
@ -101,7 +101,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="publisher">{{_('Publisher')}}</label>
|
||||
<input type="text" class="form-control typeahead" name="publisher" id="publisher" value="{% if book.publishers|length > 0 %}{{book.publishers[0].name}}{% endif %}" disabled>
|
||||
<input type="text" class="form-control typeahead" name="publisher" id="publisher" value="{% if book.publishers|length > 0 %}{{book.publishers[0].name}}{% endif %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="languages">{{_('Language')}}</label>
|
||||
@ -174,7 +174,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<a href="#" id="get_meta" class="btn btn-default" data-toggle="modal" data-target="#metaModal">{{_('Get metadata')}}</a>
|
||||
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
<button type="submit" id="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
<a href="{{ url_for('show_book', book_id=book.id) }}" class="btn btn-default">{{_('Back')}}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -207,12 +207,12 @@
|
||||
|
||||
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
{% if not origin %}
|
||||
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a>
|
||||
{% endif %}
|
||||
{% if success %}
|
||||
<a href="{{ url_for('login') }}" class="btn btn-default">{{_('Login')}}</a>
|
||||
<a href="{{ url_for('login') }}" name="login" class="btn btn-default">{{_('Login')}}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -143,6 +143,10 @@
|
||||
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
|
||||
<label for="show_author">{{_('Show author selection')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
|
||||
<label for="show_publisher">{{_('Show publisher selection')}}</label>
|
||||
</div>
|
||||
<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 %}>
|
||||
<label for="show_read_and_unread">{{_('Show read and unread')}}</label>
|
||||
@ -160,7 +164,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -60,7 +60,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<h2>{{entry.title|shortentitle(40)}}</h2>
|
||||
<h2 id="title">{{entry.title|shortentitle(40)}}</h2>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
|
||||
@ -120,13 +120,17 @@
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.publishers|length > 0 %}
|
||||
<div class="publishers">
|
||||
<p>
|
||||
<span>{{_('Publisher')}}:{% for publisher in entry.publishers %} {{publisher.name}}{% if not loop.last %},{% endif %}{% endfor %}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>{{_('Publisher')}}:
|
||||
<a href="{{url_for('publisher', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.pubdate[:10] != '0101-01-01' %}
|
||||
<p>{{_('Publishing date')}}: {{entry.pubdate|formatdate}} </p>
|
||||
{% endif %}
|
||||
@ -174,8 +178,10 @@
|
||||
|
||||
|
||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
|
||||
<h3>{{_('Description:')}}</h3>
|
||||
{{entry.comments[0].text|safe}}
|
||||
<div class="comments">
|
||||
<h3>{{_('Description:')}}</h3>
|
||||
{{entry.comments[0].text|safe}}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@ -248,7 +254,7 @@
|
||||
{% if g.user.role_edit() %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group" aria-label="Edit/Delete book">
|
||||
<a href="{{ url_for('edit_book', book_id=entry.id) }}" class="btn btn-sm btn-warning" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit metadata')}}</a>
|
||||
<a href="{{ url_for('edit_book', book_id=entry.id) }}" class="btn btn-sm btn-warning" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit metadata')}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -14,7 +14,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
|
@ -27,7 +27,10 @@
|
||||
href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}"
|
||||
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
|
||||
{% endif %}
|
||||
<link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
|
||||
<link rel="search"
|
||||
href="{{url_for('feed_osd')}}"
|
||||
type="application/opensearchdescription+xml"/>
|
||||
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
|
||||
<title>{{instance}}</title>
|
||||
<author>
|
||||
<name>{{instance}}</name>
|
||||
@ -40,9 +43,16 @@
|
||||
<title>{{entry.title}}</title>
|
||||
<id>{{entry.uuid}}</id>
|
||||
<updated>{{entry.atom_timestamp}}</updated>
|
||||
<author>
|
||||
<name>{{entry.authors[0].name}}</name>
|
||||
</author>
|
||||
{% if entry.authors.__len__() > 0 %}
|
||||
<author>
|
||||
<name>{{entry.authors[0].name}}</name>
|
||||
</author>
|
||||
{% endif %}
|
||||
{% if entry.publishers.__len__() > 0 %}
|
||||
<publisher>
|
||||
<name>{{entry.publishers[0].name}}</name>
|
||||
</publisher>
|
||||
{% endif %}
|
||||
<dcterms:language>{{entry.language}}</dcterms:language>
|
||||
{% for tag in entry.tags %}
|
||||
<category scheme="http://www.bisg.org/standards/bisac_subject/index.html"
|
||||
|
26
cps/templates/http_error.html
Normal file
26
cps/templates/http_error.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="http-error" lang="{{ g.user.locale }}">
|
||||
<head>
|
||||
<title>{{ instance }} | HTTP Error ({{ error_code }})</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<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 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">
|
||||
{% if g.user.get_theme == 1 %}
|
||||
<link href="{{ url_for('static', filename='css/caliBlur-style.css') }}" rel="stylesheet" media="screen">
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container text-center">
|
||||
<h1>{{ error_code }}</h1>
|
||||
<h3>{{ error_name }}</h3>
|
||||
<a href="{{url_for('index')}}" title="{{ _('Back to home') }}">{{_('Back to home')}}</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -17,7 +17,9 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
@ -60,7 +62,9 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
|
||||
|
@ -5,7 +5,10 @@
|
||||
<link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link title="{{_('Search')}}" type="application/opensearchdescription+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
|
||||
<link rel="search"
|
||||
href="{{url_for('feed_osd')}}"
|
||||
type="application/opensearchdescription+xml"/>
|
||||
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
|
||||
<title>{{instance}}</title>
|
||||
<author>
|
||||
<name>{{instance}}</name>
|
||||
@ -39,7 +42,7 @@
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Show Random Books')}}</content>
|
||||
</entry>
|
||||
{% if not current_user.is_anonymous %}
|
||||
{% if not current_user.is_anonymous %}
|
||||
<entry>
|
||||
<title>{{_('Read Books')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
@ -47,7 +50,7 @@
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Read Books')}}</content>
|
||||
</entry>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<entry>
|
||||
<title>{{_('Unread Books')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
@ -61,35 +64,42 @@
|
||||
<id>{{url_for('feed_authorindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books ordered by Author')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Publishers')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_publisherindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books ordered by publisher')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Category list')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_categoryindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books ordered by category')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Series list')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_seriesindex')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_seriesindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books ordered by series')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Public Shelves')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_shelfindex', public="public")}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_shelfindex', public="public")}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_('Books organized in public shelfs, visible to everyone')}}</content>
|
||||
</entry>
|
||||
{% if not current_user.is_anonymous %}
|
||||
<entry>
|
||||
</entry>
|
||||
{% if not current_user.is_anonymous %}
|
||||
<entry>
|
||||
<title>{{_('Your Shelves')}}</title>
|
||||
<link rel="subsection" href="{{url_for('feed_shelfindex')}}" type="application/atom+xml;profile=opds-catalog"/>
|
||||
<id>{{url_for('feed_shelfindex')}}</id>
|
||||
<updated>{{ current_time }}</updated>
|
||||
<content type="text">{{_("User's own shelfs, only visible to the current user himself")}}</content>
|
||||
</entry>
|
||||
{% endif %}
|
||||
</entry>
|
||||
{% endif %}
|
||||
</feed>
|
||||
|
@ -122,16 +122,16 @@
|
||||
<li id="nav_new" {% if page == 'root' %}class="active"{% endif %}><a href="{{url_for('index')}}"><span class="glyphicon glyphicon-book"></span> {{_('Recently Added')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.show_sorted() %}
|
||||
<li class="dropdown">
|
||||
<li id="nav_sort" class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-sort-by-attributes"></span>{{_('Sorted Books')}}
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li {% if page == 'newest' %}class="active"{% endif %}><a href="{{url_for('newest_books')}}">{{_('Sort By')}} {{_('Newest')}}</a></li>
|
||||
<li {% if page == 'oldest' %}class="active"{% endif %}><a href="{{url_for('oldest_books')}}">{{_('Sort By')}} {{_('Oldest')}}</a></li>
|
||||
<li {% if page == 'a-z' %}class="active"{% endif %}><a href="{{url_for('titles_ascending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Ascending')}})</a></li>
|
||||
<li {% if page == 'z-a' %}class="active"{% endif %}><a href="{{url_for('titles_descending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Descending')}})</a></li>
|
||||
<li id="nav_sort_old" {% if page == 'newest' %}class="active"{% endif %}><a href="{{url_for('newest_books')}}">{{_('Sort By')}} {{_('Newest')}}</a></li>
|
||||
<li id="nav_sort_new" {% if page == 'oldest' %}class="active"{% endif %}><a href="{{url_for('oldest_books')}}">{{_('Sort By')}} {{_('Oldest')}}</a></li>
|
||||
<li id="nav_sort_asc" {% if page == 'a-z' %}class="active"{% endif %}><a href="{{url_for('titles_ascending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Ascending')}})</a></li>
|
||||
<li id="nav_sort_desc" {% if page == 'z-a' %}class="active"{% endif %}><a href="{{url_for('titles_descending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Descending')}})</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{%endif%}
|
||||
@ -139,13 +139,13 @@
|
||||
<li id="nav_hot" {% if page == 'hot' %}class="active"{% endif %}><a href="{{url_for('hot_books')}}"><span class="glyphicon glyphicon-fire"></span>{{_('Hot Books')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.show_best_rated_books() %}
|
||||
<li {% if page == 'rated' %}class="active"{% endif %}><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span>{{_('Best rated Books')}}</a></li>
|
||||
<li id="nav_rated" {% if page == 'rated' %}class="active"{% endif %}><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span>{{_('Best rated Books')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.show_read_and_unread() %}
|
||||
{% if not g.user.is_anonymous %}
|
||||
<li {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('read_books')}}"><span class="glyphicon glyphicon-eye-open"></span>{{_('Read Books')}}</a></li>
|
||||
<li id="nav_read" {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('read_books')}}"><span class="glyphicon glyphicon-eye-open"></span>{{_('Read Books')}}</a></li>
|
||||
{%endif%}
|
||||
<li {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('unread_books')}}"><span class="glyphicon glyphicon-eye-close"></span>{{_('Unread Books')}}</a></li>
|
||||
<li id="nav_unread" {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('unread_books')}}"><span class="glyphicon glyphicon-eye-close"></span>{{_('Unread Books')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.show_random_books() %}
|
||||
<li id="nav_rand" {% if page == 'discover' %}class="active"{% endif %}><a href="{{url_for('discover')}}"><span class="glyphicon glyphicon-random"></span>{{_('Discover')}}</a></li>
|
||||
@ -159,17 +159,20 @@
|
||||
{% if g.user.show_author() %}
|
||||
<li id="nav_author" {% if page == 'author' %}class="active"{% endif %}><a href="{{url_for('author_list')}}"><span class="glyphicon glyphicon-user"></span>{{_('Authors')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.show_publisher() %}
|
||||
<li id="nav_publisher" {% if page == 'publisher' %}class="active"{% endif %}><a href="{{url_for('publisher_list')}}"><span class="glyphicon glyphicon-text-size"></span>{{_('Publishers')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.filter_language() == 'all' and g.user.show_language() %}
|
||||
<li id="nav_lang" {% if page == 'language' %}class="active"{% endif %}><a href="{{url_for('language_overview')}}"><span class="glyphicon glyphicon-flag"></span>{{_('Languages')}} </a></li>
|
||||
{%endif%}
|
||||
{% if g.user.is_authenticated or g.user.is_anonymous %}
|
||||
<li class="nav-head hidden-xs">{{_('Public Shelves')}}</li>
|
||||
{% for shelf in g.public_shelfes %}
|
||||
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list"></span>{{shelf.name}}</a></li>
|
||||
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list public_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
|
||||
{% endfor %}
|
||||
<li class="nav-head hidden-xs">{{_('Your Shelves')}}</li>
|
||||
{% for shelf in g.user.shelf %}
|
||||
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list"></span>{{shelf.name}}</a></li>
|
||||
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
|
||||
{% endfor %}
|
||||
{% if not g.user.is_anonymous %}
|
||||
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('create_shelf')}}">{{_('Create a Shelf')}}</a></li>
|
||||
|
@ -7,15 +7,11 @@
|
||||
<label for="nickname">{{_('Username')}}</label>
|
||||
<input type="text" class="form-control" id="nickname" name="nickname" placeholder="{{_('Choose a username')}}" required>
|
||||
</div>
|
||||
<!--div class="form-group required">
|
||||
<label for="password">{{_('Password')}}</label>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="{{_('Choose a password')}}" required>
|
||||
</div-->
|
||||
<div class="form-group required">
|
||||
<label for="email">{{_('E-mail address')}}</label>
|
||||
<input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{{_('Register')}}</button>
|
||||
<button type="submit" id="submit" class="btn btn-primary">{{_('Register')}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% if error %}
|
||||
|
@ -5,7 +5,7 @@
|
||||
<h2>{{_('No Results for:')}} {{searchterm}}</h2>
|
||||
<p>{{_('Please try a different search')}}</p>
|
||||
{% else %}
|
||||
<h2>{{entries|length}} {{_('Results for:')}} {{searchterm}}</h2>
|
||||
<h2>{{entries|length}} {{_('Results for:')}} {{searchterm}}</h2>
|
||||
{% if g.user.is_authenticated %}
|
||||
{% if g.user.shelf.all() or g.public_shelfes %}
|
||||
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
|
||||
@ -41,7 +41,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
|
||||
|
@ -4,9 +4,9 @@
|
||||
<h2>{{title}}</h2>
|
||||
{% if g.user.is_authenticated %}
|
||||
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
|
||||
<div data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
|
||||
<a href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a>
|
||||
<a href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
|
||||
<div id="delete_shelf" data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
|
||||
<a id="edit_shelf" href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a>
|
||||
<a id="order_shelf" href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
@ -14,14 +14,18 @@
|
||||
{% for entry in entries %}
|
||||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
<div class="cover">
|
||||
{% 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">
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
{% if entry.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" />
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
|
||||
<p class="title">{{entry.title|shortentitle}}</p>
|
||||
</a>
|
||||
<p class="author">
|
||||
{% for author in entry.authors %}
|
||||
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
|
||||
@ -56,7 +60,7 @@
|
||||
<div class="modal-body text-center">
|
||||
<span>{{_('Shelf will be lost for everybody and forever!')}}</span>
|
||||
<p></p>
|
||||
<a href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('Ok')}}</a>
|
||||
<a id="confirm" href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('Ok')}}</a>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,7 +14,7 @@
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
<button type="submit" class="btn btn-default" id="submit">{{_('Submit')}}</button>
|
||||
{% if shelf.id != None %}
|
||||
<a href="{{ url_for('show_shelf', shelf_id=shelf.id) }}" class="btn btn-default">{{_('Back')}}</a>
|
||||
{% endif %}
|
||||
|
@ -11,7 +11,7 @@
|
||||
{% if g.user.role_admin() %}
|
||||
<th data-halign="right" data-align="right" data-field="user" data-sortable="true">{{_('User')}}</th>
|
||||
{% endif %}
|
||||
<th data-halign="right" data-align="right" data-field="type" data-sortable="true">{{_('Task')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="taskMessage" data-sortable="true">{{_('Task')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="status" data-sortable="true">{{_('Status')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
|
||||
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Runtime')}}</th>
|
||||
|
@ -22,8 +22,6 @@
|
||||
<input type="password" class="form-control" name="password" id="password" value="" autocomplete="off">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label for="kindle_mail">{{_('Kindle E-Mail')}}</label>
|
||||
@ -91,6 +89,10 @@
|
||||
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
|
||||
<label for="show_author">{{_('Show author selection')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
|
||||
<label for="show_publisher">{{_('Show publisher selection')}}</label>
|
||||
</div>
|
||||
<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 %}>
|
||||
<label for="show_read_and_unread">{{_('Show read and unread')}}</label>
|
||||
@ -142,7 +144,7 @@
|
||||
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="delete"> {{_('Delete this user')}}
|
||||
<input type="checkbox" id="delete" name="delete"> {{_('Delete this user')}}
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -151,8 +153,8 @@
|
||||
<button type="submit" id="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
{% if not profile %}
|
||||
<a href="{{ url_for('admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if downloads %}
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
cps/translations/hu/LC_MESSAGES/messages.mo
Normal file
BIN
cps/translations/hu/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
1938
cps/translations/hu/LC_MESSAGES/messages.po
Normal file
1938
cps/translations/hu/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
18430
cps/translations/iso639.pickle
Normal file
18430
cps/translations/iso639.pickle
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
12
cps/ub.py
12
cps/ub.py
@ -41,6 +41,7 @@ SIDEBAR_READ_AND_UNREAD = 256
|
||||
SIDEBAR_RECENT = 512
|
||||
SIDEBAR_SORTED = 1024
|
||||
MATURE_CONTENT = 2048
|
||||
SIDEBAR_PUBLISHER = 4096
|
||||
|
||||
DEFAULT_PASS = "admin123"
|
||||
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
|
||||
@ -136,6 +137,9 @@ class UserBase:
|
||||
def show_author(self):
|
||||
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
|
||||
|
||||
def show_publisher(self):
|
||||
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
|
||||
|
||||
def show_best_rated_books(self):
|
||||
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
|
||||
|
||||
@ -297,7 +301,7 @@ class Settings(Base):
|
||||
config_anonbrowse = Column(SmallInteger, default=0)
|
||||
config_public_reg = Column(SmallInteger, default=0)
|
||||
config_default_role = Column(SmallInteger, default=0)
|
||||
config_default_show = Column(SmallInteger, default=2047)
|
||||
config_default_show = Column(SmallInteger, default=6143)
|
||||
config_columns_to_ignore = Column(String)
|
||||
config_use_google_drive = Column(Boolean)
|
||||
config_google_drive_folder = Column(String)
|
||||
@ -485,6 +489,10 @@ class Config:
|
||||
return bool((self.config_default_show is not None) and
|
||||
(self.config_default_show & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
|
||||
|
||||
def show_publisher(self):
|
||||
return bool((self.config_default_show is not None) and
|
||||
(self.config_default_show & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
|
||||
|
||||
def show_best_rated_books(self):
|
||||
return bool((self.config_default_show is not None) and
|
||||
(self.config_default_show & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
|
||||
@ -740,7 +748,7 @@ def create_admin_user():
|
||||
user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_DELETE_BOOKS + ROLE_PASSWD
|
||||
user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \
|
||||
SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED + SIDEBAR_READ_AND_UNREAD + SIDEBAR_RECENT + \
|
||||
SIDEBAR_SORTED + MATURE_CONTENT
|
||||
SIDEBAR_SORTED + MATURE_CONTENT + SIDEBAR_PUBLISHER
|
||||
|
||||
user.password = generate_password_hash(DEFAULT_PASS)
|
||||
|
||||
|
1022
cps/web.py
1022
cps/web.py
File diff suppressed because it is too large
Load Diff
135
cps/worker.py
135
cps/worker.py
@ -33,12 +33,12 @@ from email.utils import formatdate
|
||||
from email.utils import make_msgid
|
||||
|
||||
chunksize = 8192
|
||||
|
||||
# task 'status' consts
|
||||
STAT_WAITING = 0
|
||||
STAT_FAIL = 1
|
||||
STAT_STARTED = 2
|
||||
STAT_FINISH_SUCCESS = 3
|
||||
|
||||
#taskType consts
|
||||
TASK_EMAIL = 1
|
||||
TASK_CONVERT = 2
|
||||
TASK_UPLOAD = 3
|
||||
@ -169,12 +169,12 @@ class WorkerThread(threading.Thread):
|
||||
doLock.acquire()
|
||||
if self.current != self.last:
|
||||
doLock.release()
|
||||
if self.queue[self.current]['typ'] == TASK_EMAIL:
|
||||
self.send_raw_email()
|
||||
if self.queue[self.current]['typ'] == TASK_CONVERT:
|
||||
self.convert_any_format()
|
||||
if self.queue[self.current]['typ'] == TASK_CONVERT_ANY:
|
||||
self.convert_any_format()
|
||||
if self.queue[self.current]['taskType'] == TASK_EMAIL:
|
||||
self._send_raw_email()
|
||||
if self.queue[self.current]['taskType'] == TASK_CONVERT:
|
||||
self._convert_any_format()
|
||||
if self.queue[self.current]['taskType'] == TASK_CONVERT_ANY:
|
||||
self._convert_any_format()
|
||||
# TASK_UPLOAD is handled implicitly
|
||||
self.current += 1
|
||||
else:
|
||||
@ -190,7 +190,7 @@ class WorkerThread(threading.Thread):
|
||||
else:
|
||||
return "0 %"
|
||||
|
||||
def delete_completed_tasks(self):
|
||||
def _delete_completed_tasks(self):
|
||||
for index, task in reversed(list(enumerate(self.UIqueue))):
|
||||
if task['progress'] == "100 %":
|
||||
# delete tasks
|
||||
@ -202,40 +202,55 @@ class WorkerThread(threading.Thread):
|
||||
|
||||
def get_taskstatus(self):
|
||||
if self.current < len(self.queue):
|
||||
if self.queue[self.current]['status'] == STAT_STARTED:
|
||||
if self.queue[self.current]['typ'] == TASK_EMAIL:
|
||||
if self.UIqueue[self.current]['stat'] == STAT_STARTED:
|
||||
if self.queue[self.current]['taskType'] == TASK_EMAIL:
|
||||
self.UIqueue[self.current]['progress'] = self.get_send_status()
|
||||
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
|
||||
datetime.now() - self.queue[self.current]['starttime'])
|
||||
return self.UIqueue
|
||||
|
||||
def convert_any_format(self):
|
||||
def _convert_any_format(self):
|
||||
# convert book, and upload in case of google drive
|
||||
self.queue[self.current]['status'] = STAT_STARTED
|
||||
self.UIqueue[self.current]['status'] = _('Started')
|
||||
self.UIqueue[self.current]['stat'] = STAT_STARTED
|
||||
self.queue[self.current]['starttime'] = datetime.now()
|
||||
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
|
||||
curr_task = self.queue[self.current]['typ']
|
||||
filename = self.convert_ebook_format()
|
||||
curr_task = self.queue[self.current]['taskType']
|
||||
filename = self._convert_ebook_format()
|
||||
if filename:
|
||||
if web.ub.config.config_use_google_drive:
|
||||
gd.updateGdriveCalibreFromLocal()
|
||||
if curr_task == TASK_CONVERT:
|
||||
self.add_email(_(u'Send to Kindle'), self.queue[self.current]['path'], filename,
|
||||
self.queue[self.current]['settings'], self.queue[self.current]['kindle'],
|
||||
self.UIqueue[self.current]['user'], _(u"E-mail: %s" % self.queue[self.current]['title']))
|
||||
self.add_email(self.queue[self.current]['settings']['subject'], self.queue[self.current]['path'],
|
||||
filename, self.queue[self.current]['settings'], self.queue[self.current]['kindle'],
|
||||
self.UIqueue[self.current]['user'], self.queue[self.current]['title'],
|
||||
self.queue[self.current]['settings']['body'])
|
||||
|
||||
|
||||
def convert_ebook_format(self):
|
||||
def _convert_ebook_format(self):
|
||||
error_message = None
|
||||
file_path = self.queue[self.current]['file_path']
|
||||
bookid = self.queue[self.current]['bookid']
|
||||
format_old_ext = u'.' + self.queue[self.current]['settings']['old_book_format'].lower()
|
||||
format_new_ext = u'.' + self.queue[self.current]['settings']['new_book_format'].lower()
|
||||
|
||||
# check to see if destination format already exists -
|
||||
# if it does - mark the conversion task as complete and return a success
|
||||
# this will allow send to kindle workflow to continue to work
|
||||
if os.path.isfile(file_path + format_new_ext):
|
||||
web.app.logger.info("Book id %d already converted to %s", bookid, format_new_ext)
|
||||
cur_book = web.db.session.query(web.db.Books).filter(web.db.Books.id == bookid).first()
|
||||
self.queue[self.current]['path'] = file_path
|
||||
self.queue[self.current]['title'] = cur_book.title
|
||||
self._handleSuccess()
|
||||
return file_path + format_new_ext
|
||||
else:
|
||||
web.app.logger.info("Book id %d - target format of %s does not exist. Moving forward with convert.", bookid, format_new_ext)
|
||||
|
||||
# check if converter-executable is existing
|
||||
if not os.path.exists(web.ub.config.config_converterpath):
|
||||
self._handleError(_(u"Convertertool %(converter)s not found", converter=web.ub.config.config_converterpath))
|
||||
# ToDo Text is not translated
|
||||
self._handleError(u"Convertertool %s not found" % web.ub.config.config_converterpath)
|
||||
return
|
||||
|
||||
try:
|
||||
# check which converter to use kindlegen is "1"
|
||||
if format_old_ext == '.epub' and format_new_ext == '.mobi':
|
||||
@ -269,7 +284,7 @@ class WorkerThread(threading.Thread):
|
||||
|
||||
p = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
|
||||
except OSError as e:
|
||||
self._handleError(_(u"Ebook-converter failed: %s" % e))
|
||||
self._handleError(_(u"Ebook-converter failed: %(error)s", error=e))
|
||||
return
|
||||
|
||||
if web.ub.config.config_ebookconverter == 1:
|
||||
@ -313,11 +328,7 @@ class WorkerThread(threading.Thread):
|
||||
self.queue[self.current]['title'] = cur_book.title
|
||||
if web.ub.config.config_use_google_drive:
|
||||
os.remove(file_path + format_old_ext)
|
||||
self.queue[self.current]['status'] = STAT_FINISH_SUCCESS
|
||||
self.UIqueue[self.current]['status'] = _('Finished')
|
||||
self.UIqueue[self.current]['progress'] = "100 %"
|
||||
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
|
||||
datetime.now() - self.queue[self.current]['starttime'])
|
||||
self._handleSuccess()
|
||||
return file_path + format_new_ext
|
||||
else:
|
||||
error_message = format_new_ext.upper() + ' format not found on disk'
|
||||
@ -328,63 +339,62 @@ class WorkerThread(threading.Thread):
|
||||
return
|
||||
|
||||
|
||||
def add_convert(self, file_path, bookid, user_name, typ, settings, kindle_mail=None):
|
||||
def add_convert(self, file_path, bookid, user_name, taskMessage, settings, kindle_mail=None):
|
||||
addLock = threading.Lock()
|
||||
addLock.acquire()
|
||||
if self.last >= 20:
|
||||
self.delete_completed_tasks()
|
||||
self._delete_completed_tasks()
|
||||
# progress, runtime, and status = 0
|
||||
self.id += 1
|
||||
task = TASK_CONVERT_ANY
|
||||
if kindle_mail:
|
||||
task = TASK_CONVERT
|
||||
self.queue.append({'file_path':file_path, 'bookid':bookid, 'starttime': 0, 'kindle': kindle_mail,
|
||||
'status': STAT_WAITING, 'typ': task, 'settings':settings})
|
||||
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'type': typ,
|
||||
'runtime': '0 s', 'status': _('Waiting'),'id': self.id } )
|
||||
'taskType': task, 'settings':settings})
|
||||
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
|
||||
'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': task } )
|
||||
|
||||
self.last=len(self.queue)
|
||||
addLock.release()
|
||||
|
||||
|
||||
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, typ,
|
||||
text=_(u'This e-mail has been sent via Calibre-Web.')):
|
||||
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, taskMessage,
|
||||
text):
|
||||
# if more than 20 entries in the list, clean the list
|
||||
addLock = threading.Lock()
|
||||
addLock.acquire()
|
||||
if self.last >= 20:
|
||||
self.delete_completed_tasks()
|
||||
self._delete_completed_tasks()
|
||||
# progress, runtime, and status = 0
|
||||
self.id += 1
|
||||
self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath,
|
||||
'settings':settings, 'recipent':recipient, 'starttime': 0,
|
||||
'status': STAT_WAITING, 'typ': TASK_EMAIL, 'text':text})
|
||||
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'type': typ,
|
||||
'runtime': '0 s', 'status': _('Waiting'),'id': self.id })
|
||||
'taskType': TASK_EMAIL, 'text':text})
|
||||
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
|
||||
'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': TASK_EMAIL })
|
||||
self.last=len(self.queue)
|
||||
addLock.release()
|
||||
|
||||
def add_upload(self, user_name, typ):
|
||||
def add_upload(self, user_name, taskMessage):
|
||||
# if more than 20 entries in the list, clean the list
|
||||
addLock = threading.Lock()
|
||||
addLock.acquire()
|
||||
if self.last >= 20:
|
||||
self.delete_completed_tasks()
|
||||
self._delete_completed_tasks()
|
||||
# progress=100%, runtime=0, and status finished
|
||||
self.id += 1
|
||||
self.queue.append({'starttime': datetime.now(), 'status': STAT_FINISH_SUCCESS, 'typ': TASK_UPLOAD})
|
||||
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': "100 %", 'type': typ,
|
||||
'runtime': '0 s', 'status': _('Finished'),'id': self.id })
|
||||
self.queue.append({'starttime': datetime.now(), 'taskType': TASK_UPLOAD})
|
||||
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': "100 %", 'taskMess': taskMessage,
|
||||
'runtime': '0 s', 'stat': STAT_FINISH_SUCCESS,'id': self.id, 'taskType': TASK_UPLOAD})
|
||||
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
|
||||
self.last=len(self.queue)
|
||||
addLock.release()
|
||||
|
||||
|
||||
def send_raw_email(self):
|
||||
def _send_raw_email(self):
|
||||
self.queue[self.current]['starttime'] = datetime.now()
|
||||
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
|
||||
self.queue[self.current]['status'] = STAT_STARTED
|
||||
self.UIqueue[self.current]['status'] = _('Started')
|
||||
# self.queue[self.current]['status'] = STAT_STARTED
|
||||
self.UIqueue[self.current]['stat'] = STAT_STARTED
|
||||
obj=self.queue[self.current]
|
||||
# create MIME message
|
||||
msg = MIMEMultipart()
|
||||
@ -434,26 +444,23 @@ class WorkerThread(threading.Thread):
|
||||
self.asyncSMTP.login(str(obj['settings']["mail_login"]), str(obj['settings']["mail_password"]))
|
||||
self.asyncSMTP.sendmail(obj['settings']["mail_from"], obj['recipent'], msg)
|
||||
self.asyncSMTP.quit()
|
||||
self.queue[self.current]['status'] = STAT_FINISH_SUCCESS
|
||||
self.UIqueue[self.current]['status'] = _('Finished')
|
||||
self.UIqueue[self.current]['progress'] = "100 %"
|
||||
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
|
||||
datetime.now() - self.queue[self.current]['starttime'])
|
||||
|
||||
self._handleSuccess()
|
||||
sys.stderr = org_stderr
|
||||
|
||||
except (MemoryError) as e:
|
||||
self._handleError(u'Error sending email: ' + e.message)
|
||||
return None
|
||||
except (smtplib.SMTPException) as e:
|
||||
self._handleError(u'Error sending email: ' + e.smtp_error.replace("\n",'. '))
|
||||
if hasattr(e, "smtp_error"):
|
||||
text = e.smtp_error.replace("\n",'. ')
|
||||
else:
|
||||
text = ''
|
||||
self._handleError(u'Error sending email: ' + text)
|
||||
return None
|
||||
except (socket.error) as e:
|
||||
self._handleError(u'Error sending email: ' + e.strerror)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def _formatRuntime(self, runtime):
|
||||
self.UIqueue[self.current]['rt'] = runtime.total_seconds()
|
||||
val = re.split('\:|\.', str(runtime))[0:3]
|
||||
@ -465,18 +472,25 @@ class WorkerThread(threading.Thread):
|
||||
if retVal == ' s':
|
||||
retVal = '0 s'
|
||||
return retVal
|
||||
|
||||
|
||||
def _handleError(self, error_message):
|
||||
web.app.logger.error(error_message)
|
||||
self.queue[self.current]['status'] = STAT_FAIL
|
||||
self.UIqueue[self.current]['status'] = _('Failed')
|
||||
# self.queue[self.current]['status'] = STAT_FAIL
|
||||
self.UIqueue[self.current]['stat'] = STAT_FAIL
|
||||
self.UIqueue[self.current]['progress'] = "100 %"
|
||||
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
|
||||
datetime.now() - self.queue[self.current]['starttime'])
|
||||
self.UIqueue[self.current]['message'] = error_message
|
||||
|
||||
def _handleSuccess(self):
|
||||
# self.queue[self.current]['status'] = STAT_FINISH_SUCCESS
|
||||
self.UIqueue[self.current]['stat'] = STAT_FINISH_SUCCESS
|
||||
self.UIqueue[self.current]['progress'] = "100 %"
|
||||
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
|
||||
datetime.now() - self.queue[self.current]['starttime'])
|
||||
|
||||
|
||||
# Enable logging of smtp lib debug output
|
||||
class StderrLogger(object):
|
||||
|
||||
buffer = ''
|
||||
@ -491,3 +505,4 @@ class StderrLogger(object):
|
||||
self.buffer = ''
|
||||
else:
|
||||
self.buffer += message
|
||||
|
||||
|
760
messages.pot
760
messages.pot
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,6 @@ six==1.10.0
|
||||
goodreads>=0.3.2
|
||||
python-Levenshtein>=0.12.0
|
||||
# other
|
||||
lxml==3.7.2
|
||||
lxml>=3.8.0
|
||||
rarfile>=2.7
|
||||
natsort>=2.2.0
|
||||
|
31
readme.md
31
readme.md
@ -12,7 +12,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
||||
- full graphical setup
|
||||
- User management with fine grained per-user permissions
|
||||
- Admin interface
|
||||
- User Interface in dutch, english, french, german, italian, japanese, khmer, polish, russian, simplified chinese, spanish
|
||||
- User Interface in dutch, english, french, german, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish
|
||||
- OPDS feed for eBook reader apps
|
||||
- Filter and search by titles, authors, tags, series and language
|
||||
- Create custom book collection (shelves)
|
||||
@ -30,7 +30,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
||||
|
||||
## Quick start
|
||||
|
||||
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)
|
||||
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
|
||||
@ -41,6 +41,9 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
||||
*Username:* admin
|
||||
*Password:* admin123
|
||||
|
||||
**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.
|
||||
|
||||
## Runtime Configuration Options
|
||||
|
||||
The configuration can be changed as admin in the admin panel under "Configuration"
|
||||
@ -48,14 +51,14 @@ The configuration can be changed as admin in the admin panel under "Configuratio
|
||||
Server Port:
|
||||
Changes the port Calibre-Web is listening, changes take effect after pressing submit button
|
||||
|
||||
Enable public registration:
|
||||
Enable public registration:
|
||||
Tick to enable public user registration.
|
||||
|
||||
Enable anonymous browsing:
|
||||
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.
|
||||
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.
|
||||
@ -83,7 +86,7 @@ Once a project has been created, we need to create a client ID and a client secr
|
||||
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`
|
||||
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: -
|
||||
|
||||
@ -103,7 +106,7 @@ Additionally the public adress your server uses (e.g.https://example.com) has to
|
||||
|
||||
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
|
||||
11. Note that this expires after a week, so will need to be manually refresh
|
||||
|
||||
## Docker images
|
||||
|
||||
@ -160,7 +163,7 @@ Listen 443
|
||||
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
|
||||
@ -172,8 +175,8 @@ Listen 443
|
||||
|
||||
## (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
|
||||
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.
|
||||
|
||||
@ -206,7 +209,7 @@ enables the service.
|
||||
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
|
||||
`"-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
|
||||
|
2198
test/Calibre-Web TestSummary.html
Normal file
2198
test/Calibre-Web TestSummary.html
Normal file
File diff suppressed because it is too large
Load Diff
21
test/css/runner.css
Normal file
21
test/css/runner.css
Normal file
@ -0,0 +1,21 @@
|
||||
.hiddenRow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg-grey {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.table-curved {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.buttons, .report-description {
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.piechart{
|
||||
text-align: center;
|
||||
}
|
||||
|
189
test/js/runner.js
Normal file
189
test/js/runner.js
Normal file
@ -0,0 +1,189 @@
|
||||
output_list = Array();
|
||||
|
||||
/* Level - 0: Summary; 1: Failed; 2: All; 3: Skipped */
|
||||
function showCase(level) {
|
||||
table_rows = document.getElementsByTagName("tr");
|
||||
for (var i = 0; i < table_rows.length; i++) {
|
||||
row = table_rows[i];
|
||||
id = row.id;
|
||||
if (id.substr(0,2) == 'ft') {
|
||||
if (level < 1 || level == 3) {
|
||||
row.classList.add('hiddenRow');
|
||||
}
|
||||
else {
|
||||
row.classList.remove('hiddenRow');
|
||||
}
|
||||
}
|
||||
if (id.substr(0,2) == 'pt') {
|
||||
if (level > 1 && level != 3) {
|
||||
row.classList.remove('hiddenRow');
|
||||
}
|
||||
else {
|
||||
row.classList.add('hiddenRow');
|
||||
}
|
||||
}
|
||||
if (id.substr(0,2) == 'st') {
|
||||
if (level >=2) {
|
||||
row.classList.remove('hiddenRow');
|
||||
}
|
||||
else {
|
||||
row.classList.add('hiddenRow');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showClassDetail(class_id, count) {
|
||||
var testcases_list = Array(count);
|
||||
var all_hidden = true;
|
||||
for (var i = 0; i < count; i++) {
|
||||
testcase_postfix_id = 't' + class_id.substr(1) + '.' + (i+1);
|
||||
testcase_id = 'f' + testcase_postfix_id;
|
||||
testcase = document.getElementById(testcase_id);
|
||||
if (!testcase) {
|
||||
testcase_id = 'p' + testcase_postfix_id;
|
||||
testcase = document.getElementById(testcase_id);
|
||||
}
|
||||
if (!testcase) {
|
||||
testcase_id = 's' + testcase_postfix_id;
|
||||
testcase = document.getElementById(testcase_id);
|
||||
}
|
||||
testcases_list[i] = testcase;
|
||||
if (testcase.classList.contains('hiddenRow')) {
|
||||
all_hidden = false;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < count; i++) {
|
||||
testcase = testcases_list[i];
|
||||
if (!all_hidden) {
|
||||
testcase.classList.remove('hiddenRow');
|
||||
}
|
||||
else {
|
||||
testcase.classList.add('hiddenRow');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showTestDetail(div_id){
|
||||
var details_div = document.getElementById(div_id)
|
||||
var displayState = details_div.style.display
|
||||
// alert(displayState)
|
||||
if (displayState != 'block' ) {
|
||||
displayState = 'block'
|
||||
details_div.style.display = 'block'
|
||||
}
|
||||
else {
|
||||
details_div.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function html_escape(s) {
|
||||
s = s.replace(/&/g,'&');
|
||||
s = s.replace(/</g,'<');
|
||||
s = s.replace(/>/g,'>');
|
||||
return s;
|
||||
}
|
||||
|
||||
/* obsoleted by detail in <div>
|
||||
function showOutput(id, name) {
|
||||
var w = window.open("", //url
|
||||
name,
|
||||
"resizable,scrollbars,status,width=800,height=450");
|
||||
d = w.document;
|
||||
d.write("<pre>");
|
||||
d.write(html_escape(output_list[id]));
|
||||
d.write("\n");
|
||||
d.write("<a href='javascript:window.close()'>close</a>\n");
|
||||
d.write("</pre>\n");
|
||||
d.close();
|
||||
}
|
||||
*/
|
||||
function drawCircle(pass, fail, error, skip){
|
||||
var color = ["#5cb85c","#d9534f","#c00","#f0ad4e"];
|
||||
var data = [pass,fail,error,skip];
|
||||
var text_arr = ["pass", "fail", "error","skip"];
|
||||
|
||||
var canvas = document.getElementById("circle");
|
||||
var ctx = canvas.getContext("2d");
|
||||
var startPoint=0;
|
||||
var width = 20, height = 10;
|
||||
var posX = 112 * 2 + 20, posY = 30;
|
||||
var textX = posX + width + 5, textY = posY + 10;
|
||||
for(var i=0;i<data.length;i++){
|
||||
ctx.fillStyle = color[i];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(112,84);
|
||||
ctx.arc(112,84,84,startPoint,startPoint+Math.PI*2*(data[i]/(data[0]+data[1]+data[2]+data[3])),false);
|
||||
ctx.fill();
|
||||
startPoint += Math.PI*2*(data[i]/(data[0]+data[1]+data[2]+data[3]));
|
||||
ctx.fillStyle = color[i];
|
||||
ctx.fillRect(posX, posY + 20 * i, width, height);
|
||||
ctx.moveTo(posX, posY + 20 * i);
|
||||
ctx.font = 'bold 14px';
|
||||
ctx.fillStyle = color[i];
|
||||
var percent = text_arr[i] + ":"+data[i];
|
||||
ctx.fillText(percent, textX, textY + 20 * i);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function show_img(obj) {
|
||||
var obj1 = obj.nextElementSibling
|
||||
obj1.style.display='block'
|
||||
var index = 0;//每张图片的下标,
|
||||
var len = obj1.getElementsByTagName('img').length;
|
||||
var imgyuan = obj1.getElementsByClassName('imgyuan')[0]
|
||||
//var start=setInterval(autoPlay,500);
|
||||
obj1.onmouseover=function(){//当鼠标光标停在图片上,则停止轮播
|
||||
clearInterval(start);
|
||||
}
|
||||
obj1.onmouseout=function(){//当鼠标光标停在图片上,则开始轮播
|
||||
start=setInterval(autoPlay,1000);
|
||||
}
|
||||
for (var i = 0; i < len; i++) {
|
||||
var font = document.createElement('font')
|
||||
imgyuan.appendChild(font)
|
||||
}
|
||||
var lis = obj1.getElementsByTagName('font');//得到所有圆圈
|
||||
changeImg(0)
|
||||
var funny = function (i) {
|
||||
lis[i].onmouseover = function () {
|
||||
index=i
|
||||
changeImg(i)
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
funny(i);
|
||||
}
|
||||
|
||||
function autoPlay(){
|
||||
if(index>len-1){
|
||||
index=0;
|
||||
clearInterval(start); //运行一轮后停止
|
||||
}
|
||||
changeImg(index++);
|
||||
}
|
||||
imgyuan.style.width= 25*len +"px";
|
||||
//对应圆圈和图片同步
|
||||
function changeImg(index) {
|
||||
var list = obj1.getElementsByTagName('img');
|
||||
var list1 = obj1.getElementsByTagName('font');
|
||||
for (i = 0; i < list.length; i++) {
|
||||
list[i].style.display = 'none';
|
||||
list1[i].style.backgroundColor = 'white';
|
||||
}
|
||||
list[index].style.display = 'block';
|
||||
list1[index].style.backgroundColor = 'blue';
|
||||
}
|
||||
|
||||
}
|
||||
function hide_img(obj){
|
||||
obj.parentElement.style.display = "none";
|
||||
obj.parentElement.getElementsByClassName('imgyuan')[0].innerHTML = "";
|
||||
}
|
Loading…
Reference in New Issue
Block a user