1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-12-29 03:20:30 +00:00

Merge pull request #19 from janeczku/master

merge from janeczku/master
This commit is contained in:
Ethan Lin 2018-11-14 11:45:09 +08:00 committed by GitHub
commit dec50b5682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 30098 additions and 306501 deletions

3
.gitattributes vendored
View File

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

View File

@ -123,9 +123,11 @@ def pdf_preview(tmp_file_path, tmp_dir):
def get_versions():
if not use_generic_pdf_cover:
IVersion=ImageVersion.MAGICK_VERSION
IVersion = ImageVersion.MAGICK_VERSION
WVersion = ImageVersion.VERSION
else:
IVersion = _(u'not installed')
WVersion = _(u'not installed')
if use_pdf_meta:
PVersion='v'+PyPdfVersion
else:
@ -134,4 +136,4 @@ def get_versions():
XVersion = 'v'+'.'.join(map(str, lxmlversion))
else:
XVersion = _(u'not installed')
return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion}
return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion, 'Wand Version': WVersion}

View File

@ -44,5 +44,9 @@ if args.k:
print("Keyfilepath is invalid. Exiting...")
sys.exit(1)
if (args.k and not args.c) or (not args.k and args.c):
print("Certfile and Keyfile have to be used together. Exiting...")
sys.exit(1)
if args.k is "":
keyfilepath = ""

View File

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

View File

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

182
cps/helper.py Executable file → Normal file
View File

@ -13,9 +13,10 @@ import unicodedata
from io import BytesIO
import worker
import time
from flask import send_from_directory, make_response, redirect, abort
from flask_babel import gettext as _
from flask_login import current_user
from babel.dates import format_datetime
import threading
import shutil
import requests
@ -73,11 +74,14 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
# read settings and append converter task to queue
if kindle_mail:
settings = ub.get_mail_settings()
text = _(u"Convert: %s" % book.title)
settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
# text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title)
else:
text = _(u"Convert to %(format)s: %(book)s", format=new_book_format, book=book.title)
settings['old_book_format'] = u'EPUB'
settings['new_book_format'] = u'MOBI'
settings = dict()
text = (u"%s -> %s: %s" % (old_book_format, new_book_format, book.title))
settings['old_book_format'] = old_book_format
settings['new_book_format'] = new_book_format
global_WorkerThread.add_convert(file_path, book.id, user_id, text, settings, kindle_mail)
return None
else:
@ -88,7 +92,8 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
def send_test_mail(kindle_mail, user_name):
global_WorkerThread.add_email(_(u'Calibre-Web test e-mail'),None, None, ub.get_mail_settings(),
kindle_mail, user_name, _(u"Test e-mail"))
kindle_mail, user_name, _(u"Test e-mail"),
_(u'This e-mail has been sent via Calibre-Web.'))
return
@ -104,7 +109,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
text += "Sincerely\r\n\r\n"
text += "Your Calibre-Web team"
global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, ub.get_mail_settings(),
e_mail, user_name, _(u"Registration e-mail for user: %s" % user_name),text)
e_mail, user_name, _(u"Registration e-mail for user: %(name)s", name=user_name), text)
return
@ -132,7 +137,7 @@ def send_mail(book_id, kindle_mail, calibrepath, user_id):
if 'mobi' in formats:
result = formats['mobi']
elif 'epub' in formats:
# returns None if sucess, otherwise errormessage
# returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'epub', u'mobi', user_id, kindle_mail)
elif 'pdf' in formats:
result = formats['pdf'] # worker.get_attachment()
@ -140,7 +145,8 @@ def send_mail(book_id, kindle_mail, calibrepath, user_id):
return _(u"Could not find any formats suitable for sending by e-mail")
if result:
global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(),
kindle_mail, user_id, _(u"E-Mail: %s" % book.title))
kindle_mail, user_id, _(u"E-mail: %(book)s", book=book.title),
_(u'This e-mail has been sent via Calibre-Web.'))
else:
return _(u"The requested file could not be read. Maybe wrong permissions?")
@ -177,13 +183,18 @@ def get_valid_filename(value, replace_whitespace=True):
def get_sorted_author(value):
try:
regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"]
combined = "(" + ")|(".join(regexes) + ")"
value = value.split(" ")
if re.match(combined, value[-1].upper()):
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
if ',' not in value:
regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"]
combined = "(" + ")|(".join(regexes) + ")"
value = value.split(" ")
if re.match(combined, value[-1].upper()):
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
elif len(value) == 1:
value2 = value[0]
else:
value2 = value[-1] + ", " + " ".join(value[:-1])
else:
value2 = value[-1] + ", " + " ".join(value[:-1])
value2 = value
except Exception:
web.app.logger.error("Sorting author " + str(value) + "failed")
value2 = value
@ -235,18 +246,18 @@ def update_dir_structure_file(book_id, calibrepath):
path = new_title_path
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
except OSError as ex:
web.app.logger.error("Rename title from: " + path + " to " + new_title_path)
web.app.logger.error(ex, exc_info=True)
return _('Rename title from: "%s" to "%s" failed with error: %s' % (path, new_title_path, str(ex)))
web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex))
web.app.logger.debug(ex, exc_info=True)
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_title_path, error=str(ex))
if authordir != new_authordir:
try:
new_author_path = os.path.join(os.path.join(calibrepath, new_authordir), os.path.basename(path))
os.renames(path, new_author_path)
localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
except OSError as ex:
web.app.logger.error("Rename author from: " + path + " to " + new_author_path)
web.app.logger.error(ex, exc_info=True)
return _('Rename author from: "%s" to "%s" failed with error: %s' % (path, new_title_path, str(ex)))
web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex))
web.app.logger.debug(ex, exc_info=True)
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_author_path, error=str(ex))
return False
@ -260,7 +271,6 @@ def update_dir_structure_gdrive(book_id):
new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")"
if titledir != new_titledir:
# print (titledir)
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if gFile:
gFile['title'] = new_titledir
@ -269,7 +279,7 @@ def update_dir_structure_gdrive(book_id):
book.path = book.path.split('/')[0] + '/' + new_titledir
gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected
else:
error = _(u'File %s not found on Google Drive' % book.path) # file not found
error = _(u'File %(file)s not found on Google Drive', file= book.path) # file not found
if authordir != new_authordir:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
@ -278,7 +288,7 @@ def update_dir_structure_gdrive(book_id):
book.path = new_authordir + '/' + book.path.split('/')[1]
gd.updateDatabaseOnEdit(gFile['id'], book.path)
else:
error = _(u'File %s not found on Google Drive' % authordir) # file not found
error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
return error
@ -296,7 +306,7 @@ def delete_book_gdrive(book, book_format):
gd.deleteDatabaseEntry(gFile['id'])
gFile.Trash()
else:
error =_(u'Book path %s not found on Google Drive' % book.path) # file not found
error =_(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
return error
def generate_random_password():
@ -367,7 +377,11 @@ def do_download_file(book, book_format, data, headers):
else:
abort(404)
else:
response = make_response(send_from_directory(os.path.join(ub.config.config_calibre_dir, book.path), data.name + "." + book_format))
filename = os.path.join(ub.config.config_calibre_dir, book.path)
if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)):
# ToDo: improve error handling
web.app.logger.error('File not found: %s' % os.path.join(filename, data.name + "." + book_format))
response = make_response(send_from_directory(filename, data.name + "." + book_format))
response.headers = headers
return response
@ -381,25 +395,37 @@ class Updater(threading.Thread):
self.status = 0
def run(self):
self.status = 1
r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True)
fname = re.findall("filename=(.+)", r.headers['content-disposition'])[0]
self.status = 2
z = zipfile.ZipFile(BytesIO(r.content))
self.status = 3
tmp_dir = gettempdir()
z.extractall(tmp_dir)
self.status = 4
self.update_source(os.path.join(tmp_dir, os.path.splitext(fname)[0]), ub.config.get_main_dir)
self.status = 5
db.session.close()
db.engine.dispose()
ub.session.close()
ub.engine.dispose()
self.status = 6
server.Server.setRestartTyp(True)
server.Server.stopServer()
self.status = 7
try:
self.status = 1
r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True)
r.raise_for_status()
fname = re.findall("filename=(.+)", r.headers['content-disposition'])[0]
self.status = 2
z = zipfile.ZipFile(BytesIO(r.content))
self.status = 3
tmp_dir = gettempdir()
z.extractall(tmp_dir)
self.status = 4
self.update_source(os.path.join(tmp_dir, os.path.splitext(fname)[0]), ub.config.get_main_dir)
self.status = 6
time.sleep(2)
server.Server.setRestartTyp(True)
server.Server.stopServer()
self.status = 7
time.sleep(2)
except requests.exceptions.HTTPError as ex:
logging.getLogger('cps.web').info( u'HTTP Error' + ' ' + str(ex))
self.status = 8
except requests.exceptions.ConnectionError:
logging.getLogger('cps.web').info(u'Connection error')
self.status = 9
except requests.exceptions.Timeout:
logging.getLogger('cps.web').info(u'Timeout while establishing connection')
self.status = 10
except requests.exceptions.RequestException:
self.status = 11
logging.getLogger('cps.web').info(u'General error')
def get_update_status(self):
return self.status
@ -523,7 +549,7 @@ class Updater(threading.Thread):
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True)
def check_unrar(unrarLocation):
error = False
if os.path.exists(unrarLocation):
@ -547,3 +573,67 @@ def check_unrar(unrarLocation):
error=True
return (error, version)
def is_sha1(sha1):
if len(sha1) != 40:
return False
try:
int(sha1, 16)
except ValueError:
return False
return True
def get_current_version_info():
content = {}
content[0] = '$Format:%H$'
content[1] = '$Format:%cI$'
# content[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
# content[1] = '2018-09-09T10:13:08+02:00'
if is_sha1(content[0]) and len(content[1]) > 0:
return {'hash': content[0], 'datetime': content[1]}
return False
def render_task_status(tasklist):
#helper function to apply localize status information in tasklist entries
renderedtasklist=list()
for task in tasklist:
if task['user'] == current_user.nickname or current_user.role_admin():
if task['formStarttime']:
task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale())
task['formStarttime'] = ""
else:
if 'starttime' not in task:
task['starttime'] = ""
# localize the task status
if isinstance( task['stat'], int ):
if task['stat'] == worker.STAT_WAITING:
task['status'] = _(u'Waiting')
elif task['stat'] == worker.STAT_FAIL:
task['status'] = _(u'Failed')
elif task['stat'] == worker.STAT_STARTED:
task['status'] = _(u'Started')
elif task['stat'] == worker.STAT_FINISH_SUCCESS:
task['status'] = _(u'Finished')
else:
task['status'] = _(u'Unknown Status')
# localize the task type
if isinstance( task['taskType'], int ):
if task['taskType'] == worker.TASK_EMAIL:
task['taskMessage'] = _(u'E-mail: ') + task['taskMess']
elif task['taskType'] == worker.TASK_CONVERT:
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
elif task['taskType'] == worker.TASK_UPLOAD:
task['taskMessage'] = _(u'Upload: ') + task['taskMess']
elif task['taskType'] == worker.TASK_CONVERT_ANY:
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
else:
task['taskMessage'] = _(u'Unknown Task: ') + task['taskMess']
renderedtasklist.append(task)
return renderedtasklist

