diff --git a/cps/about.py b/cps/about.py index 9053dfeee..fb0313434 100644 --- a/cps/about.py +++ b/cps/about.py @@ -27,7 +27,6 @@ import importlib from collections import OrderedDict import flask -import jinja2 from flask_babel import gettext as _ from . import db, calibre_db, converter, uploader, constants, dep_check diff --git a/cps/admin.py b/cps/admin.py index 5daad9588..8c65e8589 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -567,7 +567,7 @@ def update_view_configuration(): _config_string(to_save, "config_calibre_web_title") _config_string(to_save, "config_columns_to_ignore") if _config_string(to_save, "config_title_regex"): - calibre_db.update_title_sort(config) + calibre_db.create_functions(config) if not check_valid_read_column(to_save.get("config_read_column", "0")): flash(_("Invalid Read Column"), category="error") diff --git a/cps/audio.py b/cps/audio.py index 8fd28c812..b17bc7bd5 100644 --- a/cps/audio.py +++ b/cps/audio.py @@ -26,7 +26,7 @@ from cps.constants import BookMeta log = logger.create() -def get_audio_file_info(tmp_file_path, original_file_extension, original_file_name): +def get_audio_file_info(tmp_file_path, original_file_extension, original_file_name, no_cover_processing): tmp_cover_name = None audio_file = mutagen.File(tmp_file_path) comments = None @@ -50,7 +50,7 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na pubdate = str(audio_file.tags.get('TDRC').text[0]) if "TDRC" in audio_file.tags else None if not pubdate: pubdate = str(audio_file.tags.get('TDOR').text[0]) if "TDOR" in audio_file.tags else None - if cover_data: + if cover_data and not no_cover_processing: tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') cover_info = cover_data[0] for dat in cover_data: @@ -68,18 +68,19 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na publisher = audio_file.tags.get('LABEL')[0] if "LABEL" in audio_file else None pubdate = audio_file.tags.get('DATE')[0] if "DATE" in audio_file else None cover_data = audio_file.tags.get('METADATA_BLOCK_PICTURE') - if cover_data: - tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') - cover_info = mutagen.flac.Picture(base64.b64decode(cover_data[0])) - cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:]) - if hasattr(audio_file, "pictures"): - cover_info = audio_file.pictures[0] - for dat in audio_file.pictures: - if dat.type == mutagen.id3.PictureType.COVER_FRONT: - cover_info = dat - break - tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') - cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:]) + if not no_cover_processing: + if cover_data: + tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') + cover_info = mutagen.flac.Picture(base64.b64decode(cover_data[0])) + cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:]) + if hasattr(audio_file, "pictures"): + cover_info = audio_file.pictures[0] + for dat in audio_file.pictures: + if dat.type == mutagen.id3.PictureType.COVER_FRONT: + cover_info = dat + break + tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') + cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:]) elif original_file_extension in [".aac"]: title = audio_file.tags.get('Title').value if "Title" in audio_file else None author = audio_file.tags.get('Artist').value if "Artist" in audio_file else None @@ -90,7 +91,7 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na publisher = audio_file.tags.get('Label').value if "Label" in audio_file else None pubdate = audio_file.tags.get('Year').value if "Year" in audio_file else None cover_data = audio_file.tags['Cover Art (Front)'] - if cover_data: + if cover_data and not no_cover_processing: tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') with open(tmp_cover_name, "wb") as cover_file: cover_file.write(cover_data.value.split(b"\x00",1)[1]) @@ -104,7 +105,7 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na publisher = audio_file.tags.get('Label')[0].value if "Label" in audio_file else None pubdate = audio_file.tags.get('Year')[0].value if "Year" in audio_file else None cover_data = audio_file.tags.get('WM/Picture', None) - if cover_data: + if cover_data and not no_cover_processing: tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') with open(tmp_cover_name, "wb") as cover_file: cover_file.write(cover_data[0].value) @@ -118,7 +119,7 @@ def get_audio_file_info(tmp_file_path, original_file_extension, original_file_na publisher = "" pubdate = audio_file.tags.get('©day')[0] if "©day" in audio_file.tags else None cover_data = audio_file.tags.get('covr', None) - if cover_data: + if cover_data and not no_cover_processing: tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') cover_type = None for c in cover_data: diff --git a/cps/comic.py b/cps/comic.py index 27c86c9a3..a4a4f136e 100644 --- a/cps/comic.py +++ b/cps/comic.py @@ -130,7 +130,7 @@ def _extract_cover(tmp_file_name, original_file_extension, rar_executable): return cover.cover_processing(tmp_file_name, cover_data, extension) -def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rar_executable): +def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rar_executable, no_cover_processing): if use_comic_meta: try: archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable) @@ -155,14 +155,17 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r lang = loaded_metadata.language or "" loaded_metadata.language = isoLanguages.get_lang3(lang) - + if not no_cover_processing: + cover_file = _extract_cover(tmp_file_path, original_file_extension, rar_executable) + else: + cover_file = None return BookMeta( file_path=tmp_file_path, extension=original_file_extension, title=loaded_metadata.title or original_file_name, author=" & ".join([credit["person"] for credit in loaded_metadata.credits if credit["role"] == "Writer"]) or 'Unknown', - cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable), + cover=cover_file, description=loaded_metadata.comments or "", tags="", series=loaded_metadata.series or "", @@ -171,13 +174,17 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r publisher="", pubdate="", identifiers=[]) + if not no_cover_processing: + cover_file = _extract_cover(tmp_file_path, original_file_extension, rar_executable) + else: + cover_file = None return BookMeta( file_path=tmp_file_path, extension=original_file_extension, title=original_file_name, author='Unknown', - cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable), + cover=cover_file, description="", tags="", series="", diff --git a/cps/db.py b/cps/db.py index dc08a7de9..33b195fac 100644 --- a/cps/db.py +++ b/cps/db.py @@ -24,6 +24,7 @@ from datetime import datetime, timezone from urllib.parse import quote import unidecode from weakref import WeakSet +from uuid import uuid4 from sqlite3 import OperationalError as sqliteOperationalError from sqlalchemy import create_engine @@ -533,7 +534,7 @@ class CalibreDB: def init_session(self, expire_on_commit=True): self.session = self.session_factory() self.session.expire_on_commit = expire_on_commit - self.update_title_sort(self.config) + self.create_functions(self.config) @classmethod def setup_db_cc_classes(cls, cc): @@ -901,7 +902,8 @@ class CalibreDB: def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()): query = query or '' - self.session.connection().connection.connection.create_function("lower", 1, lcase) + self.create_functions() + # self.session.connection().connection.connection.create_function("lower", 1, lcase) entries = self.session.query(database).filter(tag_filter). \ filter(func.lower(database.name).ilike("%" + query + "%")).all() # json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries]) @@ -909,7 +911,8 @@ class CalibreDB: return json_dumps def check_exists_book(self, authr, title): - self.session.connection().connection.connection.create_function("lower", 1, lcase) + self.create_functions() + # self.session.connection().connection.connection.create_function("lower", 1, lcase) q = list() author_terms = re.split(r'\s*&\s*', authr) for author_term in author_terms: @@ -920,7 +923,8 @@ class CalibreDB: def search_query(self, term, config, *join): strip_whitespaces(term).lower() - self.session.connection().connection.connection.create_function("lower", 1, lcase) + self.create_functions() + # self.session.connection().connection.connection.create_function("lower", 1, lcase) q = list() author_terms = re.split("[, ]+", term) for author_term in author_terms: @@ -1018,7 +1022,7 @@ class CalibreDB: lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code) return sorted(languages, key=lambda x: x.name, reverse=reverse_order) - def update_title_sort(self, config, conn=None): + def create_functions(self, config=None): # user defined sort function for calibre databases (Series, etc.) def _title_sort(title): # calibre sort stuff @@ -1031,12 +1035,15 @@ class CalibreDB: try: # sqlalchemy <1.4.24 and sqlalchemy 2.0 - conn = conn or self.session.connection().connection.driver_connection + conn = self.session.connection().connection.driver_connection except AttributeError: # sqlalchemy >1.4.24 - conn = conn or self.session.connection().connection.connection + conn = self.session.connection().connection.connection try: - conn.create_function("title_sort", 1, _title_sort) + if config: + conn.create_function("title_sort", 1, _title_sort) + conn.create_function('uuid4', 0, lambda: str(uuid4())) + conn.create_function("lower", 1, lcase) except sqliteOperationalError: pass diff --git a/cps/editbooks.py b/cps/editbooks.py index 122b1c2cc..6eefc61fb 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -24,7 +24,7 @@ import os from datetime import datetime, timezone import json from shutil import copyfile -from uuid import uuid4 + from markupsafe import escape, Markup # dependency of flask from functools import wraps @@ -97,161 +97,22 @@ def show_edit_book(book_id): @login_required_if_no_ano @edit_required def edit_book(book_id): - modify_date = False - edit_error = False - - # create the function for sorting... - calibre_db.update_title_sort(config) - - book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) - # Book not found - if not book: - flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"), - category="error") - return redirect(url_for("web.index")) - - to_save = request.form.to_dict() - - try: - # Update folder of book on local disk - edited_books_id = None - title_author_error = None - # handle book title change - title_change = handle_title_on_edit(book, to_save["book_title"]) - # handle book author change - input_authors, author_change = handle_author_on_edit(book, to_save["author_name"]) - if author_change or title_change: - edited_books_id = book.id - modify_date = True - title_author_error = helper.update_dir_structure(edited_books_id, - config.get_book_path(), - input_authors[0]) - if title_author_error: - flash(title_author_error, category="error") - calibre_db.session.rollback() - book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) - - # handle upload other formats from local disk - meta = upload_single_file(request, book, book_id) - # only merge metadata if file was uploaded and no error occurred (meta equals not false or none) - upload_format = False - if meta: - upload_format = merge_metadata(to_save, meta) - # handle upload covers from local disk - cover_upload_success = upload_cover(request, book) - if cover_upload_success: - book.has_cover = 1 - modify_date = True - - # upload new covers or new file formats to google drive - if config.config_use_google_drive: - gdriveutils.updateGdriveCalibreFromLocal() - - if to_save.get("cover_url", None): - if not current_user.role_upload(): - edit_error = True - flash(_("User has no rights to upload cover"), category="error") - if to_save["cover_url"].endswith('/static/generic_cover.jpg'): - book.has_cover = 0 - else: - result, error = helper.save_cover_from_url(to_save["cover_url"].strip(), book.path) - if result is True: - book.has_cover = 1 - modify_date = True - helper.replace_cover_thumbnail_cache(book.id) - else: - flash(error, category="error") - - # Add default series_index to book - modify_date |= edit_book_series_index(to_save["series_index"], book) - # Handle book comments/description - modify_date |= edit_book_comments(Markup(to_save['description']).unescape(), book) - # Handle identifiers - input_identifiers = identifier_list(to_save, book) - modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) - if warning: - flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning") - modify_date |= modification - # Handle book tags - modify_date |= edit_book_tags(to_save['tags'], book) - # Handle book series - modify_date |= edit_book_series(to_save["series"], book) - # handle book publisher - modify_date |= edit_book_publisher(to_save['publisher'], book) - # handle book languages - try: - modify_date |= edit_book_languages(to_save['languages'], book, upload_format) - except ValueError as e: - flash(str(e), category="error") - edit_error = True - # handle book ratings - modify_date |= edit_book_ratings(to_save, book) - # handle cc data - modify_date |= edit_all_cc_data(book_id, book, to_save) - - if to_save.get("pubdate", None): - try: - book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d") - except ValueError as e: - book.pubdate = db.Books.DEFAULT_PUBDATE - flash(str(e), category="error") - edit_error = True - else: - book.pubdate = db.Books.DEFAULT_PUBDATE - - if modify_date: - book.last_modified = datetime.now(timezone.utc) - kobo_sync_status.remove_synced_book(edited_books_id, all=True) - calibre_db.set_metadata_dirty(book.id) - - calibre_db.session.merge(book) - calibre_db.session.commit() - if config.config_use_google_drive: - gdriveutils.updateGdriveCalibreFromLocal() - if meta is not False \ - and edit_error is not True \ - and title_author_error is not True \ - and cover_upload_success is not False: - flash(_("Metadata successfully updated"), category="success") - if "detail_view" in to_save: - return redirect(url_for('web.show_book', book_id=book.id)) - else: - return render_edit_book(book_id) - except ValueError as e: - log.error_or_exception("Error: {}".format(e)) - calibre_db.session.rollback() - flash(str(e), category="error") - return redirect(url_for('web.show_book', book_id=book.id)) - except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e: - log.error_or_exception("Database error: {}".format(e)) - calibre_db.session.rollback() - flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error") - return redirect(url_for('web.show_book', book_id=book.id)) - except Exception as ex: - log.error_or_exception(ex) - calibre_db.session.rollback() - flash(_("Error editing book: {}".format(ex)), category="error") - return redirect(url_for('web.show_book', book_id=book.id)) + return do_edit_book(book_id) @editbook.route("/upload", methods=["POST"]) @login_required_if_no_ano @upload_required def upload(): - if not config.config_uploading: - abort(404) - if request.method == 'POST' and 'btn-upload' in request.files: + if len(request.files.getlist("btn-upload-format")): + book_id = request.form.get('book_id', -1) + return do_edit_book(book_id, request.files.getlist("btn-upload-format")) + elif len(request.files.getlist("btn-upload")): for requested_file in request.files.getlist("btn-upload"): try: modify_date = False # create the function for sorting... - calibre_db.update_title_sort(config) - try: - # sqlalchemy 2.0 - uuid_func = calibre_db.session.connection().connection.driver_connection - except AttributeError: - uuid_func = calibre_db.session.connection().connection.connection - uuid_func.create_function('uuid4', 0,lambda: str(uuid4())) + calibre_db.create_functions(config) meta, error = file_handling_on_upload(requested_file) if error: return error @@ -279,9 +140,7 @@ def upload(): input_authors[0], meta.file_path, title_dir + meta.extension.lower()) - move_coverfile(meta, db_book) - if modify_date: calibre_db.set_metadata_dirty(book_id) # save data to database, reread data @@ -309,6 +168,7 @@ def upload(): flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error") return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + abort(404) @editbook.route("/admin/book/convert/", methods=['POST']) @@ -575,23 +435,166 @@ def table_xchange_author_title(): return "" -def merge_metadata(to_save, meta): - if to_save.get('author_name', "") == _('Unknown'): - to_save['author_name'] = '' - if to_save.get('book_title', "") == _('Unknown'): - to_save['book_title'] = '' - if not to_save["languages"] and meta.languages: - upload_language = True - else: - upload_language = False +def do_edit_book(book_id, upload_formats=None): + modify_date = False + edit_error = False + + # create the function for sorting... + calibre_db.create_functions(config) + + book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) + # Book not found + if not book: + flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"), + category="error") + return redirect(url_for("web.index")) + + to_save = request.form.to_dict() + + try: + # Update folder of book on local disk + edited_books_id = None + title_author_error = None + # upload_mode = False + # handle book title change + if "title" in to_save: + title_change = handle_title_on_edit(book, to_save["title"]) + # handle book author change + if not upload_formats: + input_authors, author_change = handle_author_on_edit(book, to_save["authors"]) + if author_change or title_change: + edited_books_id = book.id + modify_date = True + title_author_error = helper.update_dir_structure(edited_books_id, + config.get_book_path(), + input_authors[0]) + if title_author_error: + flash(title_author_error, category="error") + calibre_db.session.rollback() + book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) + + # handle book ratings + modify_date |= edit_book_ratings(to_save, book) + else: + # handle upload other formats from local disk + to_save, edit_error = upload_book_formats(upload_formats, book, book_id, book.has_cover) + # handle upload covers from local disk + cover_upload_success = upload_cover(request, book) + if cover_upload_success or to_save.get("format_cover"): + book.has_cover = 1 + modify_date = True + + # upload new covers or new file formats to google drive + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + + if to_save.get("cover_url",): + if not current_user.role_upload(): + edit_error = True + flash(_("User has no rights to upload cover"), category="error") + if to_save["cover_url"].endswith('/static/generic_cover.jpg'): + book.has_cover = 0 + else: + result, error = helper.save_cover_from_url(to_save["cover_url"].strip(), book.path) + if result is True: + book.has_cover = 1 + modify_date = True + helper.replace_cover_thumbnail_cache(book.id) + else: + flash(error, category="error") + + # Add default series_index to book + modify_date |= edit_book_series_index(to_save.get("series_index"), book) + # Handle book comments/description + modify_date |= edit_book_comments(Markup(to_save.get('comments')).unescape(), book) + # Handle identifiers + input_identifiers = identifier_list(to_save, book) + modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) + if warning: + flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning") + modify_date |= modification + # Handle book tags + modify_date |= edit_book_tags(to_save.get('tags'), book) + # Handle book series + modify_date |= edit_book_series(to_save.get("series"), book) + # handle book publisher + modify_date |= edit_book_publisher(to_save.get('publisher'), book) + # handle book languages + try: + invalid = [] + modify_date |= edit_book_languages(to_save.get('languages'), book, upload_mode=upload_formats, + invalid=invalid) + if invalid: + for lang in invalid: + flash(_("'%(langname)s' is not a valid language", langname=lang), category="warning") + except ValueError as e: + flash(str(e), category="error") + edit_error = True + # handle cc data + modify_date |= edit_all_cc_data(book_id, book, to_save) + + if to_save.get("pubdate") is not None: + if to_save.get("pubdate"): + try: + book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d") + except ValueError as e: + book.pubdate = db.Books.DEFAULT_PUBDATE + flash(str(e), category="error") + edit_error = True + else: + book.pubdate = db.Books.DEFAULT_PUBDATE + + if modify_date: + book.last_modified = datetime.now(timezone.utc) + kobo_sync_status.remove_synced_book(edited_books_id, all=True) + calibre_db.set_metadata_dirty(book.id) + + calibre_db.session.merge(book) + calibre_db.session.commit() + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + if edit_error is not True and title_author_error is not True and cover_upload_success is not False: + flash(_("Metadata successfully updated"), category="success") + + if upload_formats: + resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)} + return Response(json.dumps(resp), mimetype='application/json') + + if "detail_view" in to_save: + return redirect(url_for('web.show_book', book_id=book.id)) + else: + return render_edit_book(book_id) + except ValueError as e: + log.error_or_exception("Error: {}".format(e)) + calibre_db.session.rollback() + flash(str(e), category="error") + return redirect(url_for('web.show_book', book_id=book.id)) + except (OperationalError, IntegrityError, StaleDataError, InterfaceError) as e: + log.error_or_exception("Database error: {}".format(e)) + calibre_db.session.rollback() + flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error") + return redirect(url_for('web.show_book', book_id=book.id)) + except Exception as ex: + log.error_or_exception(ex) + calibre_db.session.rollback() + flash(_("Error editing book: {}".format(ex)), category="error") + return redirect(url_for('web.show_book', book_id=book.id)) + + +def merge_metadata(book, meta, to_save): + if meta.cover: + to_save['cover_format'] = meta.cover for s_field, m_field in [ - ('tags', 'tags'), ('author_name', 'author'), ('series', 'series'), + ('tags', 'tags'), ('authors', 'author'), ('series', 'series'), ('series_index', 'series_id'), ('languages', 'languages'), - ('book_title', 'title')]: - to_save[s_field] = to_save[s_field] or getattr(meta, m_field, '') - to_save["description"] = to_save["description"] or Markup( - getattr(meta, 'description', '')).unescape() - return upload_language + ('title', 'title'), ('comments', 'description')]: + try: + val = None if len(getattr(book, s_field)) else getattr(meta, m_field, '') + except TypeError: + val = None if len(str(getattr(book, s_field))) else getattr(meta, m_field, '') + if val: + to_save[s_field] = val + def identifier_list(to_save, book): """Generate a list of Identifiers from form information""" @@ -1002,84 +1005,93 @@ def edit_book_ratings(to_save, book): def edit_book_tags(tags, book): - input_tags = tags.split(',') - input_tags = list(map(lambda it: strip_whitespaces(it), input_tags)) - # Remove duplicates - input_tags = helper.uniq(input_tags) - return modify_database_object(input_tags, book.tags, db.Tags, calibre_db.session, 'tags') - + if tags is not None: + input_tags = tags.split(',') + input_tags = list(map(lambda it: strip_whitespaces(it), input_tags)) + # Remove duplicates + input_tags = helper.uniq(input_tags) + return modify_database_object(input_tags, book.tags, db.Tags, calibre_db.session, 'tags') + return False def edit_book_series(series, book): - input_series = [strip_whitespaces(series)] - input_series = [x for x in input_series if x != ''] - return modify_database_object(input_series, book.series, db.Series, calibre_db.session, 'series') + if series is not None: + input_series = [strip_whitespaces(series)] + input_series = [x for x in input_series if x != ''] + return modify_database_object(input_series, book.series, db.Series, calibre_db.session, 'series') + return False def edit_book_series_index(series_index, book): - # Add default series_index to book - modify_date = False - series_index = series_index or '1' - if not series_index.replace('.', '', 1).isdigit(): - flash(_("Seriesindex: %(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning") - return False - if str(book.series_index) != series_index: - book.series_index = series_index - modify_date = True - return modify_date + if series_index: + # Add default series_index to book + modify_date = False + series_index = series_index or '1' + if not series_index.replace('.', '', 1).isdigit(): + flash(_("Seriesindex: %(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning") + return False + if str(book.series_index) != series_index: + book.series_index = series_index + modify_date = True + return modify_date + return False # Handle book comments/description def edit_book_comments(comments, book): - modify_date = False - if comments: - comments = clean_string(comments, book.id) - if len(book.comments): - if book.comments[0].text != comments: - book.comments[0].text = comments - modify_date = True - else: + if comments is not None: + modify_date = False if comments: - book.comments.append(db.Comments(comment=comments, book=book.id)) - modify_date = True - return modify_date + comments = clean_string(comments, book.id) + if len(book.comments): + if book.comments[0].text != comments: + book.comments[0].text = comments + modify_date = True + else: + if comments: + book.comments.append(db.Comments(comment=comments, book=book.id)) + modify_date = True + return modify_date def edit_book_languages(languages, book, upload_mode=False, invalid=None): - input_languages = languages.split(',') - unknown_languages = [] - if not upload_mode: - input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages) - else: - input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages) - for lang in unknown_languages: - log.error("'%s' is not a valid language", lang) - if isinstance(invalid, list): - invalid.append(lang) + if languages is not None: + input_languages = languages.split(',') + unknown_languages = [] + if not upload_mode: + input_l = isoLanguages.get_language_code_from_name(get_locale(), input_languages, unknown_languages) else: - raise ValueError(_("'%(langname)s' is not a valid language", langname=lang)) - # ToDo: Not working correct - if upload_mode and len(input_l) == 1: - # If the language of the file is excluded from the users view, it's not imported, to allow the user to view - # the book it's language is set to the filter language - if input_l[0] != current_user.filter_language() and current_user.filter_language() != "all": - input_l[0] = calibre_db.session.query(db.Languages). \ - filter(db.Languages.lang_code == current_user.filter_language()).first().lang_code - # Remove duplicates - input_l = helper.uniq(input_l) - return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages') + input_l = isoLanguages.get_valid_language_codes_from_code(get_locale(), input_languages, unknown_languages) + for lang in unknown_languages: + log.error("'%s' is not a valid language", lang) + if isinstance(invalid, list): + invalid.append(lang) + else: + raise ValueError(_("'%(langname)s' is not a valid language", langname=lang)) + # ToDo: Not working correct + if upload_mode and len(input_l) == 1: + # If the language of the file is excluded from the users view, it's not imported, to allow the user to view + # the book it's language is set to the filter language + if input_l[0] != current_user.filter_language() and current_user.filter_language() != "all": + input_l[0] = calibre_db.session.query(db.Languages). \ + filter(db.Languages.lang_code == current_user.filter_language()).first().lang_code + # Remove duplicates from normalized langcodes + input_l = helper.uniq(input_l) + return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages') + return False def edit_book_publisher(publishers, book): - changed = False - if publishers: - publisher = strip_whitespaces(publishers) - if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name): - changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session, - 'publisher') - elif len(book.publishers): - changed |= modify_database_object([], book.publishers, db.Publishers, calibre_db.session, 'publisher') - return changed - + if publishers is not None: + changed = False + if publishers: + publisher = strip_whitespaces(publishers) + if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name): + changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session, + 'publisher') + elif len(book.publishers): + changed |= modify_database_object([], book.publishers, db.Publishers, calibre_db.session, 'publisher') + return changed + return False def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string): changed = False @@ -1160,61 +1172,66 @@ def edit_cc_data(book_id, book, to_save, cc): changed = False for c in cc: cc_string = "custom_column_" + str(c.id) - if not c.is_multiple: - if len(getattr(book, cc_string)) > 0: - cc_db_value = getattr(book, cc_string)[0].value - else: - cc_db_value = None - if strip_whitespaces(to_save[cc_string]): - if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]: - change, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string) + if to_save.get(cc_string) is not None: + if not c.is_multiple: + if len(getattr(book, cc_string)) > 0: + cc_db_value = getattr(book, cc_string)[0].value else: - change, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string) - changed |= change + cc_db_value = None + if strip_whitespaces(to_save[cc_string]): + if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]: + change, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string) + else: + change, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string) + changed |= change + else: + if cc_db_value is not None: + # remove old cc_val + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if not del_cc.books or len(del_cc.books) == 0: + calibre_db.session.delete(del_cc) + changed = True else: - if cc_db_value is not None: - # remove old cc_val - del_cc = getattr(book, cc_string)[0] - getattr(book, cc_string).remove(del_cc) - if not del_cc.books or len(del_cc.books) == 0: - calibre_db.session.delete(del_cc) - changed = True - else: - input_tags = to_save[cc_string].split(',') - input_tags = list(map(lambda it: strip_whitespaces(it), input_tags)) - changed |= modify_database_object(input_tags, - getattr(book, cc_string), - db.cc_classes[c.id], - calibre_db.session, - 'custom') + input_tags = to_save[cc_string].split(',') + input_tags = list(map(lambda it: strip_whitespaces(it), input_tags)) + changed |= modify_database_object(input_tags, + getattr(book, cc_string), + db.cc_classes[c.id], + calibre_db.session, + 'custom') return changed -# returns None if no file is uploaded -# returns False if an error occurs, in all other cases the ebook metadata is returned -def upload_single_file(file_request, book, book_id): +# returns False if an error occurs or no book is uploaded, in all other cases the ebook metadata to change is returned +def upload_book_formats(requested_files, book, book_id, no_cover=True): # Check and handle Uploaded file - requested_file = file_request.files.get('btn-upload-format', None) + to_save = dict() + error = False allowed_extensions = config.config_upload_formats.split(',') - if requested_file: + for requested_file in requested_files: + current_filename = requested_file.filename if config.config_check_extensions and allowed_extensions != ['']: if not validate_mime_type(requested_file, allowed_extensions): flash(_("File type isn't allowed to be uploaded to this server"), category="error") - return False - # check for empty request - if requested_file.filename != '': + error = True + continue + if current_filename != '': if not current_user.role_upload(): flash(_("User has no rights to upload additional file formats"), category="error") - return False - if '.' in requested_file.filename: - file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() + error = True + continue + if '.' in current_filename: + file_ext = current_filename.rsplit('.', 1)[-1].lower() if file_ext not in allowed_extensions and '' not in allowed_extensions: flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), category="error") - return False + error = True + continue else: flash(_('File to be uploaded must have an extension'), category="error") - return False + error = True + continue file_name = book.path.rsplit('/', 1)[-1] filepath = os.path.normpath(os.path.join(config.get_book_path(), book.path)) @@ -1227,41 +1244,50 @@ def upload_single_file(file_request, book, book_id): except OSError: flash(_("Failed to create path %(path)s (Permission denied).", path=filepath), category="error") - return False + error = True + continue try: requested_file.save(saved_filename) except OSError: flash(_("Failed to store file %(file)s.", file=saved_filename), category="error") - return False + error = True + continue file_size = os.path.getsize(saved_filename) - is_format = calibre_db.get_book_format(book_id, file_ext.upper()) # Format entry already exists, no need to update the database - if is_format: + if calibre_db.get_book_format(book_id, file_ext.upper()): log.warning('Book format %s already existing', file_ext.upper()) else: try: db_format = db.Data(book_id, file_ext.upper(), file_size, file_name) calibre_db.session.add(db_format) calibre_db.session.commit() - calibre_db.update_title_sort(config) + calibre_db.create_functions(config) except (OperationalError, IntegrityError, StaleDataError) as e: calibre_db.session.rollback() log.error_or_exception("Database error: {}".format(e)) flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), category="error") - return False # return redirect(url_for('web.show_book', book_id=book.id)) + error = True + continue # Queue uploader info link = '{}'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) upload_text = N_("File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=link) WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title))) - - return uploader.process( - saved_filename, *os.path.splitext(requested_file.filename), - rar_executable=config.config_rarfile_location) - return None + meta = uploader.process( + saved_filename, + *os.path.splitext(current_filename), + rar_executable=config.config_rarfile_location, + no_cover=no_cover) + merge_metadata(book, meta, to_save) + #if to_save.get('languages'): + # langs = [] + # for lang_code in to_save['languages'].split(','): + # langs.append(isoLanguages.get_language_name(get_locale(), lang_code)) + # to_save['languages'] = ",".join(langs) + return to_save, error def upload_cover(cover_request, book): @@ -1295,7 +1321,6 @@ def handle_title_on_edit(book, book_title): def handle_author_on_edit(book, author_name, update_stored=True): change = False - # handle author(s) input_authors = prepare_authors(author_name, config.get_book_path(), config.config_use_google_drive) # Search for each author if author is in database, if not, author name and sorted author name is generated new @@ -1325,7 +1350,6 @@ def search_objects_remove(db_book_object, db_type, input_elements): if db_type == 'custom': type_elements = c_elements.value else: - # type_elements = c_elements.name type_elements = c_elements for inp_element in input_elements: if type_elements == inp_element: diff --git a/cps/epub.py b/cps/epub.py index e84822f3d..626e68eea 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -66,7 +66,7 @@ def get_epub_layout(book, book_data): return layout[0] -def get_epub_info(tmp_file_path, original_file_name, original_file_extension): +def get_epub_info(tmp_file_path, original_file_name, original_file_extension, no_cover_processing): ns = { 'n': 'urn:oasis:names:tc:opendocument:xmlns:container', 'pkg': 'http://www.idpf.org/2007/opf', @@ -117,7 +117,10 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): epub_metadata = parse_epub_series(ns, tree, epub_metadata) epub_zip = zipfile.ZipFile(tmp_file_path) - cover_file = parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path) + if not no_cover_processing: + cover_file = parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path) + else: + cover_file = None identifiers = [] for node in p.xpath('dc:identifier', namespaces=ns): diff --git a/cps/helper.py b/cps/helper.py index 39c9c3845..c2fc9c237 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -328,7 +328,7 @@ def edit_book_read_status(book_id, read_status=None): ub.session_commit("Book {} readbit toggled".format(book_id)) else: try: - calibre_db.update_title_sort(config) + calibre_db.create_functions(config) book = calibre_db.get_filtered_book(book_id) book_read_status = getattr(book, 'custom_column_' + str(config.config_read_column)) if len(book_read_status): diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py index 574736582..bbab70893 100644 --- a/cps/isoLanguages.py +++ b/cps/isoLanguages.py @@ -15,24 +15,30 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import sys from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES from . import logger +from .string_helper import strip_whitespaces log = logger.create() try: - from iso639 import languages, __version__ + from iso639 import languages + # iso_version = importlib.metadata.version("iso639") get = languages.get -except ImportError: - from pycountry import languages as pyc_languages try: - import pkg_resources - __version__ = pkg_resources.get_distribution('pycountry').version + ' (PyCountry)' - del pkg_resources - except (ImportError, Exception): - __version__ = "? (PyCountry)" + if sys.version_info >= (3, 12): + import pkg_resources + except ImportError: + print("Python 3.12 isn't compatible with iso-639. Please install pycountry.") +except ImportError as ex: + from pycountry import languages as pyc_languages + #try: + # iso_version = importlib.metadata.version("pycountry") + ' (PyCountry)' + #except (ImportError, Exception): + # iso_version = "?" + ' (PyCountry)' def _copy_fields(l): l.part1 = getattr(l, 'alpha_2', None) @@ -69,20 +75,20 @@ def get_language_name(locale, lang_code): return name -def get_language_codes(locale, language_names, remainder=None): - language_names = set(x.strip().lower() for x in language_names if x) +def get_language_code_from_name(locale, language_names, remainder=None): + language_names = set(strip_whitespaces(x).lower() for x in language_names if x) lang = list() - for k, v in get_language_names(locale).items(): - v = v.lower() - if v in language_names: - lang.append(k) - language_names.remove(v) + for key, val in get_language_names(locale).items(): + val = val.lower() + if val in language_names: + lang.append(key) + language_names.remove(val) if remainder is not None and language_names: remainder.extend(language_names) return lang -def get_valid_language_codes(locale, language_names, remainder=None): +def get_valid_language_codes_from_code(locale, language_names, remainder=None): lang = list() if "" in language_names: language_names.remove("") diff --git a/cps/jinjia.py b/cps/jinjia.py index 01abf5a6c..39c7f20cb 100644 --- a/cps/jinjia.py +++ b/cps/jinjia.py @@ -27,7 +27,7 @@ import datetime import mimetypes from uuid import uuid4 -from flask import Blueprint, request, url_for +from flask import Blueprint, request, url_for, g from flask_babel import format_date from .cw_login import current_user @@ -182,3 +182,12 @@ def get_cover_srcset(series): url = url_for('web.get_series_cover', series_id=series.id, resolution=shortname, c=cache_timestamp()) srcset.append(f'{url} {resolution}x') return ', '.join(srcset) + + +@jinjia.app_template_filter('music') +def contains_music(book_formats): + result = False + for format in book_formats: + if format.format.lower() in g.constants.EXTENSIONS_AUDIO: + result = True + return result diff --git a/cps/search.py b/cps/search.py index da74984b6..8cdb9b4a7 100644 --- a/cps/search.py +++ b/cps/search.py @@ -244,7 +244,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None): pagination = None cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) - calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase) + calibre_db.create_functions() + # calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase) query = calibre_db.generate_linked_query(config.config_read_column, db.Books) q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\ .outerjoin(db.Series)\ @@ -257,8 +258,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None): tags['include_' + element] = term.get('include_' + element) tags['exclude_' + element] = term.get('exclude_' + element) - author_name = term.get("author_name") - book_title = term.get("book_title") + author_name = term.get("authors") + book_title = term.get("title") publisher = term.get("publisher") pub_start = term.get("publishstart") pub_end = term.get("publishend") diff --git a/cps/static/js/edit_books.js b/cps/static/js/edit_books.js index c1eb319d4..46cc238c9 100644 --- a/cps/static/js/edit_books.js +++ b/cps/static/js/edit_books.js @@ -3,9 +3,9 @@ */ /* global Bloodhound, language, Modernizr, tinymce, getPath */ -if ($("#description").length) { +if ($("#comments").length) { tinymce.init({ - selector: "#description", + selector: "#comments", plugins: 'code', branding: false, menubar: "edit view format", @@ -93,7 +93,7 @@ var authors = new Bloodhound({ }, }); -$(".form-group #bookAuthor").typeahead( +$(".form-group #authors").typeahead( { highlight: true, minLength: 1, @@ -243,13 +243,13 @@ $("#search").on("change input.typeahead:selected", function(event) { }); }); -$("#btn-upload-format").on("change", function () { +/*$("#btn-upload-format").on("change", function () { var filename = $(this).val(); if (filename.substring(3, 11) === "fakepath") { filename = filename.substring(12); } // Remove c:\fake at beginning from localhost chrome $("#upload-format").text(filename); -}); +});*/ $("#btn-upload-cover").on("change", function () { var filename = $(this).val(); @@ -261,8 +261,8 @@ $("#btn-upload-cover").on("change", function () { $("#xchange").click(function () { this.blur(); - var title = $("#book_title").val(); - $("#book_title").val($("#bookAuthor").val()); - $("#bookAuthor").val(title); + var title = $("#title").val(); + $("#title").val($("#authors").val()); + $("#authors").val(title); }); diff --git a/cps/static/js/get_meta.js b/cps/static/js/get_meta.js index 43a40fa65..df4119dca 100644 --- a/cps/static/js/get_meta.js +++ b/cps/static/js/get_meta.js @@ -38,12 +38,12 @@ $(function () { } function populateForm (book) { - tinymce.get("description").setContent(book.description); + tinymce.get("comments").setContent(book.description); var uniqueTags = getUniqueValues('tags', book) var uniqueLanguages = getUniqueValues('languages', book) var ampSeparatedAuthors = (book.authors || []).join(" & "); - $("#bookAuthor").val(ampSeparatedAuthors); - $("#book_title").val(book.title); + $("#authors").val(ampSeparatedAuthors); + $("#title").val(book.title); $("#tags").val(uniqueTags.join(", ")); $("#languages").val(uniqueLanguages.join(", ")); $("#rating").data("rating").setValue(Math.round(book.rating)); @@ -172,7 +172,7 @@ $(function () { $("#get_meta").click(function () { populate_provider(); - var bookTitle = $("#book_title").val(); + var bookTitle = $("#title").val(); $("#keyword").val(bookTitle); keyword = bookTitle; doSearch(bookTitle); diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 2dbea6bc2..d04a04599 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -130,8 +130,13 @@ $(".container-fluid").bind('drop', function (e) { } }); if (dt.files.length) { - $("#btn-upload")[0].files = dt.files; - $("#form-upload").submit(); + if($("#btn-upload-format").length) { + $("#btn-upload-format")[0].files = dt.files; + $("#form-upload-format").submit(); + } else { + $("#btn-upload")[0].files = dt.files; + $("#form-upload").submit(); + } } } }); @@ -140,14 +145,28 @@ $("#btn-upload").change(function() { $("#form-upload").submit(); }); -$("#form-upload").uploadprogress({ - redirect_url: getPath() + "/", //"{{ url_for('web.index')}}", - uploadedMsg: $("#form-upload").data("message"), //"{{_('Upload done, processing, please wait...')}}", - modalTitle: $("#form-upload").data("title"), //"{{_('Uploading...')}}", - modalFooter: $("#form-upload").data("footer"), //"{{_('Close')}}", - modalTitleFailed: $("#form-upload").data("failed") //"{{_('Error')}}" +$("#btn-upload-format").change(function() { + $("#form-upload-format").submit(); }); + +$("#form-upload").uploadprogress({ + redirect_url: getPath() + "/", + uploadedMsg: $("#form-upload").data("message"), + modalTitle: $("#form-upload").data("title"), + modalFooter: $("#form-upload").data("footer"), + modalTitleFailed: $("#form-upload").data("failed") +}); + +$("#form-upload-format").uploadprogress({ + redirect_url: getPath() + "/", + uploadedMsg: $("#form-upload-format").data("message"), + modalTitle: $("#form-upload-format").data("title"), + modalFooter: $("#form-upload-format").data("footer"), + modalTitleFailed: $("#form-upload-format").data("failed") +}); + + $(document).ready(function() { var inp = $('#query').first() if (inp.length) { diff --git a/cps/tasks/thumbnail.py b/cps/tasks/thumbnail.py index 884ae0c30..a7658c02f 100644 --- a/cps/tasks/thumbnail.py +++ b/cps/tasks/thumbnail.py @@ -201,6 +201,7 @@ class TaskGenerateCoverThumbnails(CalibreTask): with open(filename, 'wb') as fd: copyfileobj(stream, fd) + except Exception as ex: # Bubble exception to calling function self.log.debug('Error generating thumbnail file: ' + str(ex)) diff --git a/cps/templates/author.html b/cps/templates/author.html index f7314586d..1e02597c2 100644 --- a/cps/templates/author.html +++ b/cps/templates/author.html @@ -62,11 +62,9 @@ {{author.name.replace('|',',')|shortentitle(30)}} {% endif %} {% endfor %} - {% for format in entry.Books.data %} - {% if format.format|lower in g.constants.EXTENSIONS_AUDIO %} + {% if entry.Books.data|music %} - {% endif %} - {% endfor %} + {% endif %}

{% if entry.Books.series.__len__() > 0 %}

diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 57fe701c7..94dbbb3bf 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -47,26 +47,41 @@ {% endif %} +{% if current_user.role_upload() and g.allow_upload %} +

+
+
+ + +
+ +
+ +
+
+
+{% endif %} +
- - + +
- +
- - + +
@@ -196,13 +211,6 @@
{% endfor %} {% endif %} - {% if current_user.role_upload() and g.allow_upload %} -
- -
- -
- {% endif %}