From b7aaa0f24dd5142b05fff19b58913930f9910b28 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 2 Nov 2023 17:05:02 +0100 Subject: [PATCH 01/31] Add metadata change code --- cps/admin.py | 10 ++++- cps/config_sql.py | 40 ++++++++++++----- cps/constants.py | 5 +++ cps/helper.py | 82 +++++++++++++++++++++++++++++++--- cps/tasks/convert.py | 34 +++++++++++--- cps/templates/config_edit.html | 10 ++--- 6 files changed, 152 insertions(+), 29 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 045a9523..51fddbee 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -47,7 +47,7 @@ from . import constants, logger, helper, services, cli_param from . import db, calibre_db, ub, web_server, config, updater_thread, gdriveutils, \ kobo_sync_status, schedule from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \ - valid_email, check_username + valid_email, check_username, get_calibre_binarypath from .gdriveutils import is_gdrive_ready, gdrive_support from .render_template import render_title_template, get_sidebar_config from .services.worker import WorkerThread @@ -1761,8 +1761,14 @@ def _configuration_update_helper(): constants.EXTENSIONS_UPLOAD = config.config_upload_formats.split(',') _config_string(to_save, "config_calibre") - _config_string(to_save, "config_converterpath") + _config_string(to_save, "config_binariesdir") _config_string(to_save, "config_kepubifypath") + if "config_binariesdir" in to_save: + calibre_status = helper.check_calibre(config.config_binariesdir) + if calibre_status: + return _configuration_result(calibre_status) + to_save["config_converterpath"] = get_calibre_binarypath("ebook-convert") + _config_string(to_save, "config_converterpath") reboot_required |= _config_int(to_save, "config_login_type") diff --git a/cps/config_sql.py b/cps/config_sql.py index 21644ccd..485c3fc2 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -34,6 +34,7 @@ except ImportError: from sqlalchemy.ext.declarative import declarative_base from . import constants, logger +from .subproc_wrapper import process_wait log = logger.create() @@ -138,6 +139,7 @@ class _Settings(_Base): config_kepubifypath = Column(String, default=None) config_converterpath = Column(String, default=None) + config_binariesdir = Column(String, default=None) config_calibre = Column(String) config_rarfile_location = Column(String, default=None) config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD)) @@ -184,9 +186,11 @@ class ConfigSQL(object): self.load() change = False - if self.config_converterpath == None: # pylint: disable=access-member-before-definition + + if self.config_binariesdir == None: # pylint: disable=access-member-before-definition change = True - self.config_converterpath = autodetect_calibre_binary() + self.config_binariesdir = autodetect_calibre_binaries() + self.config_converterpath = autodetect_converter_binary(self.config_binariesdir) if self.config_kepubifypath == None: # pylint: disable=access-member-before-definition change = True @@ -469,17 +473,32 @@ def _migrate_table(session, orm_class, secret_key=None): session.rollback() -def autodetect_calibre_binary(): +def autodetect_calibre_binaries(): if sys.platform == "win32": - calibre_path = ["C:\\program files\\calibre\\ebook-convert.exe", - "C:\\program files(x86)\\calibre\\ebook-convert.exe", - "C:\\program files(x86)\\calibre2\\ebook-convert.exe", - "C:\\program files\\calibre2\\ebook-convert.exe"] + calibre_path = ["C:\\program files\\calibre\\", + "C:\\program files(x86)\\calibre\\", + "C:\\program files(x86)\\calibre2\\", + "C:\\program files\\calibre2\\"] else: - calibre_path = ["/opt/calibre/ebook-convert"] + calibre_path = ["/opt/calibre/"] for element in calibre_path: - if os.path.isfile(element) and os.access(element, os.X_OK): - return element + supported_binary_paths = [os.path.join(element, binary) for binary in constants.SUPPORTED_CALIBRE_BINARIES.values()] + if all(os.path.isfile(binary_path) and os.access(binary_path, os.X_OK) for binary_path in supported_binary_paths): + values = [process_wait([binary_path, "--version"], pattern='\(calibre (.*)\)') for binary_path in supported_binary_paths] + if all(values): + version = values[0].group(1) + log.debug("calibre version %s", version) + return element + return "" + + +def autodetect_converter_binary(calibre_path): + if sys.platform == "win32": + converter_path = os.path.join(calibre_path, "ebook-convert.exe") + else: + converter_path = os.path.join(calibre_path, "ebook-convert") + if calibre_path and os.path.isfile(converter_path) and os.access(converter_path, os.X_OK): + return converter_path return "" @@ -521,6 +540,7 @@ def load_configuration(session, secret_key): session.commit() + def get_flask_session_key(_session): flask_settings = _session.query(_Flask_Settings).one_or_none() if flask_settings == None: diff --git a/cps/constants.py b/cps/constants.py index 18c4f1b1..d8842e78 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -156,6 +156,11 @@ EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr' 'prc', 'doc', 'docx', 'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'} +_extension = "" +if sys.platform == "win32": + _extension = ".exe" +SUPPORTED_CALIBRE_BINARIES = {binary:binary + _extension for binary in ["ebook-convert", "calibredb"]} + def has_flag(value, bit_flag): return bit_flag == (bit_flag & (value or 0)) diff --git a/cps/helper.py b/cps/helper.py index 0c526d01..cabc0363 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -54,8 +54,8 @@ from . import calibre_db, cli_param from .tasks.convert import TaskConvert from . import logger, config, db, ub, fs from . import gdriveutils as gd -from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES -from .subproc_wrapper import process_wait +from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES, SUPPORTED_CALIBRE_BINARIES +from .subproc_wrapper import process_wait, process_open from .services.worker import WorkerThread from .tasks.mail import TaskEmail from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails @@ -938,9 +938,10 @@ def save_cover(img, book_path): def do_download_file(book, book_format, client, data, headers): + book_name = data.name if config.config_use_google_drive: # startTime = time.time() - df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format) + df = gd.getFileFromEbooksFolder(book.path, book_name + "." + book_format) # log.debug('%s', time.time() - startTime) if df: return gd.do_gdrive_download(df, headers) @@ -948,20 +949,47 @@ def do_download_file(book, book_format, client, data, headers): abort(404) else: filename = os.path.join(config.config_calibre_dir, book.path) - if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)): + if not os.path.isfile(os.path.join(filename, book_name + "." + book_format)): # 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, book_name + "." + book_format)) if client == "kobo" and book_format == "kepub": headers["Content-Disposition"] = headers["Content-Disposition"].replace(".kepub", ".kepub.epub") - response = make_response(send_from_directory(filename, data.name + "." + book_format)) + if config.config_binariesdir: + filename, book_name = do_calibre_export(book, book_format) + + response = make_response(send_from_directory(filename, book_name + "." + book_format)) # ToDo Check headers parameter for element in headers: response.headers[element[0]] = element[1] - log.info('Downloading file: {}'.format(os.path.join(filename, data.name + "." + book_format))) + log.info('Downloading file: {}'.format(os.path.join(filename, book_name + "." + book_format))) return response + +def do_calibre_export(book, book_format): + try: + quotes = [3, 5, 7, 9] + tmp_dir = os.path.join(gettempdir(), 'calibre_web') + if not os.path.isdir(tmp_dir): + os.mkdir(tmp_dir) + calibredb_binarypath = get_calibre_binarypath("calibredb") + opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', str(book.id), + '--with-library', config.config_calibre_dir, '--to-dir', tmp_dir, + '--formats', book_format, "--template", "{} - {{authors}}".format(book.title)] + file_name = book.title + if len(book.authors) > 0: + file_name = file_name + ' - ' + book.authors[0].name + p = process_open(opf_command, quotes) + _, err = p.communicate() + if err: + log.error('Metadata embedder encountered an error: %s', err) + return tmp_dir, file_name + except OSError as ex: + # ToDo real error handling + log.error_or_exception(ex) + + ################################## @@ -984,6 +1012,35 @@ def check_unrar(unrar_location): return _('Error executing UnRar') +def check_calibre(calibre_location): + if not calibre_location: + return + + if not os.path.exists(calibre_location): + return _('Could not find the specified directory') + + if not os.path.isdir(calibre_location): + return _('Please specify a directory, not a file') + + try: + supported_binary_paths = [os.path.join(calibre_location, binary) for binary in SUPPORTED_CALIBRE_BINARIES.values()] + binaries_available=[os.path.isfile(binary_path) and os.access(binary_path, os.X_OK) for binary_path in supported_binary_paths] + if all(binaries_available): + values = [process_wait([binary_path, "--version"], pattern='\(calibre (.*)\)') for binary_path in supported_binary_paths] + if all(values): + version = values[0].group(1) + log.debug("calibre version %s", version) + else: + return _('Calibre binaries not viable') + else: + missing_binaries=[path for path, available in zip(SUPPORTED_CALIBRE_BINARIES.values(), binaries_available) if not available] + return _('Missing calibre binaries: %(missing)s', missing=", ".join(missing_binaries)) + + except (OSError, UnicodeDecodeError) as err: + log.error_or_exception(err) + return _('Error excecuting Calibre') + + def json_serial(obj): """JSON serializer for objects not serializable by default json code""" @@ -1047,6 +1104,17 @@ def get_download_link(book_id, book_format, client): abort(404) +def get_calibre_binarypath(binary): + binariesdir = config.config_binariesdir + if binariesdir: + try: + return os.path.join(binariesdir, SUPPORTED_CALIBRE_BINARIES[binary]) + except KeyError as ex: + log.error("Binary not supported by Calibre-Web: %s", SUPPORTED_CALIBRE_BINARIES[binary]) + pass + return "" + + def clear_cover_thumbnail_cache(book_id): if config.schedule_generate_book_covers: WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id), hidden=True) diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py index df6ae104..2bef9a20 100755 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -19,8 +19,10 @@ import os import re from glob import glob -from shutil import copyfile +from shutil import copyfile, copyfileobj from markupsafe import escape +from tempfile import gettempdir +from time import time from sqlalchemy.exc import SQLAlchemyError from flask_babel import lazy_gettext as N_ @@ -35,10 +37,11 @@ from cps.ub import init_db_thread from cps.tasks.mail import TaskEmail from cps import gdriveutils - +from cps.constants import SUPPORTED_CALIBRE_BINARIES log = logger.create() +current_milli_time = lambda: int(round(time() * 1000)) class TaskConvert(CalibreTask): def __init__(self, file_path, book_id, task_message, settings, ereader_mail, user=None): @@ -61,15 +64,20 @@ class TaskConvert(CalibreTask): data = worker_db.get_book_format(self.book_id, self.settings['old_book_format']) df = gdriveutils.getFileFromEbooksFolder(cur_book.path, data.name + "." + self.settings['old_book_format'].lower()) - if df: + df_cover = gdriveutils.getFileFromEbooksFolder(cur_book.path, "cover.jpg") + if df and df_cover: datafile = os.path.join(config.config_calibre_dir, cur_book.path, data.name + "." + self.settings['old_book_format'].lower()) + datafile_cover = os.path.join(config.config_calibre_dir, + cur_book.path, "cover.jpg") if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)): os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path)) df.GetContentFile(datafile) + df_cover.GetContentFile(datafile_cover) worker_db.session.close() else: + # ToDo Include cover in error handling error_message = _("%(format)s not found on Google Drive: %(fn)s", format=self.settings['old_book_format'], fn=data.name + "." + self.settings['old_book_format'].lower()) @@ -79,6 +87,7 @@ class TaskConvert(CalibreTask): filename = self._convert_ebook_format() if config.config_use_google_drive: os.remove(self.file_path + '.' + self.settings['old_book_format'].lower()) + os.remove(os.path.join(config.config_calibre_dir, cur_book.path, "cover.jpg")) if filename: if config.config_use_google_drive: @@ -225,15 +234,30 @@ class TaskConvert(CalibreTask): return check, None def _convert_calibre(self, file_path, format_old_ext, format_new_ext): + book_id = self.book_id try: # Linux py2.7 encode as list without quotes no empty element for parameters # linux py3.x no encode and as list without quotes no empty element for parameters # windows py2.7 encode as string with quotes empty element for parameters is okay # windows py 3.x no encode and as string with quotes empty element for parameters is okay # separate handling for windows and linux - quotes = [1, 2] + + quotes = [3, 5] + tmp_dir = os.path.join(gettempdir(), 'calibre_web') + if not os.path.isdir(tmp_dir): + os.mkdir(tmp_dir) + calibredb_binarypath = os.path.join(config.config_binariesdir, SUPPORTED_CALIBRE_BINARIES["calibredb"]) + opf_command = [calibredb_binarypath, 'show_metadata', '--as-opf', str(book_id), '--with-library', config.config_calibre_dir] + p = process_open(opf_command, quotes) + p.wait() + path_tmp_opf = os.path.join(tmp_dir, "metadata_" + str(current_milli_time()) + ".opf") + with open(path_tmp_opf, 'w') as fd: + copyfileobj(p.stdout, fd) + + quotes = [1, 2, 4, 6] command = [config.config_converterpath, (file_path + format_old_ext), - (file_path + format_new_ext)] + (file_path + format_new_ext), '--from-opf', path_tmp_opf, + '--cover', os.path.join(os.path.dirname(file_path), 'cover.jpg')] quotes_index = 3 if config.config_calibre: parameters = config.config_calibre.split(" ") diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index d101f960..2ec0575c 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -323,12 +323,12 @@
- +
- - - - + + + +
From fad6550ff17307f8c5289d1dcb5991575da69717 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 6 Nov 2023 16:35:39 +0100 Subject: [PATCH 02/31] Show "all" opds feed entries only if there is at least one entry --- cps/opds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/opds.py b/cps/opds.py index 4067712f..b13b0570 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -502,7 +502,7 @@ def render_element_index(database_column, linked_table, folder): 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() elements = [] - if off == 0: + if off == 0 and entries: elements.append({'id': "00", 'name': _("All")}) shift = 1 for entry in entries[ From d68e57c4fc5525b32c2312a251434368d9211251 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Tue, 7 Nov 2023 19:30:13 +0100 Subject: [PATCH 03/31] Implement split library and books Bugfix arrows in comic reader Fix kobo download link Updated requirement --- cps/admin.py | 5 +- cps/config_sql.py | 5 + cps/editbooks.py | 22 +- cps/epub.py | 3 +- cps/helper.py | 6 +- cps/kobo.py | 2 +- cps/static/js/kthoom.js | 5 +- cps/tasks/convert.py | 6 +- cps/tasks/mail.py | 2 +- cps/tasks/metadata_backup.py | 2 +- cps/tasks/thumbnail.py | 4 +- cps/templates/config_db.html | 12 + cps/web.py | 6 +- requirements.txt | 2 +- setup.cfg | 2 +- test/Calibre-Web TestSummary_Linux.html | 3878 ++++++++++++++--------- 16 files changed, 2428 insertions(+), 1534 deletions(-) mode change 100755 => 100644 cps/editbooks.py mode change 100755 => 100644 cps/tasks/convert.py mode change 100755 => 100644 cps/tasks/mail.py mode change 100755 => 100644 cps/web.py diff --git a/cps/admin.py b/cps/admin.py index 045a9523..c9f28e26 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -1702,7 +1702,7 @@ def _db_configuration_update_helper(): return _db_configuration_result('{}'.format(ex), gdrive_error) 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']: return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdrive_error) else: @@ -1725,6 +1725,9 @@ def _db_configuration_update_helper(): calibre_db.update_config(config) if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK): 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() return _db_configuration_result(None, gdrive_error) diff --git a/cps/config_sql.py b/cps/config_sql.py index 21644ccd..f6c0991c 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -69,6 +69,8 @@ class _Settings(_Base): config_calibre_dir = 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_external_port = Column(Integer, default=constants.DEFAULT_PORT) config_certfile = Column(String) @@ -389,6 +391,9 @@ class ConfigSQL(object): self.db_configured = False 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): try: calibre_uuid = calibre_db.session.query(Library_table).one_or_none() diff --git a/cps/editbooks.py b/cps/editbooks.py old mode 100755 new mode 100644 index b8f6363f..2449eab9 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -126,7 +126,7 @@ def edit_book(book_id): edited_books_id = book.id modify_date = True title_author_error = helper.update_dir_structure(edited_books_id, - config.config_calibre_dir, + config.get_book_path(), input_authors[0], renamed_author=renamed) if title_author_error: @@ -271,7 +271,7 @@ def upload(): meta.extension.lower()) else: error = helper.update_dir_structure(book_id, - config.config_calibre_dir, + config.get_book_path(), input_authors[0], meta.file_path, title_dir + meta.extension.lower(), @@ -321,7 +321,7 @@ def convert_bookformat(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) - 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) if rtn is None: @@ -391,7 +391,7 @@ def edit_list_book(param): elif param == 'title': sort_param = book.sort 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: ret = Response(json.dumps({'success': True, 'newValue': book.title}), mimetype='application/json') @@ -409,7 +409,7 @@ def edit_list_book(param): mimetype='application/json') elif param == 'authors': 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) if not rename_error: ret = Response(json.dumps({ @@ -513,10 +513,10 @@ def merge_list_book(): for element in from_book.data: if element.format not in to_file: # 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_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, element.name + "." + element.format.lower())) copyfile(filepath_old, filepath_new) @@ -556,7 +556,7 @@ def table_xchange_author_title(): if edited_books_id: # 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) if modify_date: book.last_modified = datetime.utcnow() @@ -753,7 +753,7 @@ def move_coverfile(meta, db_book): cover_file = meta.cover else: 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: os.makedirs(new_cover_path, exist_ok=True) copyfile(cover_file, os.path.join(new_cover_path, "cover.jpg")) @@ -839,7 +839,7 @@ def delete_book_from_table(book_id, book_format, json_response): book = calibre_db.get_book(book_id) if book: 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 json_response: return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id), @@ -1172,7 +1172,7 @@ def upload_single_file(file_request, book, book_id): return False 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) # check if file path exists, otherwise create it, copy file to calibre path and delete temp file diff --git a/cps/epub.py b/cps/epub.py index 50adba59..b45f3e51 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -48,7 +48,8 @@ def get_epub_layout(book, book_data): 'n': 'urn:oasis:names:tc:opendocument:xmlns:container', '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: epubZip = zipfile.ZipFile(file_path) diff --git a/cps/helper.py b/cps/helper.py index 0c526d01..bd219594 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -781,7 +781,7 @@ def get_book_cover_internal(book, resolution=None): # Send the book cover from the Calibre directory 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")): return send_from_directory(cover_file_path, "cover.jpg") else: @@ -934,7 +934,7 @@ def save_cover(img, book_path): else: return False, message 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): @@ -947,7 +947,7 @@ def do_download_file(book, book_format, client, data, headers): else: abort(404) 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)): # ToDo: improve error handling log.error('File not found: %s', os.path.join(filename, data.name + "." + book_format)) diff --git a/cps/kobo.py b/cps/kobo.py index 0a968081..9900200e 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -205,7 +205,7 @@ def HandleSyncRequest(): for book in books: formats = [data.format for data in book.Books.data] 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) entitlement = { diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 67b18fc1..b7ed6c8b 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -179,8 +179,9 @@ kthoom.ImageFile = function(file) { }; function updateDirectionButtons(){ - var left, right = 1; - if (currentImage == 0 ) { + var left = 1; + var right = 1; + if (currentImage <= 0 ) { if (settings.direction === 0) { left = 0; } else { diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py old mode 100755 new mode 100644 index df6ae104..7b2c8718 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -62,11 +62,11 @@ class TaskConvert(CalibreTask): df = gdriveutils.getFileFromEbooksFolder(cur_book.path, data.name + "." + self.settings['old_book_format'].lower()) if df: - datafile = os.path.join(config.config_calibre_dir, + datafile = os.path.join(config.get_book_path(), cur_book.path, data.name + "." + self.settings['old_book_format'].lower()) - if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)): - os.makedirs(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.get_book_path(), cur_book.path)) df.GetContentFile(datafile) worker_db.session.close() else: diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py old mode 100755 new mode 100644 index a305b623..36133ccf --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -239,7 +239,7 @@ class TaskEmail(CalibreTask): @classmethod def _get_attachment(cls, book_path, filename): """Get file as MIMEBase message""" - calibre_path = config.config_calibre_dir + calibre_path = config.get_book_path() if config.config_use_google_drive: df = gdriveutils.getFileFromEbooksFolder(book_path, filename) if df: diff --git a/cps/tasks/metadata_backup.py b/cps/tasks/metadata_backup.py index 1751feeb..45015ccf 100644 --- a/cps/tasks/metadata_backup.py +++ b/cps/tasks/metadata_backup.py @@ -114,7 +114,7 @@ class TaskBackupMetadata(CalibreTask): True) else: # 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 doc = etree.ElementTree(package) try: diff --git a/cps/tasks/thumbnail.py b/cps/tasks/thumbnail.py index 6d11fe97..dd9ee1e0 100644 --- a/cps/tasks/thumbnail.py +++ b/cps/tasks/thumbnail.py @@ -209,7 +209,7 @@ class TaskGenerateCoverThumbnails(CalibreTask): if stream is not None: stream.close() 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): raise Exception('Book cover file not found') @@ -404,7 +404,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask): if stream is not None: 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): raise Exception('Book cover file not found') diff --git a/cps/templates/config_db.html b/cps/templates/config_db.html index 0090bd95..6e54d97c 100644 --- a/cps/templates/config_db.html +++ b/cps/templates/config_db.html @@ -16,6 +16,18 @@
+
+ + +
+
+
+ + + + +
+
{% if feature_support['gdrive'] %}
diff --git a/cps/web.py b/cps/web.py old mode 100755 new mode 100644 index 4430dd90..6b26f29e --- a/cps/web.py +++ b/cps/web.py @@ -1192,7 +1192,7 @@ def serve_book(book_id, book_format, anyname): if book_format.upper() == 'TXT': log.info('Serving book: %s', data.name) 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() result = chardet.detect(rawdata) return make_response( @@ -1202,7 +1202,7 @@ def serve_book(book_id, book_format, anyname): return "File Not Found" # enable byte range read of pdf 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: log.info('Serving book: %s', data.name) 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...")}] return Response(json.dumps(response), mimetype='application/json') 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) if result is None: ub.update_download(book_id, int(current_user.id)) diff --git a/requirements.txt b/requirements.txt index e384ed87..53be7c2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ iso-639>=0.4.5,<0.5.0 PyPDF>=3.0.0,<3.16.0 pytz>=2016.10 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 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.4.0 diff --git a/setup.cfg b/setup.cfg index 4bcd1a11..46e5421b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,7 +49,7 @@ install_requires = PyPDF>=3.0.0,<3.16.0 pytz>=2016.10 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 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.4.0 diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 7ca3dad5..f5ad2347 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2023-10-16 19:38:22

+

Start Time: 2023-11-07 19:32:08

-

Stop Time: 2023-10-17 02:18:49

+

Stop Time: 2023-11-08 00:42:45

-

Duration: 5h 37 min

+

Duration: 4h 11 min

@@ -234,11 +234,11 @@ - + TestBackupMetadata 22 - 22 - 0 + 21 + 1 0 0 @@ -266,11 +266,31 @@ - +
TestBackupMetadata - test_backup_change_book_description
- PASS + +
+ FAIL +
+ + + + @@ -1014,12 +1034,12 @@ - + TestEditAdditionalBooks 20 - 18 - 0 - 0 + 9 + 1 + 8 2 Detail @@ -1037,11 +1057,50 @@ - +
TestEditAdditionalBooks - test_change_upload_formats
- PASS + +
+ ERROR +
+ + + + @@ -1055,11 +1114,31 @@ - +
TestEditAdditionalBooks - test_delete_role
- PASS + +
+ FAIL +
+ + + + @@ -1109,56 +1188,233 @@ - +
TestEditAdditionalBooks - test_title_sort
- PASS + +
+ ERROR +
+ + + + - +
TestEditAdditionalBooks - test_upload_cbz_coverformats
- PASS + +
+ ERROR +
+ + + + - +
TestEditAdditionalBooks - test_upload_edit_role
- PASS + +
+ ERROR +
+ + + + - +
TestEditAdditionalBooks - test_upload_metadata_cb7
- PASS + +
+ ERROR +
+ + + + - +
TestEditAdditionalBooks - test_upload_metadata_cbr
- PASS + +
+ ERROR +
+ + + + - +
TestEditAdditionalBooks - test_upload_metadata_cbt
- PASS + +
+ ERROR +
+ + + + @@ -1189,11 +1445,42 @@ - +
TestEditAdditionalBooks - test_writeonly_path
- PASS + +
+ ERROR +
+ + + + @@ -1226,6 +1513,490 @@ + + _ErrorHolder + 9 + 0 + 0 + 9 + 0 + + Detail + + + + + + + +
tearDownClass (test_edit_additional_books)
+ + +
+ ERROR +
+ + + + + + + + + + +
tearDownClass (test_thumbnail_env)
+ + +
+ ERROR +
+ + + + + + + + + + +
tearDownClass (test_thumbnails)
+ + +
+ ERROR +
+ + + + + + + + + + +
setUpClass (test_upload_epubs)
+ + +
+ ERROR +
+ + + + + + + + + + +
setUpClass (test_user_list)
+ + +
+ ERROR +
+ + + + + + + + + + +
setUpClass (test_user_load)
+ + +
+ ERROR +
+ + + + + + + + + + +
setUpClass (test_user_template)
+ + +
+ ERROR +
+ + + + + + + + + + +
setUpClass (test_visiblilitys)
+ + +
+ ERROR +
+ + + + + + + + + + +
setUpClass (test_zz_helper)
+ + +
+ ERROR +
+ + + + + + + + + TestEditBooks 38 @@ -1234,13 +2005,13 @@ 0 2 - Detail + Detail - +
TestEditBooks - test_download_book
@@ -1249,7 +2020,7 @@ - +
TestEditBooks - test_edit_author
@@ -1258,7 +2029,7 @@ - +
TestEditBooks - test_edit_category
@@ -1267,7 +2038,7 @@ - +
TestEditBooks - test_edit_comments
@@ -1276,7 +2047,7 @@ - +
TestEditBooks - test_edit_custom_bool
@@ -1285,7 +2056,7 @@ - +
TestEditBooks - test_edit_custom_categories
@@ -1294,7 +2065,7 @@ - +
TestEditBooks - test_edit_custom_comment
@@ -1303,7 +2074,7 @@ - +
TestEditBooks - test_edit_custom_date
@@ -1312,7 +2083,7 @@ - +
TestEditBooks - test_edit_custom_float
@@ -1321,7 +2092,7 @@ - +
TestEditBooks - test_edit_custom_int
@@ -1330,7 +2101,7 @@ - +
TestEditBooks - test_edit_custom_rating
@@ -1339,7 +2110,7 @@ - +
TestEditBooks - test_edit_custom_single_select
@@ -1348,7 +2119,7 @@ - +
TestEditBooks - test_edit_custom_text
@@ -1357,7 +2128,7 @@ - +
TestEditBooks - test_edit_language
@@ -1366,7 +2137,7 @@ - +
TestEditBooks - test_edit_publisher
@@ -1375,7 +2146,7 @@ - +
TestEditBooks - test_edit_publishing_date
@@ -1384,7 +2155,7 @@ - +
TestEditBooks - test_edit_rating
@@ -1393,7 +2164,7 @@ - +
TestEditBooks - test_edit_series
@@ -1402,7 +2173,7 @@ - +
TestEditBooks - test_edit_title
@@ -1411,19 +2182,19 @@ - +
TestEditBooks - test_rename_upper_lowercase
- SKIP + SKIP
-