39
cps/reverseproxy.py Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
class ReverseProxied(object):
"""Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is
different than what is used locally.
Code courtesy of: http://flask.pocoo.org/snippets/35/
In nginx:
location /myprefix {
proxy_pass http://127.0.0.1:8083;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /myprefix;
}
"""
def __init__(self, application):
self.app = application
def __call__(self, environ, start_response):
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
if script_name:
environ['SCRIPT_NAME'] = script_name
path_info = environ.get('PATH_INFO', '')
if path_info and path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):]
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
environ['wsgi.url_scheme'] = scheme
servr = environ.get('HTTP_X_FORWARDED_SERVER', '')
if servr:
environ['HTTP_HOST'] = servr
return self.app(environ, start_response)

View File

@ -1,10 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from socket import error as SocketError
import sys
import os
import signal
import web
try:
from gevent.pywsgi import WSGIServer
from gevent.pool import Pool
@ -17,8 +19,6 @@ except ImportError:
from tornado import version as tornadoVersion
gevent_present = False
import web
class server:
@ -26,7 +26,8 @@ class server:
restart= False
def __init__(self):
pass
signal.signal(signal.SIGINT, self.killServer)
signal.signal(signal.SIGTERM, self.killServer)
def start_gevent(self):
try:
@ -39,11 +40,16 @@ class server:
else:
self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
self.wsgiserver.serve_forever()
except SocketError:
web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...')
self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
self.wsgiserver.serve_forever()
try:
web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...')
self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
self.wsgiserver.serve_forever()
except (OSError, SocketError) as e:
web.app.logger.info("Error starting server: %s" % e.strerror)
print("Error starting server: %s" % e.strerror)
web.helper.global_WorkerThread.stop()
sys.exit(1)
except Exception:
web.app.logger.info("Unknown error while starting gevent")
@ -53,20 +59,27 @@ class server:
# leave subprocess out to allow forking for fetchers and processors
self.start_gevent()
else:
web.app.logger.info('Starting Tornado server')
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
ssl={"certfile": web.ub.config.get_config_certfile(),
"keyfile": web.ub.config.get_config_keyfile()}
else:
ssl=None
# Max Buffersize set to 200MB
http_server = HTTPServer(WSGIContainer(web.app),
max_buffer_size = 209700000,
ssl_options=ssl)
http_server.listen(web.ub.config.config_port)
self.wsgiserver=IOLoop.instance()
self.wsgiserver.start() # wait for stop signal
self.wsgiserver.close(True)
try:
web.app.logger.info('Starting Tornado server')
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
ssl={"certfile": web.ub.config.get_config_certfile(),
"keyfile": web.ub.config.get_config_keyfile()}
else:
ssl=None
# Max Buffersize set to 200MB
http_server = HTTPServer(WSGIContainer(web.app),
max_buffer_size = 209700000,
ssl_options=ssl)
http_server.listen(web.ub.config.config_port)
self.wsgiserver=IOLoop.instance()
self.wsgiserver.start()
# wait for stop signal
self.wsgiserver.close(True)
except SocketError as e:
web.app.logger.info("Error starting server: %s" % e.strerror)
print("Error starting server: %s" % e.strerror)
web.helper.global_WorkerThread.stop()
sys.exit(1)
if self.restart == True:
web.app.logger.info("Performing restart of Calibre-Web")
@ -86,6 +99,9 @@ class server:
def setRestartTyp(self,starttyp):
self.restart=starttyp
def killServer(self, signum, frame):
self.stopServer()
def stopServer(self):
if gevent_present:
self.wsgiserver.close()

View File

