1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-06-18 19:29:57 +00:00

Merge pull request #1 from apollo1220/feature/custom-pages

Feature/custom pages
This commit is contained in:
apollo1220 2024-03-21 13:28:43 -04:00 committed by GitHub
commit f20b0f3064
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 676 additions and 291 deletions

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ gdrive_credentials
client_secrets.json client_secrets.json
gmail.json gmail.json
/.key /.key
pages/

View File

@ -1705,7 +1705,7 @@ def _db_configuration_update_helper():
return _db_configuration_result('{}'.format(ex), gdrive_error) return _db_configuration_result('{}'.format(ex), gdrive_error)
if db_change or not db_valid or not config.db_configured \ if db_change or not db_valid or not config.db_configured \
or config.config_calibre_dir != to_save["config_calibre_dir"]: or config.config_calibre_dir != to_save["config_calibre_dir"]:
if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']: if not os.path.exists(metadata_db) or not to_save['config_calibre_dir']:
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error) return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error)
else: else:
@ -1728,6 +1728,9 @@ def _db_configuration_update_helper():
calibre_db.update_config(config) calibre_db.update_config(config)
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK): if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_("DB is not Writeable"), category="warning") flash(_("DB is not Writeable"), category="warning")
_config_string(to_save, "config_calibre_split_dir")
config.config_calibre_split = to_save.get('config_calibre_split', 0) == "on"
calibre_db.update_config(config)
config.save() config.save()
return _db_configuration_result(None, gdrive_error) return _db_configuration_result(None, gdrive_error)

View File

@ -69,6 +69,8 @@ class _Settings(_Base):
config_calibre_dir = Column(String) config_calibre_dir = Column(String)
config_calibre_uuid = Column(String) config_calibre_uuid = Column(String)
config_calibre_split = Column(Boolean, default=False)
config_calibre_split_dir = Column(String)
config_port = Column(Integer, default=constants.DEFAULT_PORT) config_port = Column(Integer, default=constants.DEFAULT_PORT)
config_external_port = Column(Integer, default=constants.DEFAULT_PORT) config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
config_certfile = Column(String) config_certfile = Column(String)
@ -389,6 +391,9 @@ class ConfigSQL(object):
self.db_configured = False self.db_configured = False
self.save() self.save()
def get_book_path(self):
return self.config_calibre_split_dir if self.config_calibre_split_dir else self.config_calibre_dir
def store_calibre_uuid(self, calibre_db, Library_table): def store_calibre_uuid(self, calibre_db, Library_table):
try: try:
calibre_uuid = calibre_db.session.query(Library_table).one_or_none() calibre_uuid = calibre_db.session.query(Library_table).one_or_none()

View File

