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

Feature/custom pages
pull/2892/head
apollo1220 2 months ago committed by GitHub
commit f20b0f3064
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

2
.gitignore vendored

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

@ -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)

@ -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()

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

@ -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)

@ -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)

@ -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))

@ -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 = {

@ -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)

@ -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)

@ -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[

@ -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)

@ -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:

@ -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 {

@ -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:

@ -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:

@ -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:

@ -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')

@ -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'] %}

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

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

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

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

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

@ -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):

@ -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))

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

@ -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
Loading…
Cancel
Save