@ -5,6 +5,22 @@
src: local('Grand Hotel'), local('GrandHotel-Regular'), url("fonts/GrandHotel-Regular.ttf") format('truetype');
}
html.http-error {
margin: 0;
height: 100%;
}
.http-error body {
margin: 0;
height: 100%;
display: table;
width: 100%;
}
.http-error body > div {
display: table-cell;
vertical-align: middle;
text-align: center;
}
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
body { margin-bottom: 40px;}
a{color: #45b29d}a:hover{color: #444;}

View File

@ -142,6 +142,17 @@ var languages = new Bloodhound({
}
});
var publishers = new Bloodhound({
name: "publisher",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: getPath() + "/get_publishers_json?q=%QUERY"
}
});
function sourceSplit(query, cb, split, source) {
var bhAdapter = source.ttAdapter();
@ -224,6 +235,20 @@ promiseLanguages.done(function() {
);
});
var promisePublishers = publishers.initialize();
promisePublishers.done(function() {
$("#publisher").typeahead(
{
highlight: true, minLength: 0,
hint: true
}, {
name: "publishers",
displayKey: "name",
source: publishers.ttAdapter()
}
);
});
$("#search").on("change input.typeahead:selected", function() {
var form = $("form").serialize();
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) {

View File

@ -104,18 +104,39 @@ $(function() {
var $this = $(this);
var buttonText = $this.html();
$this.html("...");
$("#update_error").addClass("hidden")
$.ajax({
dataType: "json",
url: window.location.pathname + "/../../get_update_status",
success: function success(data) {
$this.html(buttonText);
if (data.status === true) {
$("#check_for_update").addClass("hidden");
$("#perform_update").removeClass("hidden");
$("#update_info")
.removeClass("hidden")
.find("span").html(data.commit);
var cssClass = '';
var message = ''
if (data.success === true) {
if (data.update === true) {
$("#check_for_update").addClass("hidden");
$("#perform_update").removeClass("hidden");
$("#update_info")
.removeClass("hidden")
.find("span").html(data.commit);
data.history.reverse().forEach(function(entry, index) {
$("<tr><td>" + entry[0] + "</td><td>" + entry[1] + "</td></tr>").appendTo($("#update_table"));
});
cssClass = 'alert-warning'
} else {
cssClass = 'alert-success'
}
} else {
cssClass = 'alert-danger'
}
message = '<div class="alert ' + cssClass
+ ' fade in"><a href="#" class="close" data-dismiss="alert">&times;</a>' + data.message + '</div>';
$(message).insertAfter($("#update_table"));
}
});
});

View File

@ -1,105 +1,140 @@
{% extends "layout.html" %}
{% block body %}
<div class="discover">
<h2>{{_('User list')}}</h2>
<table class="table table-striped" id="table_user">
<tr>
<th>{{_('Nickname')}}</th>
<th>{{_('E-mail')}}</th>
<th>{{_('Kindle')}}</th>
<th>{{_('DLS')}}</th>
<th class="hidden-xs">{{_('Admin')}}</th>
<th class="hidden-xs">{{_('Download')}}</th>
<th class="hidden-xs">{{_('Upload')}}</th>
<th class="hidden-xs">{{_('Edit')}}</th>
</tr>
{% for user in content %}
{% if not user.role_anonymous() or config.config_anonbrowse %}
<tr>
<td><a href="{{url_for('edit_user', user_id=user.id)}}">{{user.nickname}}</a></td>
<td>{{user.email}}</td>
<td>{{user.kindle_mail}}</td>
<td>{{user.downloads.count()}}</td>
<td class="hidden-xs">{% if user.role_admin() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td class="hidden-xs">{% if user.role_download() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td class="hidden-xs">{% if user.role_upload() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td class="hidden-xs">{% if user.role_edit() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
</tr>
{% endif %}
{% endfor %}
</table>
<div class="btn btn-default" id="admin_new_user"><a href="{{url_for('new_user')}}">{{_('Add new user')}}</a></div>
<h2>{{_('SMTP e-mail server settings')}}</h2>
<table class="table table-striped" id="table_email">
<tr>
<th>{{_('SMTP hostname')}}</th>
<th>{{_('SMTP port')}}</th>
<th>{{_('SSL')}}</th>
<th>{{_('SMTP login')}}</th>
<th class="hidden-xs">{{_('From mail')}}</th>
</tr>
<tr>
<td>{{email.mail_server}}</td>
<td>{{email.mail_port}}</td>
<td>{% if email.mail_use_ssl %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td>{{email.mail_login}}</td>
<td class="hidden-xs">{{email.mail_from}}</td>
</table>
<div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div>
<div id="container">
<h2>{{_('Configuration')}}</h2>
<div class="col-xs-12 col-sm-6">
<div class="Row">
<div class="col-xs-6 col-sm-6">{{_('Calibre DB dir')}}</div>
<div class="col-xs-6 col-sm-6">{{config.config_calibre_dir}}</div>
</div>
<div class="Row">
<div class="col-xs-6 col-sm-6">{{_('Log level')}}</div>
<div class="col-xs-6 col-sm-6">{{config.get_Log_Level()}}</div>
</div>
<div class="Row">
<div class="col-xs-6 col-sm-6">{{_('Port')}}</div>
<div class="col-xs-6 col-sm-6">{{config.config_port}}</div>
<div class="container-fluid">
<div class="row">
<div class="col">
<h2>{{_('User list')}}</h2>
<table class="table table-striped" id="table_user">
<tr>
<th>{{_('Nickname')}}</th>
<th>{{_('E-mail')}}</th>
<th>{{_('Kindle')}}</th>
<th>{{_('DLS')}}</th>
<th class="hidden-xs">{{_('Admin')}}</th>
<th class="hidden-xs">{{_('Download')}}</th>
<th class="hidden-xs">{{_('Upload')}}</th>
<th class="hidden-xs">{{_('Edit')}}</th>
</tr>
{% for user in content %}
{% if not user.role_anonymous() or config.config_anonbrowse %}
<tr>
<td><a href="{{url_for('edit_user', user_id=user.id)}}">{{user.nickname}}</a></td>
<td>{{user.email}}</td>
<td>{{user.kindle_mail}}</td>
<td>{{user.downloads.count()}}</td>
<td class="hidden-xs">{% if user.role_admin() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td class="hidden-xs">{% if user.role_download() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td class="hidden-xs">{% if user.role_upload() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td class="hidden-xs">{% if user.role_edit() %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
</tr>
{% endif %}
{% endfor %}
</table>
<div class="btn btn-default" id="admin_new_user"><a href="{{url_for('new_user')}}">{{_('Add new user')}}</a></div>
</div>
</div>
<div class="col-xs-12 col-sm-6">
<div class="Row">
<div class="col-xs-6 col-sm-7">{{_('Books per page')}}</div>
<div class="col-xs-6 col-sm-5">{{config.config_books_per_page}}</div>
<div class="row">
<div class="col">
<h2>{{_('SMTP e-mail server settings')}}</h2>
<table class="table table-striped" id="table_email">
<tr>
<th>{{_('SMTP hostname')}}</th>
<th>{{_('SMTP port')}}</th>
<th>{{_('SSL')}}</th>
<th>{{_('SMTP login')}}</th>
<th class="hidden-xs">{{_('From mail')}}</th>
</tr>
<tr>
<td>{{email.mail_server}}</td>
<td>{{email.mail_port}}</td>
<td>{% if email.mail_use_ssl %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</td>
<td>{{email.mail_login}}</td>
<td class="hidden-xs">{{email.mail_from}}</td>
</tr>
</table>
<div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div>
</div>
<div class="Row">
<div class="col-xs-6 col-sm-7">{{_('Uploading')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_uploading %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="row">
<div class="col">
<h2>{{_('Configuration')}}</h2>
<div class="col-xs-12 col-sm-6">
<div class="row">
<div class="col-xs-6 col-sm-6">{{_('Calibre DB dir')}}</div>
<div class="col-xs-6 col-sm-6">{{config.config_calibre_dir}}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6">{{_('Log level')}}</div>
<div class="col-xs-6 col-sm-6">{{config.get_Log_Level()}}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6">{{_('Port')}}</div>
<div class="col-xs-6 col-sm-6">{{config.config_port}}</div>
</div>
</div>
<div class="col-xs-12 col-sm-6">
<div class="row">
<div class="col-xs-6 col-sm-7">{{_('Books per page')}}</div>
<div class="col-xs-6 col-sm-5">{{config.config_books_per_page}}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-7">{{_('Uploading')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_uploading %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-7">{{_('Anonymous browsing')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_anonbrowse %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-7">{{_('Public registration')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_public_reg %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="row">
<div class="col-xs-6 col-sm-7">{{_('Remote login')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_remote_login %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
</div>
<div class="btn btn-default"><a id="basic_config" href="{{url_for('configuration')}}">{{_('Basic Configuration')}}</a></div>
<div class="btn btn-default"><a id="view_config" href="{{url_for('view_configuration')}}">{{_('UI Configuration')}}</a></div>
</div>
<div class="Row">
<div class="col-xs-6 col-sm-7">{{_('Anonymous browsing')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_anonbrowse %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="row">
<div class="col">
<h2>{{_('Administration')}}</h2>
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
<div class="btn btn-default" id="admin_restart"data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div>
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
</div>
<div class="Row">
<div class="col-xs-6 col-sm-7">{{_('Public registration')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_public_reg %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="Row">
<div class="col-xs-6 col-sm-7">{{_('Remote login')}}</div>
<div class="col-xs-6 col-sm-5">{% if config.config_remote_login %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div>
<div class="row">
<div class="col">
<h2>{{_('Update')}}</h2>
<table class="table table-striped" id="update_table">
<thead>
<tr>
<th class="col-xs-3">{{_('Version')}}</th>
<th class="col-xl-8">{{_('Details')}}</th>
</tr>
</thead>
<tbody>
<tr id="current_version">
<td>{{commit}} </td>
<td><i>{{_('Current version')}}</i></td>
</tr>
</tbody>
</table>
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>
<div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#UpdateprogressDialog">{{_('Perform Update')}}</div>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-12">
<p></p>
<div class="btn btn-default"><a href="{{url_for('configuration')}}">{{_('Basic Configuration')}}</a></div>
<div class="btn btn-default"><a href="{{url_for('view_configuration')}}">{{_('UI Configuration')}}</a></div>
</div>
<h2>{{_('Administration')}}</h2>
<div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div>
<div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div>
<p></p>
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
<div class="btn btn-default" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div>
<div class="btn btn-default" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>
<div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#UpdateprogressDialog">{{_('Perform Update')}}</div>
</div>
<!-- Modal -->
<div id="RestartDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
@ -130,7 +165,6 @@
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
</div>
</div>
</div>
</div>
<div id="UpdateprogressDialog" class="modal fade" role="dialog">

View File

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

View File

@ -6,9 +6,9 @@
<div class="col-sm-3 col-lg-3 col-xs-12">
<div class="cover">
{% if book.has_cover %}
<img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" />
<img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" alt="{{ book.title }}"/>
{% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" />
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/>
{% endif %}
</div>
{% if g.user.role_delete_books() %}
@ -26,7 +26,7 @@
{% endif %}
{% endif %}
{% if display_convertbtn and conversion_formats|length > 0 %}
{% if source_formats|length > 0 and conversion_formats|length > 0 %}
<div class="text-center more-stuff"><h4>{{_('Convert book format:')}}</h4>
<form class="padded-bottom" action="{{ url_for('convert_bookformat', book_id=book.id) }}" method="post" id="book_convert_frm">
<div class="form-group">
@ -34,8 +34,8 @@
<label class="control-label" for="book_format_from">{{_('Convert from:')}}</label>
<select class="form-control" name="book_format_from" id="book_format_from">
<option disabled selected value> -- {{_('select an option')}} -- </option>
{% for file in book.data %}
<option>{{file.format}} </option>
{% for format in source_formats %}
<option>{{format|upper}} </option>
{% endfor %}
</select>
<label class="control-label" for="book_format_to">{{_('Convert to:')}}</label>
@ -101,7 +101,7 @@
</div>
<div class="form-group">
<label for="publisher">{{_('Publisher')}}</label>
<input type="text" class="form-control typeahead" name="publisher" id="publisher" value="{% if book.publishers|length > 0 %}{{book.publishers[0].name}}{% endif %}" disabled>
<input type="text" class="form-control typeahead" name="publisher" id="publisher" value="{% if book.publishers|length > 0 %}{{book.publishers[0].name}}{% endif %}">
</div>
<div class="form-group">
<label for="languages">{{_('Language')}}</label>
@ -174,7 +174,7 @@
</label>
</div>
<a href="#" id="get_meta" class="btn btn-default" data-toggle="modal" data-target="#metaModal">{{_('Get metadata')}}</a>
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
<button type="submit" id="submit" class="btn btn-default">{{_('Submit')}}</button>
<a href="{{ url_for('show_book', book_id=book.id) }}" class="btn btn-default">{{_('Back')}}</a>
</div>
</form>

View File

@ -207,12 +207,12 @@
<div class="col-sm-12">
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
{% if not origin %}
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a>
{% endif %}
{% if success %}
<a href="{{ url_for('login') }}" class="btn btn-default">{{_('Login')}}</a>
<a href="{{ url_for('login') }}" name="login" class="btn btn-default">{{_('Login')}}</a>
{% endif %}
</div>
</form>

View File

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

View File

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

View File

@ -14,7 +14,9 @@
{% endif %}
</div>
<div class="meta">
<p class="title">{{entry.title|shortentitle}}</p>
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>

View File

@ -27,7 +27,10 @@
href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}"
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
{% endif %}
<link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
<link rel="search"
href="{{url_for('feed_osd')}}"
type="application/opensearchdescription+xml"/>
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
<title>{{instance}}</title>
<author>
<name>{{instance}}</name>
@ -40,9 +43,16 @@
<title>{{entry.title}}</title>
<id>{{entry.uuid}}</id>
<updated>{{entry.atom_timestamp}}</updated>
<author>
<name>{{entry.authors[0].name}}</name>
</author>
{% if entry.authors.__len__() > 0 %}
<author>
<name>{{entry.authors[0].name}}</name>
</author>
{% endif %}
{% if entry.publishers.__len__() > 0 %}
<publisher>
<name>{{entry.publishers[0].name}}</name>
</publisher>
{% endif %}
<dcterms:language>{{entry.language}}</dcterms:language>
{% for tag in entry.tags %}
<category scheme="http://www.bisg.org/standards/bisac_subject/index.html"

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html class="http-error" lang="{{ g.user.locale }}">
<head>
<title>{{ instance }} | HTTP Error ({{ error_code }})</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Bootstrap -->
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen">
{% if g.user.get_theme == 1 %}
<link href="{{ url_for('static', filename='css/caliBlur-style.css') }}" rel="stylesheet" media="screen">
{% endif %}
</head>
<body>
<div class="container text-center">
<h1>{{ error_code }}</h1>
<h3>{{ error_name }}</h3>
<a href="{{url_for('index')}}" title="{{ _('Back to home') }}">{{_('Back to home')}}</a>
</div>
</body>
</html>

View File

@ -17,7 +17,9 @@
</a>
</div>
<div class="meta">
<p class="title">{{entry.title|shortentitle}}</p>
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
@ -60,7 +62,9 @@
</a>
</div>
<div class="meta">
<p class="title">{{entry.title|shortentitle}}</p>
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>

View File

@ -5,7 +5,10 @@
<link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link title="{{_('Search')}}" type="application/opensearchdescription+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
<link rel="search"
href="{{url_for('feed_osd')}}"
type="application/opensearchdescription+xml"/>
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
<title>{{instance}}</title>
<author>
<name>{{instance}}</name>
@ -39,7 +42,7 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Show Random Books')}}</content>
</entry>
{% if not current_user.is_anonymous %}
{% if not current_user.is_anonymous %}
<entry>
<title>{{_('Read Books')}}</title>
<link rel="subsection" href="{{url_for('feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -47,7 +50,7 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Read Books')}}</content>
</entry>
{% endif %}
{% endif %}
<entry>
<title>{{_('Unread Books')}}</title>
<link rel="subsection" href="{{url_for('feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -61,35 +64,42 @@
<id>{{url_for('feed_authorindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by Author')}}</content>
</entry>
<entry>
</entry>
<entry>
<title>{{_('Publishers')}}</title>
<link rel="subsection" href="{{url_for('feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_publisherindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by publisher')}}</content>
</entry>
<entry>
<title>{{_('Category list')}}</title>
<link rel="subsection" href="{{url_for('feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_categoryindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by category')}}</content>
</entry>
<entry>
</entry>
<entry>
<title>{{_('Series list')}}</title>
<link rel="subsection" href="{{url_for('feed_seriesindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_seriesindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by series')}}</content>
</entry>
<entry>
</entry>
<entry>
<title>{{_('Public Shelves')}}</title>
<link rel="subsection" href="{{url_for('feed_shelfindex', public="public")}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_shelfindex', public="public")}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books organized in public shelfs, visible to everyone')}}</content>
</entry>
{% if not current_user.is_anonymous %}
<entry>
</entry>
{% if not current_user.is_anonymous %}
<entry>
<title>{{_('Your Shelves')}}</title>
<link rel="subsection" href="{{url_for('feed_shelfindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_shelfindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_("User's own shelfs, only visible to the current user himself")}}</content>
</entry>
{% endif %}
</entry>
{% endif %}
</feed>

View File

@ -122,16 +122,16 @@
<li id="nav_new" {% if page == 'root' %}class="active"{% endif %}><a href="{{url_for('index')}}"><span class="glyphicon glyphicon-book"></span> {{_('Recently Added')}}</a></li>
{%endif%}
{% if g.user.show_sorted() %}
<li class="dropdown">
<li id="nav_sort" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-sort-by-attributes"></span>{{_('Sorted Books')}}
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li {% if page == 'newest' %}class="active"{% endif %}><a href="{{url_for('newest_books')}}">{{_('Sort By')}} {{_('Newest')}}</a></li>
<li {% if page == 'oldest' %}class="active"{% endif %}><a href="{{url_for('oldest_books')}}">{{_('Sort By')}} {{_('Oldest')}}</a></li>
<li {% if page == 'a-z' %}class="active"{% endif %}><a href="{{url_for('titles_ascending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Ascending')}})</a></li>
<li {% if page == 'z-a' %}class="active"{% endif %}><a href="{{url_for('titles_descending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Descending')}})</a></li>
<li id="nav_sort_old" {% if page == 'newest' %}class="active"{% endif %}><a href="{{url_for('newest_books')}}">{{_('Sort By')}} {{_('Newest')}}</a></li>
<li id="nav_sort_new" {% if page == 'oldest' %}class="active"{% endif %}><a href="{{url_for('oldest_books')}}">{{_('Sort By')}} {{_('Oldest')}}</a></li>
<li id="nav_sort_asc" {% if page == 'a-z' %}class="active"{% endif %}><a href="{{url_for('titles_ascending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Ascending')}})</a></li>
<li id="nav_sort_desc" {% if page == 'z-a' %}class="active"{% endif %}><a href="{{url_for('titles_descending')}}">{{_('Sort By')}} {{_('Title')}} ({{_('Descending')}})</a></li>
</ul>
</li>
{%endif%}
@ -139,13 +139,13 @@
<li id="nav_hot" {% if page == 'hot' %}class="active"{% endif %}><a href="{{url_for('hot_books')}}"><span class="glyphicon glyphicon-fire"></span>{{_('Hot Books')}}</a></li>
{%endif%}
{% if g.user.show_best_rated_books() %}
<li {% if page == 'rated' %}class="active"{% endif %}><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span>{{_('Best rated Books')}}</a></li>
<li id="nav_rated" {% if page == 'rated' %}class="active"{% endif %}><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span>{{_('Best rated Books')}}</a></li>
{%endif%}
{% if g.user.show_read_and_unread() %}
{% if not g.user.is_anonymous %}
<li {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('read_books')}}"><span class="glyphicon glyphicon-eye-open"></span>{{_('Read Books')}}</a></li>
<li id="nav_read" {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('read_books')}}"><span class="glyphicon glyphicon-eye-open"></span>{{_('Read Books')}}</a></li>
{%endif%}
<li {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('unread_books')}}"><span class="glyphicon glyphicon-eye-close"></span>{{_('Unread Books')}}</a></li>
<li id="nav_unread" {% if page == 'read' %}class="active"{% endif %}><a href="{{url_for('unread_books')}}"><span class="glyphicon glyphicon-eye-close"></span>{{_('Unread Books')}}</a></li>
{%endif%}
{% if g.user.show_random_books() %}
<li id="nav_rand" {% if page == 'discover' %}class="active"{% endif %}><a href="{{url_for('discover')}}"><span class="glyphicon glyphicon-random"></span>{{_('Discover')}}</a></li>
@ -159,17 +159,20 @@
{% if g.user.show_author() %}
<li id="nav_author" {% if page == 'author' %}class="active"{% endif %}><a href="{{url_for('author_list')}}"><span class="glyphicon glyphicon-user"></span>{{_('Authors')}}</a></li>
{%endif%}
{% if g.user.show_publisher() %}
<li id="nav_publisher" {% if page == 'publisher' %}class="active"{% endif %}><a href="{{url_for('publisher_list')}}"><span class="glyphicon glyphicon-text-size"></span>{{_('Publishers')}}</a></li>
{%endif%}
{% if g.user.filter_language() == 'all' and g.user.show_language() %}
<li id="nav_lang" {% if page == 'language' %}class="active"{% endif %}><a href="{{url_for('language_overview')}}"><span class="glyphicon glyphicon-flag"></span>{{_('Languages')}} </a></li>
{%endif%}
{% if g.user.is_authenticated or g.user.is_anonymous %}
<li class="nav-head hidden-xs">{{_('Public Shelves')}}</li>
{% for shelf in g.public_shelfes %}
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list"></span>{{shelf.name}}</a></li>
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list public_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
{% endfor %}
<li class="nav-head hidden-xs">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %}
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list"></span>{{shelf.name}}</a></li>
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
{% endfor %}
{% if not g.user.is_anonymous %}
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('create_shelf')}}">{{_('Create a Shelf')}}</a></li>

View File

@ -7,15 +7,11 @@
<label for="nickname">{{_('Username')}}</label>
<input type="text" class="form-control" id="nickname" name="nickname" placeholder="{{_('Choose a username')}}" required>
</div>
<!--div class="form-group required">
<label for="password">{{_('Password')}}</label>
<input type="password" class="form-control" id="password" name="password" placeholder="{{_('Choose a password')}}" required>
</div-->
<div class="form-group required">
<label for="email">{{_('E-mail address')}}</label>
<input type="email" class="form-control" id="email" name="email" placeholder="{{_('Your email address')}}" required>
</div>
<button type="submit" class="btn btn-primary">{{_('Register')}}</button>
<button type="submit" id="submit" class="btn btn-primary">{{_('Register')}}</button>
</form>
</div>
{% if error %}

View File

@ -5,7 +5,7 @@
<h2>{{_('No Results for:')}} {{searchterm}}</h2>
<p>{{_('Please try a different search')}}</p>
{% else %}
<h2>{{entries|length}} {{_('Results for:')}} {{searchterm}}</h2>
<h2>{{entries|length}} {{_('Results for:')}} {{searchterm}}</h2>
{% if g.user.is_authenticated %}
{% if g.user.shelf.all() or g.public_shelfes %}
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
@ -41,7 +41,9 @@
{% endif %}
</div>
<div class="meta">
<p class="title">{{entry.title|shortentitle}}</p>
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>

View File

@ -4,9 +4,9 @@
<h2>{{title}}</h2>
{% if g.user.is_authenticated %}
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
<div data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
<a href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a>
<a href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
<div id="delete_shelf" data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
<a id="edit_shelf" href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a>
<a id="order_shelf" href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
{% endif %}
{% endif %}
<div class="row">
@ -14,14 +14,18 @@
{% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
{% if entry.has_cover is defined %}
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" />
</a>
{% endif %}
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" />
{% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %}
</a>
</div>
<div class="meta">
<p class="title">{{entry.title|shortentitle}}</p>
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
@ -56,7 +60,7 @@
<div class="modal-body text-center">
<span>{{_('Shelf will be lost for everybody and forever!')}}</span>
<p></p>
<a href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('Ok')}}</a>
<a id="confirm" href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('Ok')}}</a>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
</div>
</div>

View File

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

View File

@ -11,7 +11,7 @@
{% if g.user.role_admin() %}
<th data-halign="right" data-align="right" data-field="user" data-sortable="true">{{_('User')}}</th>
{% endif %}
<th data-halign="right" data-align="right" data-field="type" data-sortable="true">{{_('Task')}}</th>
<th data-halign="right" data-align="right" data-field="taskMessage" data-sortable="true">{{_('Task')}}</th>
<th data-halign="right" data-align="right" data-field="status" data-sortable="true">{{_('Status')}}</th>
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Runtime')}}</th>

View File

@ -22,8 +22,6 @@
<input type="password" class="form-control" name="password" id="password" value="" autocomplete="off">
</div>
{% endif %}
{% endif %}
<div class="form-group">
<label for="kindle_mail">{{_('Kindle E-Mail')}}</label>
@ -91,6 +89,10 @@
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
<label for="show_author">{{_('Show author selection')}}</label>
</div>
<div class="form-group">
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
<label for="show_publisher">{{_('Show publisher selection')}}</label>
</div>
<div class="form-group">
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
<label for="show_read_and_unread">{{_('Show read and unread')}}</label>
@ -142,7 +144,7 @@
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
<div class="checkbox">
<label>
<input type="checkbox" name="delete"> {{_('Delete this user')}}
<input type="checkbox" id="delete" name="delete"> {{_('Delete this user')}}
</label>
</div>
{% endif %}
@ -151,8 +153,8 @@
<button type="submit" id="submit" class="btn btn-default">{{_('Submit')}}</button>
{% if not profile %}
<a href="{{ url_for('admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a>
</div>
{% endif %}
</div>
</form>
{% if downloads %}

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

18430
cps/translations/iso639.pickle Normal file

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

View File

@ -41,6 +41,7 @@ SIDEBAR_READ_AND_UNREAD = 256
SIDEBAR_RECENT = 512
SIDEBAR_SORTED = 1024
MATURE_CONTENT = 2048
SIDEBAR_PUBLISHER = 4096
DEFAULT_PASS = "admin123"
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
@ -136,6 +137,9 @@ class UserBase:
def show_author(self):
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
def show_publisher(self):
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
def show_best_rated_books(self):
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
@ -297,7 +301,7 @@ class Settings(Base):
config_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0)
config_default_role = Column(SmallInteger, default=0)
config_default_show = Column(SmallInteger, default=2047)
config_default_show = Column(SmallInteger, default=6143)
config_columns_to_ignore = Column(String)
config_use_google_drive = Column(Boolean)
config_google_drive_folder = Column(String)
@ -485,6 +489,10 @@ class Config:
return bool((self.config_default_show is not None) and
(self.config_default_show & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
def show_publisher(self):
return bool((self.config_default_show is not None) and
(self.config_default_show & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
def show_best_rated_books(self):
return bool((self.config_default_show is not None) and
(self.config_default_show & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
@ -740,7 +748,7 @@ def create_admin_user():
user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_DELETE_BOOKS + ROLE_PASSWD
user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \
SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED + SIDEBAR_READ_AND_UNREAD + SIDEBAR_RECENT + \
SIDEBAR_SORTED + MATURE_CONTENT
SIDEBAR_SORTED + MATURE_CONTENT + SIDEBAR_PUBLISHER
user.password = generate_password_hash(DEFAULT_PASS)

1022
cps/web.py

File diff suppressed because it is too large Load Diff

View File

@ -33,12 +33,12 @@ from email.utils import formatdate
from email.utils import make_msgid
chunksize = 8192
# task 'status' consts
STAT_WAITING = 0
STAT_FAIL = 1
STAT_STARTED = 2
STAT_FINISH_SUCCESS = 3
#taskType consts
TASK_EMAIL = 1
TASK_CONVERT = 2
TASK_UPLOAD = 3
@ -169,12 +169,12 @@ class WorkerThread(threading.Thread):
doLock.acquire()
if self.current != self.last:
doLock.release()
if self.queue[self.current]['typ'] == TASK_EMAIL:
self.send_raw_email()
if self.queue[self.current]['typ'] == TASK_CONVERT:
self.convert_any_format()
if self.queue[self.current]['typ'] == TASK_CONVERT_ANY:
self.convert_any_format()
if self.queue[self.current]['taskType'] == TASK_EMAIL:
self._send_raw_email()
if self.queue[self.current]['taskType'] == TASK_CONVERT:
self._convert_any_format()
if self.queue[self.current]['taskType'] == TASK_CONVERT_ANY:
self._convert_any_format()
# TASK_UPLOAD is handled implicitly
self.current += 1
else:
@ -190,7 +190,7 @@ class WorkerThread(threading.Thread):
else:
return "0 %"
def delete_completed_tasks(self):
def _delete_completed_tasks(self):
for index, task in reversed(list(enumerate(self.UIqueue))):
if task['progress'] == "100 %":
# delete tasks
@ -202,40 +202,55 @@ class WorkerThread(threading.Thread):
def get_taskstatus(self):
if self.current < len(self.queue):
if self.queue[self.current]['status'] == STAT_STARTED:
if self.queue[self.current]['typ'] == TASK_EMAIL:
if self.UIqueue[self.current]['stat'] == STAT_STARTED:
if self.queue[self.current]['taskType'] == TASK_EMAIL:
self.UIqueue[self.current]['progress'] = self.get_send_status()
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
datetime.now() - self.queue[self.current]['starttime'])
return self.UIqueue
def convert_any_format(self):
def _convert_any_format(self):
# convert book, and upload in case of google drive
self.queue[self.current]['status'] = STAT_STARTED
self.UIqueue[self.current]['status'] = _('Started')
self.UIqueue[self.current]['stat'] = STAT_STARTED
self.queue[self.current]['starttime'] = datetime.now()
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
curr_task = self.queue[self.current]['typ']
filename = self.convert_ebook_format()
curr_task = self.queue[self.current]['taskType']
filename = self._convert_ebook_format()
if filename:
if web.ub.config.config_use_google_drive:
gd.updateGdriveCalibreFromLocal()
if curr_task == TASK_CONVERT:
self.add_email(_(u'Send to Kindle'), self.queue[self.current]['path'], filename,
self.queue[self.current]['settings'], self.queue[self.current]['kindle'],
self.UIqueue[self.current]['user'], _(u"E-mail: %s" % self.queue[self.current]['title']))
self.add_email(self.queue[self.current]['settings']['subject'], self.queue[self.current]['path'],
filename, self.queue[self.current]['settings'], self.queue[self.current]['kindle'],
self.UIqueue[self.current]['user'], self.queue[self.current]['title'],
self.queue[self.current]['settings']['body'])
def convert_ebook_format(self):
def _convert_ebook_format(self):
error_message = None
file_path = self.queue[self.current]['file_path']
bookid = self.queue[self.current]['bookid']
format_old_ext = u'.' + self.queue[self.current]['settings']['old_book_format'].lower()
format_new_ext = u'.' + self.queue[self.current]['settings']['new_book_format'].lower()
# check to see if destination format already exists -
# if it does - mark the conversion task as complete and return a success
# this will allow send to kindle workflow to continue to work
if os.path.isfile(file_path + format_new_ext):
web.app.logger.info("Book id %d already converted to %s", bookid, format_new_ext)
cur_book = web.db.session.query(web.db.Books).filter(web.db.Books.id == bookid).first()
self.queue[self.current]['path'] = file_path
self.queue[self.current]['title'] = cur_book.title
self._handleSuccess()
return file_path + format_new_ext
else:
web.app.logger.info("Book id %d - target format of %s does not exist. Moving forward with convert.", bookid, format_new_ext)
# check if converter-executable is existing
if not os.path.exists(web.ub.config.config_converterpath):
self._handleError(_(u"Convertertool %(converter)s not found", converter=web.ub.config.config_converterpath))
# ToDo Text is not translated
self._handleError(u"Convertertool %s not found" % web.ub.config.config_converterpath)
return
try:
# check which converter to use kindlegen is "1"
if format_old_ext == '.epub' and format_new_ext == '.mobi':
@ -269,7 +284,7 @@ class WorkerThread(threading.Thread):
p = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
except OSError as e:
self._handleError(_(u"Ebook-converter failed: %s" % e))
self._handleError(_(u"Ebook-converter failed: %(error)s", error=e))
return
if web.ub.config.config_ebookconverter == 1:
@ -313,11 +328,7 @@ class WorkerThread(threading.Thread):
self.queue[self.current]['title'] = cur_book.title
if web.ub.config.config_use_google_drive:
os.remove(file_path + format_old_ext)
self.queue[self.current]['status'] = STAT_FINISH_SUCCESS
self.UIqueue[self.current]['status'] = _('Finished')
self.UIqueue[self.current]['progress'] = "100 %"
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
datetime.now() - self.queue[self.current]['starttime'])
self._handleSuccess()
return file_path + format_new_ext
else:
error_message = format_new_ext.upper() + ' format not found on disk'
@ -328,63 +339,62 @@ class WorkerThread(threading.Thread):
return
def add_convert(self, file_path, bookid, user_name, typ, settings, kindle_mail=None):
def add_convert(self, file_path, bookid, user_name, taskMessage, settings, kindle_mail=None):
addLock = threading.Lock()
addLock.acquire()
if self.last >= 20:
self.delete_completed_tasks()
self._delete_completed_tasks()
# progress, runtime, and status = 0
self.id += 1
task = TASK_CONVERT_ANY
if kindle_mail:
task = TASK_CONVERT
self.queue.append({'file_path':file_path, 'bookid':bookid, 'starttime': 0, 'kindle': kindle_mail,
'status': STAT_WAITING, 'typ': task, 'settings':settings})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'type': typ,
'runtime': '0 s', 'status': _('Waiting'),'id': self.id } )
'taskType': task, 'settings':settings})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': task } )
self.last=len(self.queue)
addLock.release()
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, typ,
text=_(u'This e-mail has been sent via Calibre-Web.')):
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, taskMessage,
text):
# if more than 20 entries in the list, clean the list
addLock = threading.Lock()
addLock.acquire()
if self.last >= 20:
self.delete_completed_tasks()
self._delete_completed_tasks()
# progress, runtime, and status = 0
self.id += 1
self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath,
'settings':settings, 'recipent':recipient, 'starttime': 0,
'status': STAT_WAITING, 'typ': TASK_EMAIL, 'text':text})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'type': typ,
'runtime': '0 s', 'status': _('Waiting'),'id': self.id })
'taskType': TASK_EMAIL, 'text':text})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': TASK_EMAIL })
self.last=len(self.queue)
addLock.release()
def add_upload(self, user_name, typ):
def add_upload(self, user_name, taskMessage):
# if more than 20 entries in the list, clean the list
addLock = threading.Lock()
addLock.acquire()
if self.last >= 20:
self.delete_completed_tasks()
self._delete_completed_tasks()
# progress=100%, runtime=0, and status finished
self.id += 1
self.queue.append({'starttime': datetime.now(), 'status': STAT_FINISH_SUCCESS, 'typ': TASK_UPLOAD})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': "100 %", 'type': typ,
'runtime': '0 s', 'status': _('Finished'),'id': self.id })
self.queue.append({'starttime': datetime.now(), 'taskType': TASK_UPLOAD})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': "100 %", 'taskMess': taskMessage,
'runtime': '0 s', 'stat': STAT_FINISH_SUCCESS,'id': self.id, 'taskType': TASK_UPLOAD})
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
self.last=len(self.queue)
addLock.release()
def send_raw_email(self):
def _send_raw_email(self):
self.queue[self.current]['starttime'] = datetime.now()
self.UIqueue[self.current]['formStarttime'] = self.queue[self.current]['starttime']
self.queue[self.current]['status'] = STAT_STARTED
self.UIqueue[self.current]['status'] = _('Started')
# self.queue[self.current]['status'] = STAT_STARTED
self.UIqueue[self.current]['stat'] = STAT_STARTED
obj=self.queue[self.current]
# create MIME message
msg = MIMEMultipart()
@ -434,26 +444,23 @@ class WorkerThread(threading.Thread):
self.asyncSMTP.login(str(obj['settings']["mail_login"]), str(obj['settings']["mail_password"]))
self.asyncSMTP.sendmail(obj['settings']["mail_from"], obj['recipent'], msg)
self.asyncSMTP.quit()
self.queue[self.current]['status'] = STAT_FINISH_SUCCESS
self.UIqueue[self.current]['status'] = _('Finished')
self.UIqueue[self.current]['progress'] = "100 %"
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
datetime.now() - self.queue[self.current]['starttime'])
self._handleSuccess()
sys.stderr = org_stderr
except (MemoryError) as e:
self._handleError(u'Error sending email: ' + e.message)
return None
except (smtplib.SMTPException) as e:
self._handleError(u'Error sending email: ' + e.smtp_error.replace("\n",'. '))
if hasattr(e, "smtp_error"):
text = e.smtp_error.replace("\n",'. ')
else:
text = ''
self._handleError(u'Error sending email: ' + text)
return None
except (socket.error) as e:
self._handleError(u'Error sending email: ' + e.strerror)
return None
def _formatRuntime(self, runtime):
self.UIqueue[self.current]['rt'] = runtime.total_seconds()
val = re.split('\:|\.', str(runtime))[0:3]
@ -465,18 +472,25 @@ class WorkerThread(threading.Thread):
if retVal == ' s':
retVal = '0 s'
return retVal
def _handleError(self, error_message):
web.app.logger.error(error_message)
self.queue[self.current]['status'] = STAT_FAIL
self.UIqueue[self.current]['status'] = _('Failed')
# self.queue[self.current]['status'] = STAT_FAIL
self.UIqueue[self.current]['stat'] = STAT_FAIL
self.UIqueue[self.current]['progress'] = "100 %"
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
datetime.now() - self.queue[self.current]['starttime'])
self.UIqueue[self.current]['message'] = error_message
def _handleSuccess(self):
# self.queue[self.current]['status'] = STAT_FINISH_SUCCESS
self.UIqueue[self.current]['stat'] = STAT_FINISH_SUCCESS
self.UIqueue[self.current]['progress'] = "100 %"
self.UIqueue[self.current]['runtime'] = self._formatRuntime(
datetime.now() - self.queue[self.current]['starttime'])
# Enable logging of smtp lib debug output
class StderrLogger(object):
buffer = ''
@ -491,3 +505,4 @@ class StderrLogger(object):
self.buffer = ''
else:
self.buffer += message

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -12,7 +12,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup
- User management with fine grained per-user permissions
- Admin interface
- User Interface in dutch, english, french, german, italian, japanese, khmer, polish, russian, simplified chinese, spanish
- User Interface in dutch, english, french, german, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish
- OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language
- Create custom book collection (shelves)
@ -30,7 +30,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
## Quick start
1. Install dependencies by running `pip install --target vendor -r requirements.txt`.
1. Install dependencies by running `pip install --target vendor -r requirements.txt`.
2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
@ -41,6 +41,9 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
*Username:* admin
*Password:* admin123
**Issues with Ubuntu:**
Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
## Runtime Configuration Options
The configuration can be changed as admin in the admin panel under "Configuration"
@ -48,14 +51,14 @@ The configuration can be changed as admin in the admin panel under "Configuratio
Server Port:
Changes the port Calibre-Web is listening, changes take effect after pressing submit button
Enable public registration:
Enable public registration:
Tick to enable public user registration.
Enable anonymous browsing:
Enable anonymous browsing:
Tick to allow not logged in users to browse the catalog, anonymous user permissions can be set as admin ("Guest" user)
Enable uploading:
Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick library to be installed.
Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick library to be installed.
Enable remote login ("magic link"):
Tick to enable remote login, i.e. a link that allows user to log in via a different device.
@ -83,7 +86,7 @@ Once a project has been created, we need to create a client ID and a client secr
5. Select Web Application and then next
6. Give the Credentials a name and enter your callback, which will be CALIBRE_WEB_URL/gdrive/callback
7. Click save
8. Download json file and place it in `calibre-web` directory, with the name `client_secrets.json`
8. Download json file and place it in `calibre-web` directory, with the name `client_secrets.json`
The Drive API should now be setup and ready to use, so we need to integrate it into Calibre-Web. This is done as below: -
@ -103,7 +106,7 @@ Additionally the public adress your server uses (e.g.https://example.com) has to
9. Open config page
10. Click enable watch of metadata.db
11. Note that this expires after a week, so will need to be manually refresh
11. Note that this expires after a week, so will need to be manually refresh
## Docker images
@ -160,7 +163,7 @@ Listen 443
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
SSLCertificateFile "C:\Apache24\conf\ssl\test.crt"
SSLCertificateKeyFile "C:\Apache24\conf\ssl\test.key"
<Location "/calibre-web" >
RequestHeader set X-SCRIPT-NAME /calibre-web
RequestHeader set X-SCHEME https
@ -172,8 +175,8 @@ Listen 443
## (Optional) SSL Configuration
For configuration of calibre-web as SSL Server go to the Config page in the Admin section. Enter the certfile- and keyfile-location, optionally change port to 443 and press submit.
Afterwards the server can only be accessed via SSL. In case of a misconfiguration (wrong/invalid files) both files can be overridden via command line options
For configuration of calibre-web as SSL Server go to the Config page in the Admin section. Enter the certfile- and keyfile-location, optionally change port to 443 and press submit.
Afterwards the server can only be accessed via SSL. In case of a misconfiguration (wrong/invalid files) both files can be overridden via command line options
-c [certfile location] -k [keyfile location]
By using "" as file locations the server runs as non SSL server again. The correct file path can than be entered on the Config page. After the next restart without command line options the changed file paths are applied.
@ -206,7 +209,7 @@ enables the service.
Starting the script with `-h` lists all supported command line options
Currently supported are 2 options, which are both useful for running multiple instances of Calibre-Web
`"-p path"` allows to specify the location of the settings database
`"-g path"` allows to specify the location of the google-drive database
`"-c path"` allows to specify the location of SSL certfile, works only in combination with keyfile
`"-k path"` allows to specify the location of SSL keyfile, works only in combination with certfile
`"-p path"` allows to specify the location of the settings database
`"-g path"` allows to specify the location of the google-drive database
`"-c path"` allows to specify the location of SSL certfile, works only in combination with keyfile
`"-k path"` allows to specify the location of SSL keyfile, works only in combination with certfile

File diff suppressed because it is too large Load Diff

21
test/css/runner.css Normal file
View File

@ -0,0 +1,21 @@
.hiddenRow {
display: none;
}
.bg-grey {
background-color: rgba(0, 0, 0, 0.03);
}
.table-curved {
border-radius: 20px;
}
.buttons, .report-description {
margin: 5px;
padding: 5px;
}
.piechart{
text-align: center;
}

189
test/js/runner.js Normal file
View File

@ -0,0 +1,189 @@
output_list = Array();
/* Level - 0: Summary; 1: Failed; 2: All; 3: Skipped */
function showCase(level) {
table_rows = document.getElementsByTagName("tr");
for (var i = 0; i < table_rows.length; i++) {
row = table_rows[i];
id = row.id;
if (id.substr(0,2) == 'ft') {
if (level < 1 || level == 3) {
row.classList.add('hiddenRow');
}
else {
row.classList.remove('hiddenRow');
}
}
if (id.substr(0,2) == 'pt') {
if (level > 1 && level != 3) {
row.classList.remove('hiddenRow');
}
else {
row.classList.add('hiddenRow');
}
}
if (id.substr(0,2) == 'st') {
if (level >=2) {
row.classList.remove('hiddenRow');
}
else {
row.classList.add('hiddenRow');
}
}
}
}
function showClassDetail(class_id, count) {
var testcases_list = Array(count);
var all_hidden = true;
for (var i = 0; i < count; i++) {
testcase_postfix_id = 't' + class_id.substr(1) + '.' + (i+1);
testcase_id = 'f' + testcase_postfix_id;
testcase = document.getElementById(testcase_id);
if (!testcase) {
testcase_id = 'p' + testcase_postfix_id;
testcase = document.getElementById(testcase_id);
}
if (!testcase) {
testcase_id = 's' + testcase_postfix_id;
testcase = document.getElementById(testcase_id);
}
testcases_list[i] = testcase;
if (testcase.classList.contains('hiddenRow')) {
all_hidden = false;
}
}
for (var i = 0; i < count; i++) {
testcase = testcases_list[i];
if (!all_hidden) {
testcase.classList.remove('hiddenRow');
}
else {
testcase.classList.add('hiddenRow');
}
}
}
function showTestDetail(div_id){
var details_div = document.getElementById(div_id)
var displayState = details_div.style.display
// alert(displayState)
if (displayState != 'block' ) {
displayState = 'block'
details_div.style.display = 'block'
}
else {
details_div.style.display = 'none'
}
}
function html_escape(s) {
s = s.replace(/&/g,'&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 = "";
}