@ -135,7 +135,7 @@ def edit_book(book_id):
edited_books_id = book.id edited_books_id = book.id
modify_date = True modify_date = True
title_author_error = helper.update_dir_structure(edited_books_id, title_author_error = helper.update_dir_structure(edited_books_id,
config.config_calibre_dir, config.get_book_path(),
input_authors[0], input_authors[0],
renamed_author=renamed) renamed_author=renamed)
if title_author_error: if title_author_error:
@ -280,7 +280,7 @@ def upload():
meta.extension.lower()) meta.extension.lower())
else: else:
error = helper.update_dir_structure(book_id, error = helper.update_dir_structure(book_id,
config.config_calibre_dir, config.get_book_path(),
input_authors[0], input_authors[0],
meta.file_path, meta.file_path,
title_dir + meta.extension.lower(), title_dir + meta.extension.lower(),
@ -330,7 +330,7 @@ def convert_bookformat(book_id):
return redirect(url_for('edit-book.show_edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), rtn = helper.convert_book_format(book_id, config.get_book_path(), book_format_from.upper(),
book_format_to.upper(), current_user.name) book_format_to.upper(), current_user.name)
if rtn is None: if rtn is None:
@ -400,7 +400,7 @@ def edit_list_book(param):
elif param == 'title': elif param == 'title':
sort_param = book.sort sort_param = book.sort
if handle_title_on_edit(book, vals.get('value', "")): if handle_title_on_edit(book, vals.get('value', "")):
rename_error = helper.update_dir_structure(book.id, config.config_calibre_dir) rename_error = helper.update_dir_structure(book.id, config.get_book_path())
if not rename_error: if not rename_error:
ret = Response(json.dumps({'success': True, 'newValue': book.title}), ret = Response(json.dumps({'success': True, 'newValue': book.title}),
mimetype='application/json') mimetype='application/json')
@ -418,7 +418,7 @@ def edit_list_book(param):
mimetype='application/json') mimetype='application/json')
elif param == 'authors': elif param == 'authors':
input_authors, __, renamed = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") input_authors, __, renamed = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
rename_error = helper.update_dir_structure(book.id, config.config_calibre_dir, input_authors[0], rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0],
renamed_author=renamed) renamed_author=renamed)
if not rename_error: if not rename_error:
ret = Response(json.dumps({ ret = Response(json.dumps({
@ -522,10 +522,10 @@ def merge_list_book():
for element in from_book.data: for element in from_book.data:
if element.format not in to_file: if element.format not in to_file:
# create new data entry with: book_id, book_format, uncompressed_size, name # create new data entry with: book_id, book_format, uncompressed_size, name
filepath_new = os.path.normpath(os.path.join(config.config_calibre_dir, filepath_new = os.path.normpath(os.path.join(config.get_book_path(),
to_book.path, to_book.path,
to_name + "." + element.format.lower())) to_name + "." + element.format.lower()))
filepath_old = os.path.normpath(os.path.join(config.config_calibre_dir, filepath_old = os.path.normpath(os.path.join(config.get_book_path(),
from_book.path, from_book.path,
element.name + "." + element.format.lower())) element.name + "." + element.format.lower()))
copyfile(filepath_old, filepath_new) copyfile(filepath_old, filepath_new)
@ -565,7 +565,7 @@ def table_xchange_author_title():
if edited_books_id: if edited_books_id:
# toDo: Handle error # toDo: Handle error
edit_error = helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], edit_error = helper.update_dir_structure(edited_books_id, config.get_book_path(), input_authors[0],
renamed_author=renamed) renamed_author=renamed)
if modify_date: if modify_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
@ -762,7 +762,7 @@ def move_coverfile(meta, db_book):
cover_file = meta.cover cover_file = meta.cover
else: else:
cover_file = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg') cover_file = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg')
new_cover_path = os.path.join(config.config_calibre_dir, db_book.path) new_cover_path = os.path.join(config.get_book_path(), db_book.path)
try: try:
os.makedirs(new_cover_path, exist_ok=True) os.makedirs(new_cover_path, exist_ok=True)
copyfile(cover_file, os.path.join(new_cover_path, "cover.jpg")) copyfile(cover_file, os.path.join(new_cover_path, "cover.jpg"))
@ -848,7 +848,7 @@ def delete_book_from_table(book_id, book_format, json_response):
book = calibre_db.get_book(book_id) book = calibre_db.get_book(book_id)
if book: if book:
try: try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
if not result: if not result:
if json_response: if json_response:
return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id), return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
@ -1184,7 +1184,7 @@ def upload_single_file(file_request, book, book_id):
return False return False
file_name = book.path.rsplit('/', 1)[-1] file_name = book.path.rsplit('/', 1)[-1]
filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) filepath = os.path.normpath(os.path.join(config.get_book_path(), book.path))
saved_filename = os.path.join(filepath, file_name + '.' + file_ext) saved_filename = os.path.join(filepath, file_name + '.' + file_ext)
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file # check if file path exists, otherwise create it, copy file to calibre path and delete temp file

108
cps/editpage.py Normal file
View File

@ -0,0 +1,108 @@
import os
import flask
from flask import Blueprint, Flask, abort, request
from functools import wraps
from pathlib import Path
from flask_login import current_user, login_required
from werkzeug.exceptions import NotFound
from .render_template import render_title_template
from . import logger, config, ub
from .constants import CONFIG_DIR as _CONFIG_DIR
log = logger.create()
editpage = Blueprint('editpage', __name__)
def edit_required(f):
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_edit() or current_user.role_admin():
return f(*args, **kwargs)
abort(403)
return inner
def _get_checkbox(dictionary, field, default):
new_value = dictionary.get(field, default)
convertor = lambda y: y == "on"
new_value = convertor(new_value)
return new_value
@editpage.route("/admin/page/<string:file>", methods=["GET", "POST"])
@login_required
@edit_required
def edit_page(file):
doc = ""
title = ""
name = ""
icon = "file"
is_enabled = True
order = 0
position = "0"
page = ub.session.query(ub.Page).filter(ub.Page.id == file).first()
try:
title = page.title
name = page.name
icon = page.icon
is_enabled = page.is_enabled
order = page.order
position = page.position
except AttributeError:
if file != "new":
abort(404)
if request.method == "POST":
to_save = request.form.to_dict()
title = to_save.get("title", "").strip()
name = to_save.get("name", "").strip()
icon = to_save.get("icon", "").strip()
position = to_save.get("position", "").strip()
order = int(to_save.get("order", 0))
content = to_save.get("content", "").strip()
is_enabled = _get_checkbox(to_save, "is_enabled", True)
if page:
page.title = title
page.name = name
page.icon = icon
page.is_enabled = is_enabled
page.order = order
page.position = position
ub.session_commit("Page edited {}".format(file))
else:
new_page = ub.Page(title=title, name=name, icon=icon, is_enabled=is_enabled, order=order, position=position)
ub.session.add(new_page)
ub.session_commit("Page added {}".format(file))
if (file == "new"):
file = str(new_page.id)
dir_config_path = os.path.join(_CONFIG_DIR, 'pages')
file_name = Path(name + '.md')
file_path = dir_config_path / file_name
os.makedirs(dir_config_path, exist_ok=True)
try:
with open(file_path, 'w') as f:
f.write(content)
f.close()
except Exception as ex:
log.error(ex)
if file != "new":
try:
dir_config_path = Path(_CONFIG_DIR) / 'pages'
file_path = dir_config_path / f"{name}.md"
with open(file_path, 'r') as f:
doc = f.read()
except NotFound:
log.error("'%s' was accessed but file doesn't exists." % file)
else:
doc = "## New file\n\nInformation"
return render_title_template("edit_page.html", title=title, name=name, icon=icon, is_enabled=is_enabled, order=order, position=position, content=doc, file=file)

View File

@ -48,7 +48,8 @@ def get_epub_layout(book, book_data):
'n': 'urn:oasis:names:tc:opendocument:xmlns:container', 'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
'pkg': 'http://www.idpf.org/2007/opf', 'pkg': 'http://www.idpf.org/2007/opf',
} }
file_path = os.path.normpath(os.path.join(config.config_calibre_dir, book.path, book_data.name + "." + book_data.format.lower())) file_path = os.path.normpath(os.path.join(config.get_book_path(),
book.path, book_data.name + "." + book_data.format.lower()))
try: try:
epubZip = zipfile.ZipFile(file_path) epubZip = zipfile.ZipFile(file_path)

