Merge pull request #19 from janeczku/master

merge from janeczku/master
pull/700/head
Ethan Lin 6 years ago committed by GitHub
commit dec50b5682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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/css/libs/* linguist-vendored
cps/static/js/libs/* linguist-vendored cps/static/js/libs/* linguist-vendored

@ -123,9 +123,11 @@ def pdf_preview(tmp_file_path, tmp_dir):
def get_versions(): def get_versions():
if not use_generic_pdf_cover: if not use_generic_pdf_cover:
IVersion=ImageVersion.MAGICK_VERSION IVersion = ImageVersion.MAGICK_VERSION
WVersion = ImageVersion.VERSION
else: else:
IVersion = _(u'not installed') IVersion = _(u'not installed')
WVersion = _(u'not installed')
if use_pdf_meta: if use_pdf_meta:
PVersion='v'+PyPdfVersion PVersion='v'+PyPdfVersion
else: else:
@ -134,4 +136,4 @@ def get_versions():
XVersion = 'v'+'.'.join(map(str, lxmlversion)) XVersion = 'v'+'.'.join(map(str, lxmlversion))
else: else:
XVersion = _(u'not installed') 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...") print("Keyfilepath is invalid. Exiting...")
sys.exit(1) 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 "": if args.k is "":
keyfilepath = "" keyfilepath = ""

@ -45,5 +45,5 @@ def versioncheck():
elif ub.config.config_ebookconverter == 2: elif ub.config.config_ebookconverter == 2:
return versionCalibre() return versionCalibre()
else: else:
return {'ebook_converter':''} return {'ebook_converter':_(u'not configured')}

@ -149,19 +149,19 @@ def getDrive(drive=None, gauth=None):
drive.auth.Refresh() drive.auth.Refresh()
return drive return drive
def listRootFolders(drive=None): def listRootFolders():
drive = getDrive(drive) drive = getDrive(Gdrive.Instance().drive)
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
fileList = drive.ListFile({'q': folder}).GetList() fileList = drive.ListFile({'q': folder}).GetList()
return fileList return fileList
def getEbooksFolder(drive=None): def getEbooksFolder(drive):
return getFolderInFolder('root',config.config_google_drive_folder,drive) return getFolderInFolder('root',config.config_google_drive_folder,drive)
def getFolderInFolder(parentId, folderName,drive=None): def getFolderInFolder(parentId, folderName, drive):
drive = getDrive(drive) # drive = getDrive(drive)
query="" query=""
if folderName: if folderName:
query = "title = '%s' and " % folderName.replace("'", "\\'") query = "title = '%s' and " % folderName.replace("'", "\\'")
@ -190,7 +190,6 @@ def getEbooksFolderId(drive=None):
def getFile(pathId, fileName, drive): def getFile(pathId, fileName, drive):
# drive = getDrive(Gdrive.Instance().drive)
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'")) metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'"))
fileList = drive.ListFile({'q': metaDataFile}).GetList() fileList = drive.ListFile({'q': metaDataFile}).GetList()
@ -200,8 +199,8 @@ def getFile(pathId, fileName, drive):
return fileList[0] return fileList[0]
def getFolderId(path, drive=None): def getFolderId(path, drive):
drive = getDrive(drive) # drive = getDrive(drive)
currentFolderId = getEbooksFolderId(drive) currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/' sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first() storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
@ -249,7 +248,7 @@ def getFileFromEbooksFolder(path, fileName):
return None return None
def copyDriveFileRemote(drive, origin_file_id, copy_title): '''def copyDriveFileRemote(drive, origin_file_id, copy_title):
drive = getDrive(drive) drive = getDrive(drive)
copied_file = {'title': copy_title} copied_file = {'title': copy_title}
try: try:
@ -258,7 +257,7 @@ def copyDriveFileRemote(drive, origin_file_id, copy_title):
return drive.CreateFile({'id': file_data['id']}) return drive.CreateFile({'id': file_data['id']})
except errors.HttpError as error: except errors.HttpError as error:
print ('An error occurred: %s' % error) print ('An error occurred: %s' % error)
return None return None'''
# Download metadata.db from gdrive # Download metadata.db from gdrive
@ -347,7 +346,6 @@ def uploadFileToEbooksFolder(destFile, f):
def watchChange(drive, channel_id, channel_type, channel_address, def watchChange(drive, channel_id, channel_type, channel_address,
channel_token=None, expiration=None): channel_token=None, expiration=None):
# drive = getDrive(drive)
# Watch for all changes to a user's Drive. # Watch for all changes to a user's Drive.
# Args: # Args:
# service: Drive API service instance. # service: Drive API service instance.
@ -390,8 +388,6 @@ def watchFile(drive, file_id, channel_id, channel_type, channel_address,
Raises: Raises:
apiclient.errors.HttpError: if http request to create channel fails. apiclient.errors.HttpError: if http request to create channel fails.
""" """
# drive = getDrive(drive)
body = { body = {
'id': channel_id, 'id': channel_id,
'type': channel_type, 'type': channel_type,
@ -413,8 +409,6 @@ def stopChannel(drive, channel_id, resource_id):
Raises: Raises:
apiclient.errors.HttpError: if http request to create channel fails. apiclient.errors.HttpError: if http request to create channel fails.
""" """
# drive = getDrive(drive)
# service=drive.auth.service
body = { body = {
'id': channel_id, 'id': channel_id,
'resourceId': resource_id 'resourceId': resource_id
@ -423,7 +417,6 @@ def stopChannel(drive, channel_id, resource_id):
def getChangeById (drive, change_id): def getChangeById (drive, change_id):
# drive = getDrive(drive)
# Print a single Change resource information. # Print a single Change resource information.
# #
# Args: # Args:
@ -454,11 +447,13 @@ def updateDatabaseOnEdit(ID,newPath):
storedPathName.path = newPath storedPathName.path = newPath
session.commit() session.commit()
# Deletes the hashes in database of deleted book # Deletes the hashes in database of deleted book
def deleteDatabaseEntry(ID): def deleteDatabaseEntry(ID):
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete() session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
session.commit() session.commit()
# Gets cover file from gdrive # Gets cover file from gdrive
def get_cover_via_gdrive(cover_path): def get_cover_via_gdrive(cover_path):
df = getFileFromEbooksFolder(cover_path, 'cover.jpg') df = getFileFromEbooksFolder(cover_path, 'cover.jpg')

@ -13,9 +13,10 @@ import unicodedata
from io import BytesIO from io import BytesIO
import worker import worker
import time import time
from flask import send_from_directory, make_response, redirect, abort from flask import send_from_directory, make_response, redirect, abort
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user
from babel.dates import format_datetime
import threading import threading
import shutil import shutil
import requests 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 # read settings and append converter task to queue
if kindle_mail: if kindle_mail:
settings = ub.get_mail_settings() 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: else:
text = _(u"Convert to %(format)s: %(book)s", format=new_book_format, book=book.title) settings = dict()
settings['old_book_format'] = u'EPUB' text = (u"%s -> %s: %s" % (old_book_format, new_book_format, book.title))
settings['new_book_format'] = u'MOBI' 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) global_WorkerThread.add_convert(file_path, book.id, user_id, text, settings, kindle_mail)
return None return None
else: 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): def send_test_mail(kindle_mail, user_name):
global_WorkerThread.add_email(_(u'Calibre-Web test e-mail'),None, None, ub.get_mail_settings(), 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 return
@ -104,7 +109,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
text += "Sincerely\r\n\r\n" text += "Sincerely\r\n\r\n"
text += "Your Calibre-Web team" text += "Your Calibre-Web team"
global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, ub.get_mail_settings(), 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 return
@ -132,7 +137,7 @@ def send_mail(book_id, kindle_mail, calibrepath, user_id):
if 'mobi' in formats: if 'mobi' in formats:
result = formats['mobi'] result = formats['mobi']
elif 'epub' in formats: 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) return convert_book_format(book_id, calibrepath, u'epub', u'mobi', user_id, kindle_mail)
elif 'pdf' in formats: elif 'pdf' in formats:
result = formats['pdf'] # worker.get_attachment() 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") return _(u"Could not find any formats suitable for sending by e-mail")
if result: if result:
global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(), 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: else:
return _(u"The requested file could not be read. Maybe wrong permissions?") 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): def get_sorted_author(value):
try: try:
if ',' not in value:
regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"] regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"]
combined = "(" + ")|(".join(regexes) + ")" combined = "(" + ")|(".join(regexes) + ")"
value = value.split(" ") value = value.split(" ")
if re.match(combined, value[-1].upper()): if re.match(combined, value[-1].upper()):
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1] value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
elif len(value) == 1:
value2 = value[0]
else: else:
value2 = value[-1] + ", " + " ".join(value[:-1]) value2 = value[-1] + ", " + " ".join(value[:-1])
else:
value2 = value
except Exception: except Exception:
web.app.logger.error("Sorting author " + str(value) + "failed") web.app.logger.error("Sorting author " + str(value) + "failed")
value2 = value value2 = value
@ -235,18 +246,18 @@ def update_dir_structure_file(book_id, calibrepath):
path = new_title_path path = new_title_path
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
except OSError as ex: except OSError as ex:
web.app.logger.error("Rename title from: " + path + " to " + new_title_path) web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex))
web.app.logger.error(ex, exc_info=True) web.app.logger.debug(ex, exc_info=True)
return _('Rename title from: "%s" to "%s" failed with error: %s' % (path, new_title_path, str(ex))) 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: if authordir != new_authordir:
try: try:
new_author_path = os.path.join(os.path.join(calibrepath, new_authordir), os.path.basename(path)) new_author_path = os.path.join(os.path.join(calibrepath, new_authordir), os.path.basename(path))
os.renames(path, new_author_path) os.renames(path, new_author_path)
localbook.path = new_authordir + '/' + localbook.path.split('/')[1] localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
except OSError as ex: except OSError as ex:
web.app.logger.error("Rename author from: " + path + " to " + new_author_path) web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex))
web.app.logger.error(ex, exc_info=True) web.app.logger.debug(ex, exc_info=True)
return _('Rename author from: "%s" to "%s" failed with error: %s' % (path, new_title_path, str(ex))) 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 return False
@ -260,7 +271,6 @@ def update_dir_structure_gdrive(book_id):
new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")" new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")"
if titledir != new_titledir: if titledir != new_titledir:
# print (titledir)
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir) gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if gFile: if gFile:
gFile['title'] = new_titledir gFile['title'] = new_titledir
@ -269,7 +279,7 @@ def update_dir_structure_gdrive(book_id):
book.path = book.path.split('/')[0] + '/' + new_titledir book.path = book.path.split('/')[0] + '/' + new_titledir
gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected
else: 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: if authordir != new_authordir:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir) 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] book.path = new_authordir + '/' + book.path.split('/')[1]
gd.updateDatabaseOnEdit(gFile['id'], book.path) gd.updateDatabaseOnEdit(gFile['id'], book.path)
else: 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 return error
@ -296,7 +306,7 @@ def delete_book_gdrive(book, book_format):
gd.deleteDatabaseEntry(gFile['id']) gd.deleteDatabaseEntry(gFile['id'])
gFile.Trash() gFile.Trash()
else: 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 return error
def generate_random_password(): def generate_random_password():
@ -367,7 +377,11 @@ def do_download_file(book, book_format, data, headers):
else: else:
abort(404) abort(404)
else: 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 response.headers = headers
return response return response
@ -381,8 +395,11 @@ class Updater(threading.Thread):
self.status = 0 self.status = 0
def run(self): def run(self):
try:
self.status = 1 self.status = 1
r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True) 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] fname = re.findall("filename=(.+)", r.headers['content-disposition'])[0]
self.status = 2 self.status = 2
z = zipfile.ZipFile(BytesIO(r.content)) z = zipfile.ZipFile(BytesIO(r.content))
@ -391,15 +408,24 @@ class Updater(threading.Thread):
z.extractall(tmp_dir) z.extractall(tmp_dir)
self.status = 4 self.status = 4
self.update_source(os.path.join(tmp_dir, os.path.splitext(fname)[0]), ub.config.get_main_dir) 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 self.status = 6
time.sleep(2)
server.Server.setRestartTyp(True) server.Server.setRestartTyp(True)
server.Server.stopServer() server.Server.stopServer()
self.status = 7 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): def get_update_status(self):
return self.status return self.status
@ -547,3 +573,67 @@ def check_unrar(unrarLocation):
error=True error=True
return (error, version) return (error, version)
def is_sha1(sha1):
if len(sha1) != 40:
return False
try:
int(sha1, 16)
except ValueError:
return False
return True
def get_current_version_info():
content = {}
content[0] = '$Format:%H$'
content[1] = '$Format:%cI$'
# content[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
# content[1] = '2018-09-09T10:13:08+02:00'
if is_sha1(content[0]) and len(content[1]) > 0:
return {'hash': content[0], 'datetime': content[1]}
return False
def 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

@ -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 #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from socket import error as SocketError from socket import error as SocketError
import sys import sys
import os import os
import signal
import web
try: try:
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
from gevent.pool import Pool from gevent.pool import Pool
@ -17,8 +19,6 @@ except ImportError:
from tornado import version as tornadoVersion from tornado import version as tornadoVersion
gevent_present = False gevent_present = False
import web
class server: class server:
@ -26,7 +26,8 @@ class server:
restart= False restart= False
def __init__(self): def __init__(self):
pass signal.signal(signal.SIGINT, self.killServer)
signal.signal(signal.SIGTERM, self.killServer)
def start_gevent(self): def start_gevent(self):
try: try:
@ -39,11 +40,16 @@ class server:
else: else:
self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
self.wsgiserver.serve_forever() self.wsgiserver.serve_forever()
except SocketError: except SocketError:
try:
web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...') web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...')
self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
self.wsgiserver.serve_forever() 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: except Exception:
web.app.logger.info("Unknown error while starting gevent") web.app.logger.info("Unknown error while starting gevent")
@ -53,6 +59,7 @@ class server:
# leave subprocess out to allow forking for fetchers and processors # leave subprocess out to allow forking for fetchers and processors
self.start_gevent() self.start_gevent()
else: else:
try:
web.app.logger.info('Starting Tornado server') web.app.logger.info('Starting Tornado server')
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile(): if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
ssl={"certfile": web.ub.config.get_config_certfile(), ssl={"certfile": web.ub.config.get_config_certfile(),
@ -65,8 +72,14 @@ class server:
ssl_options=ssl) ssl_options=ssl)
http_server.listen(web.ub.config.config_port) http_server.listen(web.ub.config.config_port)
self.wsgiserver=IOLoop.instance() self.wsgiserver=IOLoop.instance()
self.wsgiserver.start() # wait for stop signal self.wsgiserver.start()
# wait for stop signal
self.wsgiserver.close(True) 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: if self.restart == True:
web.app.logger.info("Performing restart of Calibre-Web") web.app.logger.info("Performing restart of Calibre-Web")
@ -86,6 +99,9 @@ class server:
def setRestartTyp(self,starttyp): def setRestartTyp(self,starttyp):
self.restart=starttyp self.restart=starttyp
def killServer(self, signum, frame):
self.stopServer()
def stopServer(self): def stopServer(self):
if gevent_present: if gevent_present:
self.wsgiserver.close() self.wsgiserver.close()

@ -5,6 +5,22 @@
src: local('Grand Hotel'), local('GrandHotel-Regular'), url("fonts/GrandHotel-Regular.ttf") format('truetype'); 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{background:#f2f2f2}body h2{font-weight:normal;color:#444}
body { margin-bottom: 40px;} body { margin-bottom: 40px;}
a{color: #45b29d}a:hover{color: #444;} 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) { function sourceSplit(query, cb, split, source) {
var bhAdapter = source.ttAdapter(); 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() { $("#search").on("change input.typeahead:selected", function() {
var form = $("form").serialize(); var form = $("form").serialize();
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) { $.getJSON( getPath() + "/get_matching_tags", form, function( data ) {

@ -104,18 +104,39 @@ $(function() {
var $this = $(this); var $this = $(this);
var buttonText = $this.html(); var buttonText = $this.html();
$this.html("..."); $this.html("...");
$("#update_error").addClass("hidden")
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../get_update_status", url: window.location.pathname + "/../../get_update_status",
success: function success(data) { success: function success(data) {
$this.html(buttonText); $this.html(buttonText);
if (data.status === true) {
var cssClass = '';
var message = ''
if (data.success === true) {
if (data.update === true) {
$("#check_for_update").addClass("hidden"); $("#check_for_update").addClass("hidden");
$("#perform_update").removeClass("hidden"); $("#perform_update").removeClass("hidden");
$("#update_info") $("#update_info")
.removeClass("hidden") .removeClass("hidden")
.find("span").html(data.commit); .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">&times;</a>' + data.message + '</div>';
$(message).insertAfter($("#update_table"));
} }
}); });
}); });

@ -1,6 +1,8 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<div class="discover"> <div class="container-fluid">
<div class="row">
<div class="col">
<h2>{{_('User list')}}</h2> <h2>{{_('User list')}}</h2>
<table class="table table-striped" id="table_user"> <table class="table table-striped" id="table_user">
<tr> <tr>
@ -29,6 +31,11 @@
{% endfor %} {% endfor %}
</table> </table>
<div class="btn btn-default" id="admin_new_user"><a href="{{url_for('new_user')}}">{{_('Add new user')}}</a></div> <div class="btn btn-default" id="admin_new_user"><a href="{{url_for('new_user')}}">{{_('Add new user')}}</a></div>
</div>
</div>
<div class="row">
<div class="col">
<h2>{{_('SMTP e-mail server settings')}}</h2> <h2>{{_('SMTP e-mail server settings')}}</h2>
<table class="table table-striped" id="table_email"> <table class="table table-striped" id="table_email">
<tr> <tr>
@ -44,62 +51,90 @@
<td>{% if email.mail_use_ssl %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</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>{{email.mail_login}}</td>
<td class="hidden-xs">{{email.mail_from}}</td> <td class="hidden-xs">{{email.mail_from}}</td>
</tr>
</table> </table>
<div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div> <div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div>
<div id="container"> </div>
</div>
<div class="row">
<div class="col">
<h2>{{_('Configuration')}}</h2> <h2>{{_('Configuration')}}</h2>
<div class="col-xs-12 col-sm-6"> <div class="col-xs-12 col-sm-6">
<div class="Row"> <div class="row">
<div class="col-xs-6 col-sm-6">{{_('Calibre DB dir')}}</div> <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 class="col-xs-6 col-sm-6">{{config.config_calibre_dir}}</div>
</div> </div>
<div class="Row"> <div class="row">
<div class="col-xs-6 col-sm-6">{{_('Log level')}}</div> <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 class="col-xs-6 col-sm-6">{{config.get_Log_Level()}}</div>
</div> </div>
<div class="Row"> <div class="row">
<div class="col-xs-6 col-sm-6">{{_('Port')}}</div> <div class="col-xs-6 col-sm-6">{{_('Port')}}</div>
<div class="col-xs-6 col-sm-6">{{config.config_port}}</div> <div class="col-xs-6 col-sm-6">{{config.config_port}}</div>
</div> </div>
</div> </div>
<div class="col-xs-12 col-sm-6"> <div class="col-xs-12 col-sm-6">
<div class="Row"> <div class="row">
<div class="col-xs-6 col-sm-7">{{_('Books per page')}}</div> <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="col-xs-6 col-sm-5">{{config.config_books_per_page}}</div>
</div> </div>
<div class="Row"> <div class="row">
<div class="col-xs-6 col-sm-7">{{_('Uploading')}}</div> <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 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>
<div class="Row"> <div class="row">
<div class="col-xs-6 col-sm-7">{{_('Anonymous browsing')}}</div> <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 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>
<div class="Row"> <div class="row">
<div class="col-xs-6 col-sm-7">{{_('Public registration')}}</div> <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 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>
<div class="Row"> <div class="row">
<div class="col-xs-6 col-sm-7">{{_('Remote login')}}</div> <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 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> </div>
</div> <div class="btn btn-default"><a id="basic_config" href="{{url_for('configuration')}}">{{_('Basic Configuration')}}</a></div>
<div class="col-xs-12 col-sm-12"> <div class="btn btn-default"><a id="view_config" href="{{url_for('view_configuration')}}">{{_('UI Configuration')}}</a></div>
<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> </div>
</div>
<div class="row">
<div class="col">
<h2>{{_('Administration')}}</h2> <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" 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" id="admin_restart"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="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
</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" 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 class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#UpdateprogressDialog">{{_('Perform Update')}}</div>
</div>
</div>
</div> </div>
<!-- Modal --> <!-- Modal -->
<div id="RestartDialog" class="modal fade" role="dialog"> <div id="RestartDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">
@ -130,7 +165,6 @@
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="UpdateprogressDialog" class="modal fade" role="dialog"> <div id="UpdateprogressDialog" class="modal fade" role="dialog">

@ -36,7 +36,9 @@
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}">
<p class="title">{{entry.title|shortentitle}}</p> <p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a> <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="col-sm-3 col-lg-3 col-xs-12">
<div class="cover"> <div class="cover">
{% if book.has_cover %} {% if book.has_cover %}
<img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" /> <img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" alt="{{ book.title }}"/>
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/>
{% endif %} {% endif %}
</div> </div>
{% if g.user.role_delete_books() %} {% if g.user.role_delete_books() %}
@ -26,7 +26,7 @@
{% endif %} {% endif %}
{% 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> <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"> <form class="padded-bottom" action="{{ url_for('convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
<div class="form-group"> <div class="form-group">
@ -34,8 +34,8 @@
<label class="control-label" for="book_format_from">{{_('Convert from:')}}</label> <label class="control-label" for="book_format_from">{{_('Convert from:')}}</label>
<select class="form-control" name="book_format_from" id="book_format_from"> <select class="form-control" name="book_format_from" id="book_format_from">
<option disabled selected value> -- {{_('select an option')}} -- </option> <option disabled selected value> -- {{_('select an option')}} -- </option>
{% for file in book.data %} {% for format in source_formats %}
<option>{{file.format}} </option> <option>{{format|upper}} </option>
{% endfor %} {% endfor %}
</select> </select>
<label class="control-label" for="book_format_to">{{_('Convert to:')}}</label> <label class="control-label" for="book_format_to">{{_('Convert to:')}}</label>
@ -101,7 +101,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="publisher">{{_('Publisher')}}</label> <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>
<div class="form-group"> <div class="form-group">
<label for="languages">{{_('Language')}}</label> <label for="languages">{{_('Language')}}</label>
@ -174,7 +174,7 @@
</label> </label>
</div> </div>
<a href="#" id="get_meta" class="btn btn-default" data-toggle="modal" data-target="#metaModal">{{_('Get metadata')}}</a> <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> <a href="{{ url_for('show_book', book_id=book.id) }}" class="btn btn-default">{{_('Back')}}</a>
</div> </div>
</form> </form>

@ -207,12 +207,12 @@
<div class="col-sm-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 %} {% if not origin %}
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a>
{% endif %} {% endif %}
{% if success %} {% 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 %} {% endif %}
</div> </div>
</form> </form>

@ -143,6 +143,10 @@
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}> <input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
<label for="show_author">{{_('Show author selection')}}</label> <label for="show_author">{{_('Show author selection')}}</label>
</div> </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"> <div class="form-group">
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}> <input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
<label for="show_read_and_unread">{{_('Show read and unread')}}</label> <label for="show_read_and_unread">{{_('Show read and unread')}}</label>
@ -160,7 +164,7 @@
</div> </div>
</div> </div>
<div class="col-sm-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>
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a>
</div> </div>
</form> </form>

@ -60,7 +60,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<h2>{{entry.title|shortentitle(40)}}</h2> <h2 id="title">{{entry.title|shortentitle(40)}}</h2>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
@ -120,13 +120,17 @@
</div> </div>
{% endif %} {% endif %}
{% if entry.publishers|length > 0 %} {% if entry.publishers|length > 0 %}
<div class="publishers"> <div class="publishers">
<p> <p>
<span>{{_('Publisher')}}:{% for publisher in entry.publishers %} {{publisher.name}}{% if not loop.last %},{% endif %}{% endfor %}</span> <span>{{_('Publisher')}}:
<a href="{{url_for('publisher', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a>
</span>
</p> </p>
</div> </div>
{% endif %} {% endif %}
{% if entry.pubdate[:10] != '0101-01-01' %} {% if entry.pubdate[:10] != '0101-01-01' %}
<p>{{_('Publishing date')}}: {{entry.pubdate|formatdate}} </p> <p>{{_('Publishing date')}}: {{entry.pubdate|formatdate}} </p>
{% endif %} {% endif %}
@ -174,8 +178,10 @@
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0%} {% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
<div class="comments">
<h3>{{_('Description:')}}</h3> <h3>{{_('Description:')}}</h3>
{{entry.comments[0].text|safe}} {{entry.comments[0].text|safe}}
</div>
{% endif %} {% endif %}
@ -248,7 +254,7 @@
{% if g.user.role_edit() %} {% if g.user.role_edit() %}
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Edit/Delete book"> <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>
</div> </div>
{% endif %} {% endif %}

@ -14,7 +14,9 @@
{% endif %} {% endif %}
</div> </div>
<div class="meta"> <div class="meta">
<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> <p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <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 }}" href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}"
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/> type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
{% endif %} {% 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> <title>{{instance}}</title>
<author> <author>
<name>{{instance}}</name> <name>{{instance}}</name>
@ -40,9 +43,16 @@
<title>{{entry.title}}</title> <title>{{entry.title}}</title>
<id>{{entry.uuid}}</id> <id>{{entry.uuid}}</id>
<updated>{{entry.atom_timestamp}}</updated> <updated>{{entry.atom_timestamp}}</updated>
{% if entry.authors.__len__() > 0 %}
<author> <author>
<name>{{entry.authors[0].name}}</name> <name>{{entry.authors[0].name}}</name>
</author> </author>
{% endif %}
{% if entry.publishers.__len__() > 0 %}
<publisher>
<name>{{entry.publishers[0].name}}</name>
</publisher>
{% endif %}
<dcterms:language>{{entry.language}}</dcterms:language> <dcterms:language>{{entry.language}}</dcterms:language>
{% for tag in entry.tags %} {% for tag in entry.tags %}
<category scheme="http://www.bisg.org/standards/bisac_subject/index.html" <category scheme="http://www.bisg.org/standards/bisac_subject/index.html"

@ -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> </a>
</div> </div>
<div class="meta"> <div class="meta">
<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> <p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
@ -60,7 +62,9 @@
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<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> <p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <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="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')}}" <link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/> 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> <title>{{instance}}</title>
<author> <author>
<name>{{instance}}</name> <name>{{instance}}</name>
@ -39,7 +42,7 @@
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Show Random Books')}}</content> <content type="text">{{_('Show Random Books')}}</content>
</entry> </entry>
{% if not current_user.is_anonymous %} {% if not current_user.is_anonymous %}
<entry> <entry>
<title>{{_('Read Books')}}</title> <title>{{_('Read Books')}}</title>
<link rel="subsection" href="{{url_for('feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -47,7 +50,7 @@
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Read Books')}}</content> <content type="text">{{_('Read Books')}}</content>
</entry> </entry>
{% endif %} {% endif %}
<entry> <entry>
<title>{{_('Unread Books')}}</title> <title>{{_('Unread Books')}}</title>
<link rel="subsection" href="{{url_for('feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -62,6 +65,13 @@
<updated>{{ current_time }}</updated> <updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by Author')}}</content> <content type="text">{{_('Books ordered by Author')}}</content>
</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> <entry>
<title>{{_('Category list')}}</title> <title>{{_('Category list')}}</title>
<link rel="subsection" href="{{url_for('feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/> <link rel="subsection" href="{{url_for('feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>

@ -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> <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%} {%endif%}
{% if g.user.show_sorted() %} {% 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"> <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="glyphicon glyphicon-sort-by-attributes"></span>{{_('Sorted Books')}}
<span class="caret"></span> <span class="caret"></span>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li {% if page == 'newest' %}class="active"{% endif %}><a href="{{url_for('newest_books')}}">{{_('Sort By')}} {{_('Newest')}}</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 {% if page == 'oldest' %}class="active"{% endif %}><a href="{{url_for('oldest_books')}}">{{_('Sort By')}} {{_('Oldest')}}</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 {% if page == 'a-z' %}class="active"{% endif %}><a href="{{url_for('titles_ascending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Ascending')}})</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 {% if page == 'z-a' %}class="active"{% endif %}><a href="{{url_for('titles_descending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Descending')}})</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> </ul>
</li> </li>
{%endif%} {%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> <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%} {%endif%}
{% if g.user.show_best_rated_books() %} {% 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%} {%endif%}
{% if g.user.show_read_and_unread() %} {% if g.user.show_read_and_unread() %}
{% if not g.user.is_anonymous %} {% 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%} {%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%} {%endif%}
{% if g.user.show_random_books() %} {% 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> <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() %} {% 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> <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%} {%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() %} {% 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> <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%} {%endif%}
{% if g.user.is_authenticated or g.user.is_anonymous %} {% if g.user.is_authenticated or g.user.is_anonymous %}
<li class="nav-head hidden-xs">{{_('Public Shelves')}}</li> <li class="nav-head hidden-xs">{{_('Public Shelves')}}</li>
{% for shelf in g.public_shelfes %} {% 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 %} {% endfor %}
<li class="nav-head hidden-xs">{{_('Your Shelves')}}</li> <li class="nav-head hidden-xs">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %} {% 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 %} {% endfor %}
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('create_shelf')}}">{{_('Create a Shelf')}}</a></li> <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> <label for="nickname">{{_('Username')}}</label>
<input type="text" class="form-control" id="nickname" name="nickname" placeholder="{{_('Choose a username')}}" required> <input type="text" class="form-control" id="nickname" name="nickname" placeholder="{{_('Choose a username')}}" required>
</div> </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"> <div class="form-group required">
<label for="email">{{_('E-mail address')}}</label> <label for="email">{{_('E-mail address')}}</label>
<input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required> <input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required>
</div> </div>
<button type="submit" class="btn btn-primary">{{_('Register')}}</button> <button type="submit" id="submit" class="btn btn-primary">{{_('Register')}}</button>
</form> </form>
</div> </div>
{% if error %} {% if error %}

@ -41,7 +41,9 @@
{% endif %} {% endif %}
</div> </div>
<div class="meta"> <div class="meta">
<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> <p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>

@ -4,9 +4,9 @@
<h2>{{title}}</h2> <h2>{{title}}</h2>
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %} {% 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> <div id="delete_shelf" 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 id="edit_shelf" 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> <a id="order_shelf" href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="row"> <div class="row">
@ -14,14 +14,18 @@
{% for entry in entries %} {% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <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"> <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('\\','/')) }}" /> {% if entry.has_cover %}
</a> <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 %} {% endif %}
</a>
</div> </div>
<div class="meta"> <div class="meta">
<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> <p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
@ -56,7 +60,7 @@
<div class="modal-body text-center"> <div class="modal-body text-center">
<span>{{_('Shelf will be lost for everybody and forever!')}}</span> <span>{{_('Shelf will be lost for everybody and forever!')}}</span>
<p></p> <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> <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
</div> </div>
</div> </div>

@ -14,7 +14,7 @@
</label> </label>
</div> </div>
{% endif %} {% 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 %} {% if shelf.id != None %}
<a href="{{ url_for('show_shelf', shelf_id=shelf.id) }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('show_shelf', shelf_id=shelf.id) }}" class="btn btn-default">{{_('Back')}}</a>
{% endif %} {% endif %}

@ -11,7 +11,7 @@
{% if g.user.role_admin() %} {% if g.user.role_admin() %}
<th data-halign="right" data-align="right" data-field="user" data-sortable="true">{{_('User')}}</th> <th data-halign="right" data-align="right" data-field="user" data-sortable="true">{{_('User')}}</th>
{% endif %} {% 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="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="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> <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"> <input type="password" class="form-control" name="password" id="password" value="" autocomplete="off">
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<label for="kindle_mail">{{_('Kindle E-Mail')}}</label> <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 %}> <input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
<label for="show_author">{{_('Show author selection')}}</label> <label for="show_author">{{_('Show author selection')}}</label>
</div> </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"> <div class="form-group">
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}> <input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
<label for="show_read_and_unread">{{_('Show read and unread')}}</label> <label for="show_read_and_unread">{{_('Show read and unread')}}</label>
@ -142,7 +144,7 @@
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %} {% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" name="delete"> {{_('Delete this user')}} <input type="checkbox" id="delete" name="delete"> {{_('Delete this user')}}
</label> </label>
</div> </div>
{% endif %} {% endif %}
@ -151,8 +153,8 @@
<button type="submit" id="submit" class="btn btn-default">{{_('Submit')}}</button> <button type="submit" id="submit" class="btn btn-default">{{_('Submit')}}</button>
{% if not profile %} {% if not profile %}
<a href="{{ url_for('admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a>
</div>
{% endif %} {% endif %}
</div>
</form> </form>
{% if downloads %} {% if downloads %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -41,6 +41,7 @@ SIDEBAR_READ_AND_UNREAD = 256
SIDEBAR_RECENT = 512 SIDEBAR_RECENT = 512
SIDEBAR_SORTED = 1024 SIDEBAR_SORTED = 1024
MATURE_CONTENT = 2048 MATURE_CONTENT = 2048
SIDEBAR_PUBLISHER = 4096
DEFAULT_PASS = "admin123" DEFAULT_PASS = "admin123"
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083)) DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
@ -136,6 +137,9 @@ class UserBase:
def show_author(self): def show_author(self):
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR)) 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): def show_best_rated_books(self):
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED)) 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_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0) config_public_reg = Column(SmallInteger, default=0)
config_default_role = 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_columns_to_ignore = Column(String)
config_use_google_drive = Column(Boolean) config_use_google_drive = Column(Boolean)
config_google_drive_folder = Column(String) config_google_drive_folder = Column(String)
@ -485,6 +489,10 @@ class Config:
return bool((self.config_default_show is not None) and return bool((self.config_default_show is not None) and
(self.config_default_show & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR)) (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): def show_best_rated_books(self):
return bool((self.config_default_show is not None) and return bool((self.config_default_show is not None) and
(self.config_default_show & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED)) (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.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 + \ 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_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) user.password = generate_password_hash(DEFAULT_PASS)

File diff suppressed because it is too large Load Diff

@ -33,12 +33,12 @@ from email.utils import formatdate
from email.utils import make_msgid from email.utils import make_msgid
chunksize = 8192 chunksize = 8192
# task 'status' consts
STAT_WAITING = 0 STAT_WAITING = 0
STAT_FAIL = 1 STAT_FAIL = 1
STAT_STARTED = 2 STAT_STARTED = 2
STAT_FINISH_SUCCESS = 3 STAT_FINISH_SUCCESS = 3
#taskType consts
TASK_EMAIL = 1 TASK_EMAIL = 1
TASK_CONVERT = 2 TASK_CONVERT = 2
TASK_UPLOAD = 3 TASK_UPLOAD = 3
@ -169,12 +169,12 @@ class WorkerThread(threading.Thread):
doLock.acquire() doLock.acquire()
if self.current != self.last: if self.current != self.last:
doLock.release() doLock.release()
if self.queue[self.current]['typ'] == TASK_EMAIL: if self.queue[self.current]['taskType'] == TASK_EMAIL:
self.send_raw_email() self._send_raw_email()
if self.queue[self.current]['typ'] == TASK_CONVERT: if self.queue[self.current]['taskType'] == TASK_CONVERT:
self.convert_any_format() self._convert_any_format()
if self.queue[self.current]['typ'] == TASK_CONVERT_ANY: if self.queue[self.current]['taskType'] == TASK_CONVERT_ANY:
self.convert_any_format() self._convert_any_format()
# TASK_UPLOAD is handled implicitly # TASK_UPLOAD is handled implicitly
self.current += 1 self.current += 1
else: else:
@ -190,7 +190,7 @@ class WorkerThread(threading.Thread):
else: else:
return "0 %" return "0 %"
def delete_completed_tasks(self): def _delete_completed_tasks(self):
for index, task in reversed(list(enumerate(self.UIqueue))): for index, task in reversed(list(enumerate(self.UIqueue))):
if task['progress'] == "100 %": if task['progress'] == "100 %":
# delete tasks # delete tasks
@ -202,40 +202,55 @@ class WorkerThread(threading.Thread):
def get_taskstatus(self): def get_taskstatus(self):
if self.current < len(self.queue): if self.current < len(self.queue):
if self.queue[self.current]['status'] == STAT_STARTED: if self.UIqueue[self.current]['stat'] == STAT_STARTED:
if self.queue[self.current]['typ'] == TASK_EMAIL: if self.queue[self.current]['taskType'] == TASK_EMAIL:
self.UIqueue[self.current]['progress'] = self.get_send_status() self.UIqueue[self.current]['progress'] = self.get_send_status()
self.UIqueue[self.current]['runtime'] = self._formatRuntime( self.UIqueue[self.current]['runtime'] = self._formatRuntime(
datetime.now() - self.queue[self.current]['starttime']) datetime.now() - self.queue[self.current]['starttime'])
return self.UIqueue return self.UIqueue
def convert_any_format(self): def _convert_any_format(self):
# convert book, and upload in case of google drive # convert book, and upload in case of google drive
self.queue[self.current]['status'] = STAT_STARTED self.UIqueue[self.current]['stat'] = STAT_STARTED
self.UIqueue[self.current]['status'] = _('Started')
self.queue[self.current]['starttime'] = datetime.now() self.queue[self.current]['starttime'] = datetime.now()
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime'] self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
curr_task = self.queue[self.current]['typ'] curr_task = self.queue[self.current]['taskType']
filename = self.convert_ebook_format() filename = self._convert_ebook_format()
if filename: if filename:
if web.ub.config.config_use_google_drive: if web.ub.config.config_use_google_drive:
gd.updateGdriveCalibreFromLocal() gd.updateGdriveCalibreFromLocal()
if curr_task == TASK_CONVERT: if curr_task == TASK_CONVERT:
self.add_email(_(u'Send to Kindle'), self.queue[self.current]['path'], filename, self.add_email(self.queue[self.current]['settings']['subject'], self.queue[self.current]['path'],
self.queue[self.current]['settings'], self.queue[self.current]['kindle'], 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.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 error_message = None
file_path = self.queue[self.current]['file_path'] file_path = self.queue[self.current]['file_path']
bookid = self.queue[self.current]['bookid'] bookid = self.queue[self.current]['bookid']
format_old_ext = u'.' + self.queue[self.current]['settings']['old_book_format'].lower() 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() 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 # check if converter-executable is existing
if not os.path.exists(web.ub.config.config_converterpath): 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 return
try: try:
# check which converter to use kindlegen is "1" # check which converter to use kindlegen is "1"
if format_old_ext == '.epub' and format_new_ext == '.mobi': 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) p = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
except OSError as e: except OSError as e:
self._handleError(_(u"Ebook-converter failed: %s" % e)) self._handleError(_(u"Ebook-converter failed: %(error)s", error=e))
return return
if web.ub.config.config_ebookconverter == 1: if web.ub.config.config_ebookconverter == 1:
@ -313,11 +328,7 @@ class WorkerThread(threading.Thread):
self.queue[self.current]['title'] = cur_book.title self.queue[self.current]['title'] = cur_book.title
if web.ub.config.config_use_google_drive: if web.ub.config.config_use_google_drive:
os.remove(file_path + format_old_ext) os.remove(file_path + format_old_ext)
self.queue[self.current]['status'] = STAT_FINISH_SUCCESS self._handleSuccess()
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'])
return file_path + format_new_ext return file_path + format_new_ext
else: else:
error_message = format_new_ext.upper() + ' format not found on disk' error_message = format_new_ext.upper() + ' format not found on disk'
@ -328,63 +339,62 @@ class WorkerThread(threading.Thread):
return 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 = threading.Lock()
addLock.acquire() addLock.acquire()
if self.last >= 20: if self.last >= 20:
self.delete_completed_tasks() self._delete_completed_tasks()
# progress, runtime, and status = 0 # progress, runtime, and status = 0
self.id += 1 self.id += 1
task = TASK_CONVERT_ANY task = TASK_CONVERT_ANY
if kindle_mail: if kindle_mail:
task = TASK_CONVERT task = TASK_CONVERT
self.queue.append({'file_path':file_path, 'bookid':bookid, 'starttime': 0, 'kindle': kindle_mail, self.queue.append({'file_path':file_path, 'bookid':bookid, 'starttime': 0, 'kindle': kindle_mail,
'status': STAT_WAITING, 'typ': task, 'settings':settings}) 'taskType': task, 'settings':settings})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'type': typ, self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
'runtime': '0 s', 'status': _('Waiting'),'id': self.id } ) 'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': task } )
self.last=len(self.queue) self.last=len(self.queue)
addLock.release() addLock.release()
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, taskMessage,
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, typ, text):
text=_(u'This e-mail has been sent via Calibre-Web.')):
# if more than 20 entries in the list, clean the list # if more than 20 entries in the list, clean the list
addLock = threading.Lock() addLock = threading.Lock()
addLock.acquire() addLock.acquire()
if self.last >= 20: if self.last >= 20:
self.delete_completed_tasks() self._delete_completed_tasks()
# progress, runtime, and status = 0 # progress, runtime, and status = 0
self.id += 1 self.id += 1
self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath, self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath,
'settings':settings, 'recipent':recipient, 'starttime': 0, 'settings':settings, 'recipent':recipient, 'starttime': 0,
'status': STAT_WAITING, 'typ': TASK_EMAIL, 'text':text}) 'taskType': TASK_EMAIL, 'text':text})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'type': typ, self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
'runtime': '0 s', 'status': _('Waiting'),'id': self.id }) 'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': TASK_EMAIL })
self.last=len(self.queue) self.last=len(self.queue)
addLock.release() 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 # if more than 20 entries in the list, clean the list
addLock = threading.Lock() addLock = threading.Lock()
addLock.acquire() addLock.acquire()
if self.last >= 20: if self.last >= 20:
self.delete_completed_tasks() self._delete_completed_tasks()
# progress=100%, runtime=0, and status finished # progress=100%, runtime=0, and status finished
self.id += 1 self.id += 1
self.queue.append({'starttime': datetime.now(), 'status': STAT_FINISH_SUCCESS, 'typ': TASK_UPLOAD}) self.queue.append({'starttime': datetime.now(), 'taskType': TASK_UPLOAD})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': "100 %", 'type': typ, self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': "100 %", 'taskMess': taskMessage,
'runtime': '0 s', 'status': _('Finished'),'id': self.id }) 'runtime': '0 s', 'stat': STAT_FINISH_SUCCESS,'id': self.id, 'taskType': TASK_UPLOAD})
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime'] self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
self.last=len(self.queue) self.last=len(self.queue)
addLock.release() addLock.release()
def send_raw_email(self): def _send_raw_email(self):
self.queue[self.current]['starttime'] = datetime.now() self.queue[self.current]['starttime'] = datetime.now()
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime'] self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
self.queue[self.current]['status'] = STAT_STARTED # self.queue[self.current]['status'] = STAT_STARTED
self.UIqueue[self.current]['status'] = _('Started') self.UIqueue[self.current]['stat'] = STAT_STARTED
obj=self.queue[self.current] obj=self.queue[self.current]
# create MIME message # create MIME message
msg = MIMEMultipart() 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.login(str(obj['settings']["mail_login"]), str(obj['settings']["mail_password"]))
self.asyncSMTP.sendmail(obj['settings']["mail_from"], obj['recipent'], msg) self.asyncSMTP.sendmail(obj['settings']["mail_from"], obj['recipent'], msg)
self.asyncSMTP.quit() self.asyncSMTP.quit()
self.queue[self.current]['status'] = STAT_FINISH_SUCCESS self._handleSuccess()
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'])
sys.stderr = org_stderr sys.stderr = org_stderr
except (MemoryError) as e: except (MemoryError) as e:
self._handleError(u'Error sending email: ' + e.message) self._handleError(u'Error sending email: ' + e.message)
return None return None
except (smtplib.SMTPException) as e: 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 return None
except (socket.error) as e: except (socket.error) as e:
self._handleError(u'Error sending email: ' + e.strerror) self._handleError(u'Error sending email: ' + e.strerror)
return None return None
def _formatRuntime(self, runtime): def _formatRuntime(self, runtime):
self.UIqueue[self.current]['rt'] = runtime.total_seconds() self.UIqueue[self.current]['rt'] = runtime.total_seconds()
val = re.split('\:|\.', str(runtime))[0:3] val = re.split('\:|\.', str(runtime))[0:3]
@ -468,15 +475,22 @@ class WorkerThread(threading.Thread):
def _handleError(self, error_message): def _handleError(self, error_message):
web.app.logger.error(error_message) web.app.logger.error(error_message)
self.queue[self.current]['status'] = STAT_FAIL # self.queue[self.current]['status'] = STAT_FAIL
self.UIqueue[self.current]['status'] = _('Failed') self.UIqueue[self.current]['stat'] = STAT_FAIL
self.UIqueue[self.current]['progress'] = "100 %" self.UIqueue[self.current]['progress'] = "100 %"
self.UIqueue[self.current]['runtime'] = self._formatRuntime( self.UIqueue[self.current]['runtime'] = self._formatRuntime(
datetime.now() - self.queue[self.current]['starttime']) datetime.now() - self.queue[self.current]['starttime'])
self.UIqueue[self.current]['message'] = error_message 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): class StderrLogger(object):
buffer = '' buffer = ''
@ -491,3 +505,4 @@ class StderrLogger(object):
self.buffer = '' self.buffer = ''
else: else:
self.buffer += message self.buffer += message

File diff suppressed because it is too large Load Diff

@ -15,6 +15,6 @@ six==1.10.0
goodreads>=0.3.2 goodreads>=0.3.2
python-Levenshtein>=0.12.0 python-Levenshtein>=0.12.0
# other # other
lxml==3.7.2 lxml>=3.8.0
rarfile>=2.7 rarfile>=2.7
natsort>=2.2.0 natsort>=2.2.0

@ -12,7 +12,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup - full graphical setup
- User management with fine grained per-user permissions - User management with fine grained per-user permissions
- Admin interface - Admin interface
- User Interface in dutch, english, french, german, 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 - OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language - Filter and search by titles, authors, tags, series and language
- Create custom book collection (shelves) - Create custom book collection (shelves)
@ -41,6 +41,9 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
*Username:* admin *Username:* admin
*Password:* admin123 *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 ## Runtime Configuration Options
The configuration can be changed as admin in the admin panel under "Configuration" The configuration can be changed as admin in the admin panel under "Configuration"

File diff suppressed because it is too large Load Diff

@ -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;
}

@ -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,'&amp;');
s = s.replace(/</g,'&lt;');
s = s.replace(/>/g,'&gt;');
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…
Cancel
Save