View File

@ -781,7 +781,7 @@ def get_book_cover_internal(book, resolution=None):
# Send the book cover from the Calibre directory # Send the book cover from the Calibre directory
else: else:
cover_file_path = os.path.join(config.config_calibre_dir, book.path) cover_file_path = os.path.join(config.get_book_path(), book.path)
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
return send_from_directory(cover_file_path, "cover.jpg") return send_from_directory(cover_file_path, "cover.jpg")
else: else:
@ -934,7 +934,7 @@ def save_cover(img, book_path):
else: else:
return False, message return False, message
else: else:
return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img) return save_cover_from_filestorage(os.path.join(config.get_book_path(), book_path), "cover.jpg", img)
def do_download_file(book, book_format, client, data, headers): def do_download_file(book, book_format, client, data, headers):
@ -947,7 +947,7 @@ def do_download_file(book, book_format, client, data, headers):
else: else:
abort(404) abort(404)
else: else:
filename = os.path.join(config.config_calibre_dir, book.path) filename = os.path.join(config.get_book_path(), book.path)
if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)): if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)):
# ToDo: improve error handling # ToDo: improve error handling
log.error('File not found: %s', os.path.join(filename, data.name + "." + book_format)) log.error('File not found: %s', os.path.join(filename, data.name + "." + book_format))

View File

@ -205,7 +205,7 @@ def HandleSyncRequest():
for book in books: for book in books:
formats = [data.format for data in book.Books.data] formats = [data.format for data in book.Books.data]
if 'KEPUB' not in formats and config.config_kepubifypath and 'EPUB' in formats: if 'KEPUB' not in formats and config.config_kepubifypath and 'EPUB' in formats:
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) helper.convert_book_format(book.Books.id, config.get_book_path(), 'EPUB', 'KEPUB', current_user.name)
kobo_reading_state = get_or_create_reading_state(book.Books.id) kobo_reading_state = get_or_create_reading_state(book.Books.id)
entitlement = { entitlement = {

28
cps/listpages.py Normal file
View File

@ -0,0 +1,28 @@
import flask
import json
from flask import Blueprint, jsonify, make_response,abort
from flask_login import current_user, login_required
from functools import wraps
from flask_babel import gettext as _
from .render_template import render_title_template
from . import ub, db
listpages = Blueprint('listpages', __name__)
def edit_required(f):
@wraps(f)
def inner(*args, **kwargs):
if current_user.role_edit() or current_user.role_admin():
return f(*args, **kwargs)
abort(403)
return inner
@listpages.route("/admin/pages/", methods=["GET"])
@login_required
@edit_required
def show_list():
pages = ub.session.query(ub.Page).order_by(ub.Page.position).order_by(ub.Page.order).all()
return render_title_template('list_pages.html', title=_("Pages List"), page="book_table", pages=pages)

View File

@ -36,6 +36,9 @@ def main():
from .gdrive import gdrive from .gdrive import gdrive
from .editbooks import editbook from .editbooks import editbook
from .about import about from .about import about
from .page import page
from .listpages import listpages
from .editpage import editpage
from .search import search from .search import search
from .search_metadata import meta from .search_metadata import meta
from .shelf import shelf from .shelf import shelf
@ -65,6 +68,9 @@ def main():
limiter.limit("3/minute",key_func=request_username)(opds) limiter.limit("3/minute",key_func=request_username)(opds)
app.register_blueprint(jinjia) app.register_blueprint(jinjia)
app.register_blueprint(about) app.register_blueprint(about)
app.register_blueprint(page)
app.register_blueprint(listpages)
app.register_blueprint(editpage)
app.register_blueprint(shelf) app.register_blueprint(shelf)
app.register_blueprint(admi) app.register_blueprint(admi)
app.register_blueprint(remotelogin) app.register_blueprint(remotelogin)

View File

@ -502,7 +502,7 @@ def render_element_index(database_column, linked_table, folder):
entries = entries.join(linked_table).join(db.Books) entries = entries.join(linked_table).join(db.Books)
entries = entries.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(database_column, 1, 1))).all() entries = entries.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(database_column, 1, 1))).all()
elements = [] elements = []
if off == 0: if off == 0 and entries:
elements.append({'id': "00", 'name': _("All")}) elements.append({'id': "00", 'name': _("All")})
shift = 1 shift = 1
for entry in entries[ for entry in entries[

38
cps/page.py Normal file
View File

@ -0,0 +1,38 @@
import os
import flask
import markdown
from flask import abort
from pathlib import Path
from flask_babel import gettext as _
from werkzeug.exceptions import NotFound
from . import logger, config, ub
from .render_template import render_title_template
from .constants import CONFIG_DIR as _CONFIG_DIR
page = flask.Blueprint('page', __name__)
log = logger.create()
@page.route('/page/<string:file>', methods=['GET'])
def get_page(file):
page = ub.session.query(ub.Page)\
.filter(ub.Page.name == file)\
.filter(ub.Page.is_enabled)\
.first()
if not page:
log.error(f"'{file}' was accessed but is not enabled or it's not in database.")
abort(404)
try:
dir_config_path = Path(_CONFIG_DIR) / 'pages'
file_path = dir_config_path / f"{file}.md"
with open(file_path, 'r') as f:
temp_md = f.read()
body = markdown.markdown(temp_md)
return render_title_template('page.html', body=body, title=page.title, page=page.name)
except NotFound:
log.error("'%s' was accessed but file doesn't exists." % file)
abort(404)

View File

@ -104,14 +104,24 @@ def get_sidebar_config(kwargs=None):
g.shelves_access = ub.session.query(ub.Shelf).filter( g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all() or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
return sidebar, simple top_pages = ub.session.query(ub.Page)\
.filter(ub.Page.position == "1")\
.filter(ub.Page.is_enabled)\
.order_by(ub.Page.order)
bottom_pages = ub.session.query(ub.Page)\
.filter(ub.Page.position == "0")\
.filter(ub.Page.is_enabled)\
.order_by(ub.Page.order)
return sidebar, simple, top_pages, bottom_pages
# Returns the template for rendering and includes the instance name # Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs): def render_title_template(*args, **kwargs):
sidebar, simple = get_sidebar_config(kwargs) sidebar, simple, top_pages, bottom_pages = get_sidebar_config(kwargs)
try: try:
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple, return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple,
top_pages=top_pages, bottom_pages=bottom_pages,
accept=constants.EXTENSIONS_UPLOAD, accept=constants.EXTENSIONS_UPLOAD,
*args, **kwargs) *args, **kwargs)
except PermissionError: except PermissionError:

View File

@ -179,8 +179,9 @@ kthoom.ImageFile = function(file) {
}; };
function updateDirectionButtons(){ function updateDirectionButtons(){
var left, right = 1; var left = 1;
if (currentImage == 0 ) { var right = 1;
if (currentImage <= 0 ) {
if (settings.direction === 0) { if (settings.direction === 0) {
left = 0; left = 0;
} else { } else {

6
cps/tasks/convert.py Executable file → Normal file
View File

@ -62,11 +62,11 @@ class TaskConvert(CalibreTask):
df = gdriveutils.getFileFromEbooksFolder(cur_book.path, df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
data.name + "." + self.settings['old_book_format'].lower()) data.name + "." + self.settings['old_book_format'].lower())
if df: if df:
datafile = os.path.join(config.config_calibre_dir, datafile = os.path.join(config.get_book_path(),
cur_book.path, cur_book.path,
data.name + "." + self.settings['old_book_format'].lower()) data.name + "." + self.settings['old_book_format'].lower())
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)): if not os.path.exists(os.path.join(config.get_book_path(), cur_book.path)):
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path)) os.makedirs(os.path.join(config.get_book_path(), cur_book.path))
df.GetContentFile(datafile) df.GetContentFile(datafile)
worker_db.session.close() worker_db.session.close()
else: else:

2
cps/tasks/mail.py Executable file → Normal file
View File

@ -239,7 +239,7 @@ class TaskEmail(CalibreTask):
@classmethod @classmethod
def _get_attachment(cls, book_path, filename): def _get_attachment(cls, book_path, filename):
"""Get file as MIMEBase message""" """Get file as MIMEBase message"""
calibre_path = config.config_calibre_dir calibre_path = config.get_book_path()
if config.config_use_google_drive: if config.config_use_google_drive:
df = gdriveutils.getFileFromEbooksFolder(book_path, filename) df = gdriveutils.getFileFromEbooksFolder(book_path, filename)
if df: if df:

View File

@ -114,7 +114,7 @@ class TaskBackupMetadata(CalibreTask):
True) True)
else: else:
# ToDo: Handle book folder not found or not readable # ToDo: Handle book folder not found or not readable
book_metadata_filepath = os.path.join(config.config_calibre_dir, book.path, 'metadata.opf') book_metadata_filepath = os.path.join(config.get_book_path(), book.path, 'metadata.opf')
# prepare finalize everything and output # prepare finalize everything and output
doc = etree.ElementTree(package) doc = etree.ElementTree(package)
try: try:

View File

@ -209,7 +209,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
if stream is not None: if stream is not None:
stream.close() stream.close()
else: else:
book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg') book_cover_filepath = os.path.join(config.get_book_path(), book.path, 'cover.jpg')
if not os.path.isfile(book_cover_filepath): if not os.path.isfile(book_cover_filepath):
raise Exception('Book cover file not found') raise Exception('Book cover file not found')
@ -404,7 +404,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
if stream is not None: if stream is not None:
stream.close() stream.close()
book_cover_filepath = os.path.join(config.config_calibre_dir, book.path, 'cover.jpg') book_cover_filepath = os.path.join(config.get_book_path(), book.path, 'cover.jpg')
if not os.path.isfile(book_cover_filepath): if not os.path.isfile(book_cover_filepath):
raise Exception('Book cover file not found') raise Exception('Book cover file not found')

View File

@ -161,6 +161,7 @@
<a class="btn btn-default" id="db_config" href="{{url_for('admin.db_configuration')}}">{{_('Edit Calibre Database Configuration')}}</a> <a class="btn btn-default" id="db_config" href="{{url_for('admin.db_configuration')}}">{{_('Edit Calibre Database Configuration')}}</a>
<a class="btn btn-default" id="basic_config" href="{{url_for('admin.configuration')}}">{{_('Edit Basic Configuration')}}</a> <a class="btn btn-default" id="basic_config" href="{{url_for('admin.configuration')}}">{{_('Edit Basic Configuration')}}</a>
<a class="btn btn-default" id="view_config" href="{{url_for('admin.view_configuration')}}">{{_('Edit UI Configuration')}}</a> <a class="btn btn-default" id="view_config" href="{{url_for('admin.view_configuration')}}">{{_('Edit UI Configuration')}}</a>
<a class="btn btn-default" id="list_pages" href="{{url_for('listpages.show_list')}}">{{_('List Pages')}}</a>
</div> </div>
</div> </div>
{% if feature_support['scheduler'] %} {% if feature_support['scheduler'] %}

View File

@ -16,6 +16,18 @@
<button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button> <button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span> </span>
</div> </div>
<div class="form-group required">
<input type="checkbox" id="config_calibre_split" name="config_calibre_split" data-control="split_settings" data-t ="{{ config.config_calibre_split_dir }}" {% if config.config_calibre_split %}checked{% endif %} >
<label for="config_calibre_split">{{_('Separate Book files from Library')}}</label>
</div>
<div data-related="split_settings">
<div class="form-group required input-group">
<input type="text" class="form-control" id="config_calibre_split_dir" name="config_calibre_split_dir" value="{% if config.config_calibre_split_dir != None %}{{ config.config_calibre_split_dir }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" data-toggle="modal" id="calibre_modal_split_path" data-link="config_calibre_split_dir" data-filefilter="" data-target="#fileModal" id="book_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
</div>
{% if feature_support['gdrive'] %} {% if feature_support['gdrive'] %}
<div class="form-group required"> <div class="form-group required">
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} > <input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >

View File

@ -0,0 +1,45 @@
{% extends "layout.html" %}
{% block body %}
<div class="discover">
<div><a class="session" href="{{url_for('listpages.show_list')}}">{{_('Back')}}</a></div>
<h2>{{_('Edit page')}}</h2>
<form role="form" class="col-md-10 col-lg-6" method="POST" action="{{ url_for('editpage.edit_page', file=file) }}"
autocomplete="off">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="title">{{_('Title')}}</label>
<input type="text" class="form-control" name="title" id="title" value="{{ title }}" required>
</div>
<div class="form-group">
<label for="name">{{_('Name')}}</label>
<input type="text" class="form-control" name="name" id="name" value="{{ name }}" required>
</div>
<div class="form-group">
<label for="icon">{{_('Icon')}}</label>
<input type="text" class="form-control" name="icon" id="icon" value="{{ icon }}" required>
<a href="https://www.w3schools.com/bootstrap/bootstrap_ref_comp_glyphs.asp" target="_blank" rel="noopener">{{_('Icons list')}}</a>
</div>
<div class="form-group">
<label for="content">{{_('Content')}}</label>
<textarea class="form-control" name="content" id="content" rows="15">{{ content }}</textarea>
</div>
<div class="form-group">
<label for="position">{{_('Position')}}</label>
<select name="position" id="position" class="form-control">
<option value="0" {% if position=="0" %}selected{% endif %}>{{ _("Sidebar Bottom") }}</option>
<option value="1" {% if position=="1" %}selected{% endif %}>{{ _("Sidebar Top") }}</option>
</select>
</div>
<div class="form-group">
<input type="checkbox" id="is_enabled" name="is_enabled" {% if is_enabled %}checked{% endif %}>
<label for="is_enabled">{{_('Enabled')}}</label>
</div>
<div class="form-group">
<label for="order">{{_('Order')}}</label>
<input type="number" class="form-control" name="order" id="order" value="{{ order }}" required>
</div>
<button type="submit" name="submit" id="page_submit" class="btn btn-default">{{_('Save')}}</button>
<a href="{{ url_for('admin.admin') }}" id="config_back" class="btn btn-default">{{_('Cancel')}}</a>
</form>
</div>
{% endblock %}

View File

@ -142,6 +142,9 @@
<div class="col-sm-2"> <div class="col-sm-2">
<nav class="navigation"> <nav class="navigation">
<ul class="list-unstyled" id="scnd-nav" intent in-standard-append="nav.navigation" in-mobile-after="#main-nav" in-mobile-class="nav navbar-nav"> <ul class="list-unstyled" id="scnd-nav" intent in-standard-append="nav.navigation" in-mobile-after="#main-nav" in-mobile-class="nav navbar-nav">
{% for element in top_pages %}
<li id="nav_{{element['name']}}" {% if page == element['name'] %}class="active"{% endif %}><a href="{{url_for('page.get_page', file=element['name'])}}"><span class="glyphicon glyphicon-{{element['icon']}}"></span> {{element['title']}}</a></li>
{% endfor %}
<li class="nav-head hidden-xs">{{_('Browse')}}</li> <li class="nav-head hidden-xs">{{_('Browse')}}</li>
{% for element in sidebar %} {% for element in sidebar %}
{% if current_user.check_visibility(element['visibility']) and element['public'] %} {% if current_user.check_visibility(element['visibility']) and element['public'] %}
@ -157,6 +160,9 @@
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li> <li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li>
<li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li> <li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li>
{% endif %} {% endif %}
{% for element in bottom_pages %}
<li id="nav_{{element['name']}}" {% if page == element['name'] %}class="active"{% endif %}><a href="{{url_for('page.get_page', file=element['name'])}}"><span class="glyphicon glyphicon-{{element['icon']}}"></span> {{element['title']}}</a></li>
{% endfor %}
{% endif %} {% endif %}
</ul> </ul>

View File

@ -0,0 +1,52 @@
{% extends "layout.html" %}
{% block header %}
<link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/libs/bootstrap-select.min.css') }}" rel="stylesheet" >
{% endblock %}
{% block body %}
<h2 class="{{page}}">{{_(title)}}</h2>
<table class="table table-striped" id="table_user">
<thead>
<tr>
<th>{{_('Name')}}</th>
<th>{{_('Title')}}</th>
<th>{{_('Icon')}}</th>
<th>{{_('Position')}}</th>
<th>{{_('Enabled')}}</th>
<th>{{_('Order')}}</th>
</tr>
</thead>
<tbody>
{% for page in pages %}
<tr>
<td><a class="session" href="{{url_for('editpage.edit_page', file=page.id)}}">{{page.name}}</a></td>
<td>{{page.title}}</td>
<td>{{page.icon}}</td>
<td>{{_('bottom') if page.position == "0" else _('top')}}</td>
<td>
{% if page.is_enabled %}
<span class="glyphicon glyphicon-ok"></span>
{% else %}
<span class="glyphicon glyphicon-remove"></span>
{% endif %}
</td>
<td>{{page.order}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a class="session" id="new_page" href="{{url_for('editpage.edit_page', file="new")}}">{{_('New Page')}}</a>
{% endblock %}
{% block js %}
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-locale-all.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
{% if not current_user.locale == 'en' %}
<script
src="{{ url_for('static', filename='js/libs/bootstrap-table/locale/bootstrap-table-' + current_user.locale + '.min.js') }}"
charset="UTF-8"></script>
{% endif %}
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
{% endblock %}

4
cps/templates/page.html Normal file
View File

@ -0,0 +1,4 @@
{% extends "layout.html" %}
{% block body %}
<div>{{body|safe}}</div>
{% endblock %}

View File

@ -538,6 +538,16 @@ class Thumbnail(Base):
generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow()) generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow())
expiration = Column(DateTime, nullable=True) expiration = Column(DateTime, nullable=True)
class Page(Base):
__tablename__ = 'page'
id = Column(Integer, primary_key=True)
title = Column(String)
name = Column(String)
icon = Column(String)
order = Column(Integer)
position = Column(String)
is_enabled = Column(Boolean, default=True)
# Add missing tables during migration of database # Add missing tables during migration of database
def add_missing_tables(engine, _session): def add_missing_tables(engine, _session):
@ -561,7 +571,8 @@ def add_missing_tables(engine, _session):
trans = conn.begin() trans = conn.begin()
conn.execute("insert into registration (domain, allow) values('%.%',1)") conn.execute("insert into registration (domain, allow) values('%.%',1)")
trans.commit() trans.commit()
if not engine.dialect.has_table(engine.connect(), "page"):
Page.__table__.create(bind=engine)
# migrate all settings missing in registration table # migrate all settings missing in registration table
def migrate_registration_table(engine, _session): def migrate_registration_table(engine, _session):

6
cps/web.py Executable file → Normal file
View File

@ -1192,7 +1192,7 @@ def serve_book(book_id, book_format, anyname):
if book_format.upper() == 'TXT': if book_format.upper() == 'TXT':
log.info('Serving book: %s', data.name) log.info('Serving book: %s', data.name)
try: try:
rawdata = open(os.path.join(config.config_calibre_dir, book.path, data.name + "." + book_format), rawdata = open(os.path.join(config.get_book_path(), book.path, data.name + "." + book_format),
"rb").read() "rb").read()
result = chardet.detect(rawdata) result = chardet.detect(rawdata)
return make_response( return make_response(
@ -1202,7 +1202,7 @@ def serve_book(book_id, book_format, anyname):
return "File Not Found" return "File Not Found"
# enable byte range read of pdf # enable byte range read of pdf
response = make_response( response = make_response(
send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)) send_from_directory(os.path.join(config.get_book_path(), book.path), data.name + "." + book_format))
if not range_header: if not range_header:
log.info('Serving book: %s', data.name) log.info('Serving book: %s', data.name)
response.headers['Accept-Ranges'] = 'bytes' response.headers['Accept-Ranges'] = 'bytes'
@ -1226,7 +1226,7 @@ def send_to_ereader(book_id, book_format, convert):
response = [{'type': "danger", 'message': _("Please configure the SMTP mail settings first...")}] response = [{'type': "danger", 'message': _("Please configure the SMTP mail settings first...")}]
return Response(json.dumps(response), mimetype='application/json') return Response(json.dumps(response), mimetype='application/json')
elif current_user.kindle_mail: elif current_user.kindle_mail:
result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir, result = send_mail(book_id, book_format, convert, current_user.kindle_mail, config.get_book_path(),
current_user.name) current_user.name)
if result is None: if result is None:
ub.update_download(book_id, int(current_user.id)) ub.update_download(book_id, int(current_user.id))

View File

@ -9,7 +9,7 @@ iso-639>=0.4.5,<0.5.0
PyPDF>=3.0.0,<3.16.0 PyPDF>=3.0.0,<3.16.0
pytz>=2016.10 pytz>=2016.10
requests>=2.28.0,<2.32.0 requests>=2.28.0,<2.32.0
SQLAlchemy>=1.3.0,<2.0.0 SQLAlchemy>=1.3.0,<2.1.0
tornado>=6.3,<6.4 tornado>=6.3,<6.4
Wand>=0.4.4,<0.7.0 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.0 unidecode>=0.04.19,<1.4.0
@ -18,3 +18,4 @@ flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0 advocate>=1.0.0,<1.1.0
Flask-Limiter>=2.3.0,<3.6.0 Flask-Limiter>=2.3.0,<3.6.0
markdown>=3.5.1

View File

@ -41,7 +41,7 @@ install_requires =
Werkzeug<3.0.0 Werkzeug<3.0.0
APScheduler>=3.6.3,<3.11.0 APScheduler>=3.6.3,<3.11.0
Babel>=1.3,<3.0 Babel>=1.3,<3.0
Flask-Babel>=0.11.1,<3.2.0 Flask-Babel>=0.11.1,<4.1.0
Flask-Login>=0.3.2,<0.6.3 Flask-Login>=0.3.2,<0.6.3
Flask-Principal>=0.3.2,<0.5.1 Flask-Principal>=0.3.2,<0.5.1
Flask>=1.0.2,<2.4.0 Flask>=1.0.2,<2.4.0
@ -49,15 +49,15 @@ install_requires =
PyPDF>=3.0.0,<3.16.0 PyPDF>=3.0.0,<3.16.0
pytz>=2016.10 pytz>=2016.10
requests>=2.28.0,<2.32.0 requests>=2.28.0,<2.32.0
SQLAlchemy>=1.3.0,<2.0.0 SQLAlchemy>=1.3.0,<2.1.0
tornado>=6.3,<6.4 tornado>=6.3,<6.4
Wand>=0.4.4,<0.7.0 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.0 unidecode>=0.04.19,<1.4.0
lxml>=3.8.0,<5.0.0 lxml>=3.8.0,<5.0.0
flask-wtf>=0.14.2,<1.2.0 flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.1.0 advocate>=1.0.0,<1.1.0
Flask-Limiter>=2.3.0,<3.5.0 Flask-Limiter>=2.3.0,<3.6.0
[options.packages.find] [options.packages.find]

File diff suppressed because it is too large Load Diff