1
0
mirror of https://github.com/janeczku/calibre-web synced 2025-01-26 00:46:55 +00:00

Merge branch 'Develop':

Better handling of incompatible iso-639 module on python3.12
Music icon only visible once if more than one audio format available
Upload (multiple) book formats with progress and merge the corresponding metadata into the book (also via drag'n drop #2252)
This commit is contained in:
Ozzie Isaacs 2024-08-21 18:44:48 +02:00
commit 6a504673e5
27 changed files with 983 additions and 1174 deletions

View File

@ -27,7 +27,6 @@ import importlib
from collections import OrderedDict from collections import OrderedDict
import flask import flask
import jinja2
from flask_babel import gettext as _ from flask_babel import gettext as _
from . import db, calibre_db, converter, uploader, constants, dep_check from . import db, calibre_db, converter, uploader, constants, dep_check

View File

@ -567,7 +567,7 @@ def update_view_configuration():
_config_string(to_save, "config_calibre_web_title") _config_string(to_save, "config_calibre_web_title")
_config_string(to_save, "config_columns_to_ignore") _config_string(to_save, "config_columns_to_ignore")
if _config_string(to_save, "config_title_regex"): 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")): if not check_valid_read_column(to_save.get("config_read_column", "0")):
flash(_("Invalid Read Column"), category="error") flash(_("Invalid Read Column"), category="error")

View File

@ -26,7 +26,7 @@ from cps.constants import BookMeta
log = logger.create() 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 tmp_cover_name = None
audio_file = mutagen.File(tmp_file_path) audio_file = mutagen.File(tmp_file_path)
comments = None 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 pubdate = str(audio_file.tags.get('TDRC').text[0]) if "TDRC" in audio_file.tags else None
if not pubdate: if not pubdate:
pubdate = str(audio_file.tags.get('TDOR').text[0]) if "TDOR" in audio_file.tags else None 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') tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
cover_info = cover_data[0] cover_info = cover_data[0]
for dat in cover_data: 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 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 pubdate = audio_file.tags.get('DATE')[0] if "DATE" in audio_file else None
cover_data = audio_file.tags.get('METADATA_BLOCK_PICTURE') cover_data = audio_file.tags.get('METADATA_BLOCK_PICTURE')
if cover_data: if not no_cover_processing:
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') if cover_data:
cover_info = mutagen.flac.Picture(base64.b64decode(cover_data[0])) 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:]) cover_info = mutagen.flac.Picture(base64.b64decode(cover_data[0]))
if hasattr(audio_file, "pictures"): cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
cover_info = audio_file.pictures[0] if hasattr(audio_file, "pictures"):
for dat in audio_file.pictures: cover_info = audio_file.pictures[0]
if dat.type == mutagen.id3.PictureType.COVER_FRONT: for dat in audio_file.pictures:
cover_info = dat if dat.type == mutagen.id3.PictureType.COVER_FRONT:
break cover_info = dat
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg') break
cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:]) 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"]: elif original_file_extension in [".aac"]:
title = audio_file.tags.get('Title').value if "Title" in audio_file else None 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 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 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 pubdate = audio_file.tags.get('Year').value if "Year" in audio_file else None
cover_data = audio_file.tags['Cover Art (Front)'] 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') tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
with open(tmp_cover_name, "wb") as cover_file: with open(tmp_cover_name, "wb") as cover_file:
cover_file.write(cover_data.value.split(b"\x00",1)[1]) 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 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 pubdate = audio_file.tags.get('Year')[0].value if "Year" in audio_file else None
cover_data = audio_file.tags.get('WM/Picture', 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') tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
with open(tmp_cover_name, "wb") as cover_file: with open(tmp_cover_name, "wb") as cover_file:
cover_file.write(cover_data[0].value) 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 = "" publisher = ""
pubdate = audio_file.tags.get('©day')[0] if "©day" in audio_file.tags else None pubdate = audio_file.tags.get('©day')[0] if "©day" in audio_file.tags else None
cover_data = audio_file.tags.get('covr', 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') tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
cover_type = None cover_type = None
for c in cover_data: for c in cover_data:

View File

@ -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) 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: if use_comic_meta:
try: try:
archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable) 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 "" lang = loaded_metadata.language or ""
loaded_metadata.language = isoLanguages.get_lang3(lang) 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( return BookMeta(
file_path=tmp_file_path, file_path=tmp_file_path,
extension=original_file_extension, extension=original_file_extension,
title=loaded_metadata.title or original_file_name, title=loaded_metadata.title or original_file_name,
author=" & ".join([credit["person"] author=" & ".join([credit["person"]
for credit in loaded_metadata.credits if credit["role"] == "Writer"]) or 'Unknown', 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 "", description=loaded_metadata.comments or "",
tags="", tags="",
series=loaded_metadata.series or "", series=loaded_metadata.series or "",
@ -171,13 +174,17 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
publisher="", publisher="",
pubdate="", pubdate="",
identifiers=[]) identifiers=[])
if not no_cover_processing:
cover_file = _extract_cover(tmp_file_path, original_file_extension, rar_executable)
else:
cover_file = None
return BookMeta( return BookMeta(
file_path=tmp_file_path, file_path=tmp_file_path,
extension=original_file_extension, extension=original_file_extension,
title=original_file_name, title=original_file_name,
author='Unknown', author='Unknown',
cover=_extract_cover(tmp_file_path, original_file_extension, rar_executable), cover=cover_file,
description="", description="",
tags="", tags="",
series="", series="",

View File

@ -24,6 +24,7 @@ from datetime import datetime, timezone
from urllib.parse import quote from urllib.parse import quote
import unidecode import unidecode
from weakref import WeakSet from weakref import WeakSet
from uuid import uuid4
from sqlite3 import OperationalError as sqliteOperationalError from sqlite3 import OperationalError as sqliteOperationalError
from sqlalchemy import create_engine from sqlalchemy import create_engine
@ -533,7 +534,7 @@ class CalibreDB:
def init_session(self, expire_on_commit=True): def init_session(self, expire_on_commit=True):
self.session = self.session_factory() self.session = self.session_factory()
self.session.expire_on_commit = expire_on_commit self.session.expire_on_commit = expire_on_commit
self.update_title_sort(self.config) self.create_functions(self.config)
@classmethod @classmethod
def setup_db_cc_classes(cls, cc): def setup_db_cc_classes(cls, cc):
@ -901,7 +902,8 @@ class CalibreDB:
def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()): def get_typeahead(self, database, query, replace=('', ''), tag_filter=true()):
query = query or '' 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). \ entries = self.session.query(database).filter(tag_filter). \
filter(func.lower(database.name).ilike("%" + query + "%")).all() filter(func.lower(database.name).ilike("%" + query + "%")).all()
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries]) # json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
@ -909,7 +911,8 @@ class CalibreDB:
return json_dumps return json_dumps
def check_exists_book(self, authr, title): 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() q = list()
author_terms = re.split(r'\s*&\s*', authr) author_terms = re.split(r'\s*&\s*', authr)
for author_term in author_terms: for author_term in author_terms:
@ -920,7 +923,8 @@ class CalibreDB:
def search_query(self, term, config, *join): def search_query(self, term, config, *join):
strip_whitespaces(term).lower() 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() q = list()
author_terms = re.split("[, ]+", term) author_terms = re.split("[, ]+", term)
for author_term in author_terms: for author_term in author_terms:
@ -1018,7 +1022,7 @@ class CalibreDB:
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code) lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
return sorted(languages, key=lambda x: x.name, reverse=reverse_order) 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.) # user defined sort function for calibre databases (Series, etc.)
def _title_sort(title): def _title_sort(title):
# calibre sort stuff # calibre sort stuff
@ -1031,12 +1035,15 @@ class CalibreDB:
try: try:
# sqlalchemy <1.4.24 and sqlalchemy 2.0 # 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: except AttributeError:
# sqlalchemy >1.4.24 # sqlalchemy >1.4.24
conn = conn or self.session.connection().connection.connection conn = self.session.connection().connection.connection
try: 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: except sqliteOperationalError:
pass pass

View File

@ -24,7 +24,7 @@ import os
from datetime import datetime, timezone from datetime import datetime, timezone
import json import json
from shutil import copyfile from shutil import copyfile
from uuid import uuid4
from markupsafe import escape, Markup # dependency of flask from markupsafe import escape, Markup # dependency of flask
from functools import wraps from functools import wraps
@ -97,161 +97,22 @@ def show_edit_book(book_id):
@login_required_if_no_ano @login_required_if_no_ano
@edit_required @edit_required
def edit_book(book_id): def edit_book(book_id):
modify_date = False return do_edit_book(book_id)
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))
@editbook.route("/upload", methods=["POST"]) @editbook.route("/upload", methods=["POST"])
@login_required_if_no_ano @login_required_if_no_ano
@upload_required @upload_required
def upload(): def upload():
if not config.config_uploading: if len(request.files.getlist("btn-upload-format")):
abort(404) book_id = request.form.get('book_id', -1)
if request.method == 'POST' and 'btn-upload' in request.files: 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"): for requested_file in request.files.getlist("btn-upload"):
try: try:
modify_date = False modify_date = False
# create the function for sorting... # create the function for sorting...
calibre_db.update_title_sort(config) calibre_db.create_functions(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()))
meta, error = file_handling_on_upload(requested_file) meta, error = file_handling_on_upload(requested_file)
if error: if error:
return error return error
@ -279,9 +140,7 @@ def upload():
input_authors[0], input_authors[0],
meta.file_path, meta.file_path,
title_dir + meta.extension.lower()) title_dir + meta.extension.lower())
move_coverfile(meta, db_book) move_coverfile(meta, db_book)
if modify_date: if modify_date:
calibre_db.set_metadata_dirty(book_id) calibre_db.set_metadata_dirty(book_id)
# save data to database, reread data # 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), flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
category="error") category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
abort(404)
@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST']) @editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
@ -575,23 +435,166 @@ def table_xchange_author_title():
return "" return ""
def merge_metadata(to_save, meta): def do_edit_book(book_id, upload_formats=None):
if to_save.get('author_name', "") == _('Unknown'): modify_date = False
to_save['author_name'] = '' edit_error = False
if to_save.get('book_title', "") == _('Unknown'):
to_save['book_title'] = '' # create the function for sorting...
if not to_save["languages"] and meta.languages: calibre_db.create_functions(config)
upload_language = True
else: book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
upload_language = False # 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 [ 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'), ('series_index', 'series_id'), ('languages', 'languages'),
('book_title', 'title')]: ('title', 'title'), ('comments', 'description')]:
to_save[s_field] = to_save[s_field] or getattr(meta, m_field, '') try:
to_save["description"] = to_save["description"] or Markup( val = None if len(getattr(book, s_field)) else getattr(meta, m_field, '')
getattr(meta, 'description', '')).unescape() except TypeError:
return upload_language 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): def identifier_list(to_save, book):
"""Generate a list of Identifiers from form information""" """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): def edit_book_tags(tags, book):
input_tags = tags.split(',') if tags is not None:
input_tags = list(map(lambda it: strip_whitespaces(it), input_tags)) input_tags = tags.split(',')
# Remove duplicates input_tags = list(map(lambda it: strip_whitespaces(it), input_tags))
input_tags = helper.uniq(input_tags) # Remove duplicates
return modify_database_object(input_tags, book.tags, db.Tags, calibre_db.session, 'tags') 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): def edit_book_series(series, book):
input_series = [strip_whitespaces(series)] if series is not None:
input_series = [x for x in input_series if x != ''] input_series = [strip_whitespaces(series)]
return modify_database_object(input_series, book.series, db.Series, calibre_db.session, '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): def edit_book_series_index(series_index, book):
# Add default series_index to book if series_index:
modify_date = False # Add default series_index to book
series_index = series_index or '1' modify_date = False
if not series_index.replace('.', '', 1).isdigit(): series_index = series_index or '1'
flash(_("Seriesindex: %(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning") if not series_index.replace('.', '', 1).isdigit():
return False flash(_("Seriesindex: %(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
if str(book.series_index) != series_index: return False
book.series_index = series_index if str(book.series_index) != series_index:
modify_date = True book.series_index = series_index
return modify_date modify_date = True
return modify_date
return False
# Handle book comments/description # Handle book comments/description
def edit_book_comments(comments, book): def edit_book_comments(comments, book):
modify_date = False if comments is not None:
if comments: modify_date = False
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: if comments:
book.comments.append(db.Comments(comment=comments, book=book.id)) comments = clean_string(comments, book.id)
modify_date = True if len(book.comments):
return modify_date 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): def edit_book_languages(languages, book, upload_mode=False, invalid=None):
input_languages = languages.split(',') if languages is not None:
unknown_languages = [] input_languages = languages.split(',')
if not upload_mode: unknown_languages = []
input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages) if not upload_mode:
else: input_l = isoLanguages.get_language_code_from_name(get_locale(), input_languages, unknown_languages)
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)
else: else:
raise ValueError(_("'%(langname)s' is not a valid language", langname=lang)) input_l = isoLanguages.get_valid_language_codes_from_code(get_locale(), input_languages, unknown_languages)
# ToDo: Not working correct for lang in unknown_languages:
if upload_mode and len(input_l) == 1: log.error("'%s' is not a valid language", lang)
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view if isinstance(invalid, list):
# the book it's language is set to the filter language invalid.append(lang)
if input_l[0] != current_user.filter_language() and current_user.filter_language() != "all": else:
input_l[0] = calibre_db.session.query(db.Languages). \ raise ValueError(_("'%(langname)s' is not a valid language", langname=lang))
filter(db.Languages.lang_code == current_user.filter_language()).first().lang_code # ToDo: Not working correct
# Remove duplicates if upload_mode and len(input_l) == 1:
input_l = helper.uniq(input_l) # If the language of the file is excluded from the users view, it's not imported, to allow the user to view
return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages') # 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): def edit_book_publisher(publishers, book):
changed = False if publishers is not None:
if publishers: changed = False
publisher = strip_whitespaces(publishers) if publishers:
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name): publisher = strip_whitespaces(publishers)
changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session, if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
'publisher') changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session,
elif len(book.publishers): 'publisher')
changed |= modify_database_object([], book.publishers, db.Publishers, calibre_db.session, 'publisher') elif len(book.publishers):
return changed 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): def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string):
changed = False changed = False
@ -1160,61 +1172,66 @@ def edit_cc_data(book_id, book, to_save, cc):
changed = False changed = False
for c in cc: for c in cc:
cc_string = "custom_column_" + str(c.id) cc_string = "custom_column_" + str(c.id)
if not c.is_multiple: if to_save.get(cc_string) is not None:
if len(getattr(book, cc_string)) > 0: if not c.is_multiple:
cc_db_value = getattr(book, cc_string)[0].value if len(getattr(book, cc_string)) > 0:
else: cc_db_value = getattr(book, cc_string)[0].value
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: else:
change, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string) cc_db_value = None
changed |= change 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: else:
if cc_db_value is not None: input_tags = to_save[cc_string].split(',')
# remove old cc_val input_tags = list(map(lambda it: strip_whitespaces(it), input_tags))
del_cc = getattr(book, cc_string)[0] changed |= modify_database_object(input_tags,
getattr(book, cc_string).remove(del_cc) getattr(book, cc_string),
if not del_cc.books or len(del_cc.books) == 0: db.cc_classes[c.id],
calibre_db.session.delete(del_cc) calibre_db.session,
changed = True 'custom')
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')
return changed return changed
# returns None if no file is uploaded # returns False if an error occurs or no book is uploaded, in all other cases the ebook metadata to change is returned
# returns False if an error occurs, in all other cases the ebook metadata is returned def upload_book_formats(requested_files, book, book_id, no_cover=True):
def upload_single_file(file_request, book, book_id):
# Check and handle Uploaded file # 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(',') 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 config.config_check_extensions and allowed_extensions != ['']:
if not validate_mime_type(requested_file, 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") flash(_("File type isn't allowed to be uploaded to this server"), category="error")
return False error = True
# check for empty request continue
if requested_file.filename != '': if current_filename != '':
if not current_user.role_upload(): if not current_user.role_upload():
flash(_("User has no rights to upload additional file formats"), category="error") flash(_("User has no rights to upload additional file formats"), category="error")
return False error = True
if '.' in requested_file.filename: continue
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() if '.' in current_filename:
file_ext = current_filename.rsplit('.', 1)[-1].lower()
if file_ext not in allowed_extensions and '' not in allowed_extensions: 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), flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext),
category="error") category="error")
return False error = True
continue
else: else:
flash(_('File to be uploaded must have an extension'), category="error") flash(_('File to be uploaded must have an extension'), category="error")
return False error = True
continue
file_name = book.path.rsplit('/', 1)[-1] file_name = book.path.rsplit('/', 1)[-1]
filepath = os.path.normpath(os.path.join(config.get_book_path(), book.path)) 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: except OSError:
flash(_("Failed to create path %(path)s (Permission denied).", path=filepath), flash(_("Failed to create path %(path)s (Permission denied).", path=filepath),
category="error") category="error")
return False error = True
continue
try: try:
requested_file.save(saved_filename) requested_file.save(saved_filename)
except OSError: except OSError:
flash(_("Failed to store file %(file)s.", file=saved_filename), category="error") flash(_("Failed to store file %(file)s.", file=saved_filename), category="error")
return False error = True
continue
file_size = os.path.getsize(saved_filename) 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 # 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()) log.warning('Book format %s already existing', file_ext.upper())
else: else:
try: try:
db_format = db.Data(book_id, file_ext.upper(), file_size, file_name) db_format = db.Data(book_id, file_ext.upper(), file_size, file_name)
calibre_db.session.add(db_format) calibre_db.session.add(db_format)
calibre_db.session.commit() calibre_db.session.commit()
calibre_db.update_title_sort(config) calibre_db.create_functions(config)
except (OperationalError, IntegrityError, StaleDataError) as e: except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback() calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e)) log.error_or_exception("Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e), flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
category="error") category="error")
return False # return redirect(url_for('web.show_book', book_id=book.id)) error = True
continue
# Queue uploader info # Queue uploader info
link = '<a href="{}">{}</a>'.format(url_for('web.show_book', book_id=book.id), escape(book.title)) link = '<a href="{}">{}</a>'.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) 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))) WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(book.title)))
meta = uploader.process(
return uploader.process( saved_filename,
saved_filename, *os.path.splitext(requested_file.filename), *os.path.splitext(current_filename),
rar_executable=config.config_rarfile_location) rar_executable=config.config_rarfile_location,
return None 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): 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): def handle_author_on_edit(book, author_name, update_stored=True):
change = False change = False
# handle author(s)
input_authors = prepare_authors(author_name, config.get_book_path(), config.config_use_google_drive) 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 # 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': if db_type == 'custom':
type_elements = c_elements.value type_elements = c_elements.value
else: else:
# type_elements = c_elements.name
type_elements = c_elements type_elements = c_elements
for inp_element in input_elements: for inp_element in input_elements:
if type_elements == inp_element: if type_elements == inp_element:

View File

@ -66,7 +66,7 @@ def get_epub_layout(book, book_data):
return layout[0] 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 = { ns = {
'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',
@ -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_metadata = parse_epub_series(ns, tree, epub_metadata)
epub_zip = zipfile.ZipFile(tmp_file_path) 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 = [] identifiers = []
for node in p.xpath('dc:identifier', namespaces=ns): for node in p.xpath('dc:identifier', namespaces=ns):

View File

@ -328,7 +328,7 @@ def edit_book_read_status(book_id, read_status=None):
ub.session_commit("Book {} readbit toggled".format(book_id)) ub.session_commit("Book {} readbit toggled".format(book_id))
else: else:
try: try:
calibre_db.update_title_sort(config) calibre_db.create_functions(config)
book = calibre_db.get_filtered_book(book_id) book = calibre_db.get_filtered_book(book_id)
book_read_status = getattr(book, 'custom_column_' + str(config.config_read_column)) book_read_status = getattr(book, 'custom_column_' + str(config.config_read_column))
if len(book_read_status): if len(book_read_status):

View File

@ -15,24 +15,30 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
from . import logger from . import logger
from .string_helper import strip_whitespaces
log = logger.create() log = logger.create()
try: try:
from iso639 import languages, __version__ from iso639 import languages
# iso_version = importlib.metadata.version("iso639")
get = languages.get get = languages.get
except ImportError:
from pycountry import languages as pyc_languages
try: try:
import pkg_resources if sys.version_info >= (3, 12):
__version__ = pkg_resources.get_distribution('pycountry').version + ' (PyCountry)' import pkg_resources
del pkg_resources except ImportError:
except (ImportError, Exception): print("Python 3.12 isn't compatible with iso-639. Please install pycountry.")
__version__ = "? (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): def _copy_fields(l):
l.part1 = getattr(l, 'alpha_2', None) l.part1 = getattr(l, 'alpha_2', None)
@ -69,20 +75,20 @@ def get_language_name(locale, lang_code):
return name return name
def get_language_codes(locale, language_names, remainder=None): def get_language_code_from_name(locale, language_names, remainder=None):
language_names = set(x.strip().lower() for x in language_names if x) language_names = set(strip_whitespaces(x).lower() for x in language_names if x)
lang = list() lang = list()
for k, v in get_language_names(locale).items(): for key, val in get_language_names(locale).items():
v = v.lower() val = val.lower()
if v in language_names: if val in language_names:
lang.append(k) lang.append(key)
language_names.remove(v) language_names.remove(val)
if remainder is not None and language_names: if remainder is not None and language_names:
remainder.extend(language_names) remainder.extend(language_names)
return lang 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() lang = list()
if "" in language_names: if "" in language_names:
language_names.remove("") language_names.remove("")

View File

@ -27,7 +27,7 @@ import datetime
import mimetypes import mimetypes
from uuid import uuid4 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 flask_babel import format_date
from .cw_login import current_user 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()) url = url_for('web.get_series_cover', series_id=series.id, resolution=shortname, c=cache_timestamp())
srcset.append(f'{url} {resolution}x') srcset.append(f'{url} {resolution}x')
return ', '.join(srcset) 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

View File

@ -244,7 +244,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
pagination = None pagination = None
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) 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) 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)\ q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
.outerjoin(db.Series)\ .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['include_' + element] = term.get('include_' + element)
tags['exclude_' + element] = term.get('exclude_' + element) tags['exclude_' + element] = term.get('exclude_' + element)
author_name = term.get("author_name") author_name = term.get("authors")
book_title = term.get("book_title") book_title = term.get("title")
publisher = term.get("publisher") publisher = term.get("publisher")
pub_start = term.get("publishstart") pub_start = term.get("publishstart")
pub_end = term.get("publishend") pub_end = term.get("publishend")

View File

@ -3,9 +3,9 @@
*/ */
/* global Bloodhound, language, Modernizr, tinymce, getPath */ /* global Bloodhound, language, Modernizr, tinymce, getPath */
if ($("#description").length) { if ($("#comments").length) {
tinymce.init({ tinymce.init({
selector: "#description", selector: "#comments",
plugins: 'code', plugins: 'code',
branding: false, branding: false,
menubar: "edit view format", menubar: "edit view format",
@ -93,7 +93,7 @@ var authors = new Bloodhound({
}, },
}); });
$(".form-group #bookAuthor").typeahead( $(".form-group #authors").typeahead(
{ {
highlight: true, highlight: true,
minLength: 1, 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(); var filename = $(this).val();
if (filename.substring(3, 11) === "fakepath") { if (filename.substring(3, 11) === "fakepath") {
filename = filename.substring(12); filename = filename.substring(12);
} // Remove c:\fake at beginning from localhost chrome } // Remove c:\fake at beginning from localhost chrome
$("#upload-format").text(filename); $("#upload-format").text(filename);
}); });*/
$("#btn-upload-cover").on("change", function () { $("#btn-upload-cover").on("change", function () {
var filename = $(this).val(); var filename = $(this).val();
@ -261,8 +261,8 @@ $("#btn-upload-cover").on("change", function () {
$("#xchange").click(function () { $("#xchange").click(function () {
this.blur(); this.blur();
var title = $("#book_title").val(); var title = $("#title").val();
$("#book_title").val($("#bookAuthor").val()); $("#title").val($("#authors").val());
$("#bookAuthor").val(title); $("#authors").val(title);
}); });

View File

@ -38,12 +38,12 @@ $(function () {
} }
function populateForm (book) { function populateForm (book) {
tinymce.get("description").setContent(book.description); tinymce.get("comments").setContent(book.description);
var uniqueTags = getUniqueValues('tags', book) var uniqueTags = getUniqueValues('tags', book)
var uniqueLanguages = getUniqueValues('languages', book) var uniqueLanguages = getUniqueValues('languages', book)
var ampSeparatedAuthors = (book.authors || []).join(" & "); var ampSeparatedAuthors = (book.authors || []).join(" & ");
$("#bookAuthor").val(ampSeparatedAuthors); $("#authors").val(ampSeparatedAuthors);
$("#book_title").val(book.title); $("#title").val(book.title);
$("#tags").val(uniqueTags.join(", ")); $("#tags").val(uniqueTags.join(", "));
$("#languages").val(uniqueLanguages.join(", ")); $("#languages").val(uniqueLanguages.join(", "));
$("#rating").data("rating").setValue(Math.round(book.rating)); $("#rating").data("rating").setValue(Math.round(book.rating));
@ -172,7 +172,7 @@ $(function () {
$("#get_meta").click(function () { $("#get_meta").click(function () {
populate_provider(); populate_provider();
var bookTitle = $("#book_title").val(); var bookTitle = $("#title").val();
$("#keyword").val(bookTitle); $("#keyword").val(bookTitle);
keyword = bookTitle; keyword = bookTitle;
doSearch(bookTitle); doSearch(bookTitle);

View File

@ -130,8 +130,13 @@ $(".container-fluid").bind('drop', function (e) {
} }
}); });
if (dt.files.length) { if (dt.files.length) {
$("#btn-upload")[0].files = dt.files; if($("#btn-upload-format").length) {
$("#form-upload").submit(); $("#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").submit();
}); });
$("#form-upload").uploadprogress({ $("#btn-upload-format").change(function() {
redirect_url: getPath() + "/", //"{{ url_for('web.index')}}", $("#form-upload-format").submit();
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')}}"
}); });
$("#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() { $(document).ready(function() {
var inp = $('#query').first() var inp = $('#query').first()
if (inp.length) { if (inp.length) {

View File

@ -201,6 +201,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
with open(filename, 'wb') as fd: with open(filename, 'wb') as fd:
copyfileobj(stream, fd) copyfileobj(stream, fd)
except Exception as ex: except Exception as ex:
# Bubble exception to calling function # Bubble exception to calling function
self.log.debug('Error generating thumbnail file: ' + str(ex)) self.log.debug('Error generating thumbnail file: ' + str(ex))

View File

@ -62,11 +62,9 @@
<a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.Books.data %} {% if entry.Books.data|music %}
{% if format.format|lower in g.constants.EXTENSIONS_AUDIO %}
<span class="glyphicon glyphicon-music"></span> <span class="glyphicon glyphicon-music"></span>
{% endif %} {% endif %}
{% endfor %}
</p> </p>
{% if entry.Books.series.__len__() > 0 %} {% if entry.Books.series.__len__() > 0 %}
<p class="series"> <p class="series">

View File

@ -47,26 +47,41 @@
</form> </form>
</div> </div>
{% endif %} {% endif %}
{% if current_user.role_upload() and g.allow_upload %}
<div class="text-center more-stuff"><!--h4 aria-label="Upload new book format"></h4-->
<form id="form-upload-format" action="{{ url_for('edit-book.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
<div class="text-center">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="book_id" value="{{ book.id }}">
<div role="group" aria-label="Upload new book format">
<label class="btn btn-primary btn-file" for="btn-upload-format">{{ _('Upload Format') }}</label>
<div class="upload-format-input-text" id="upload-format"></div>
<input id="btn-upload-format" name="btn-upload-format" type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple>
</div>
</div>
</form>
</div> </div>
{% endif %}
</div>
<form role="form" action="{{ url_for('edit-book.edit_book', book_id=book.id) }}" method="post" enctype="multipart/form-data" id="book_edit_frm"> <form role="form" action="{{ url_for('edit-book.edit_book', book_id=book.id) }}" method="post" enctype="multipart/form-data" id="book_edit_frm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-sm-9 col-xs-12"> <div class="col-sm-9 col-xs-12">
<div class="form-group"> <div class="form-group">
<label for="book_title">{{_('Book Title')}}</label> <label for="title">{{_('Book Title')}}</label>
<input type="text" class="form-control" name="book_title" id="book_title" value="{{book.title}}"> <input type="text" class="form-control" name="title" id="title" value="{{book.title}}">
</div> </div>
<div class="text-center"> <div class="text-center">
<button type="button" class="btn btn-default" id="xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span></button> <button type="button" class="btn btn-default" id="xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span></button>
</div> </div>
<div id="author_div" class="form-group"> <div id="author_div" class="form-group">
<label for="bookAuthor">{{_('Author')}}</label> <label for="bookAuthor">{{_('Author')}}</label>
<input type="text" class="form-control typeahead" autocomplete="off" name="author_name" id="bookAuthor" value="{{' & '.join(authors)}}"> <input type="text" class="form-control typeahead" autocomplete="off" name="authors" id="authors" value="{{' & '.join(authors)}}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">{{_('Description')}}</label> <label for="comments">{{_('Description')}}</label>
<textarea class="form-control" name="description" id="description" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea> <textarea class="form-control" name="comments" id="comments" rows="7">{% if book.comments %}{{book.comments[0].text}}{%endif%}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -196,13 +211,6 @@
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if current_user.role_upload() and g.allow_upload %}
<div role="group" aria-label="Upload new book format">
<label class="btn btn-primary btn-file" for="btn-upload-format">{{ _('Upload Format') }}</label>
<div class="upload-format-input-text" id="upload-format"></div>
<input id="btn-upload-format" name="btn-upload-format" type="file">
</div>
{% endif %}
<div class="checkbox"> <div class="checkbox">
<label> <label>
@ -288,7 +296,7 @@
'no_result': {{_('No Result(s) found! Please try another keyword.')|safe|tojson}}, 'no_result': {{_('No Result(s) found! Please try another keyword.')|safe|tojson}},
'author': {{_('Author')|safe|tojson}}, 'author': {{_('Author')|safe|tojson}},
'publisher': {{_('Publisher')|safe|tojson}}, 'publisher': {{_('Publisher')|safe|tojson}},
'description': {{_('Description')|safe|tojson}}, 'comments': {{_('Description')|safe|tojson}},
'source': {{_('Source')|safe|tojson}}, 'source': {{_('Source')|safe|tojson}},
}; };
var language = '{{ current_user.locale }}'; var language = '{{ current_user.locale }}';

View File

@ -15,6 +15,6 @@
<img <img
srcset="{{ srcset }}" srcset="{{ srcset }}"
src="{{ url_for('web.get_series_cover', series_id=series.id, resolution='og', c='day'|cache_timestamp) }}" src="{{ url_for('web.get_series_cover', series_id=series.id, resolution='og', c='day'|cache_timestamp) }}"
alt="{{ book_title }}" alt="{{ title }}"
/> />
{%- endmacro %} {%- endmacro %}

View File

@ -119,11 +119,9 @@
<a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='stored') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='stored') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.Books.data %} {% if entry.Books.data|music %}
{% if format.format|lower in g.constants.EXTENSIONS_AUDIO %}
<span class="glyphicon glyphicon-music"></span> <span class="glyphicon glyphicon-music"></span>
{% endif %} {% endif %}
{%endfor%}
</p> </p>
{% if entry.Books.series.__len__() > 0 %} {% if entry.Books.series.__len__() > 0 %}
<p class="series"> <p class="series">

View File

@ -80,6 +80,7 @@
<div class="form-group"> <div class="form-group">
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload" <span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"
type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple></span> type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple></span>
<input class="hide" id="btn-upload2" name="btn-upload2" type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}">
</div> </div>
</form> </form>
</li> </li>

View File

@ -73,11 +73,9 @@
<a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='stored', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.Books.data %} {% if entry.Books.data|music %}
{% if format.format|lower in g.constants.EXTENSIONS_AUDIO %}
<span class="glyphicon glyphicon-music"></span> <span class="glyphicon glyphicon-music"></span>
{% endif %} {% endif %}
{% endfor %}
</p> </p>
{% if entry.Books.series.__len__() > 0 %} {% if entry.Books.series.__len__() > 0 %}
<p class="series"> <p class="series">

View File

@ -5,12 +5,12 @@
<form role="form" id="search" action="{{ url_for('search.advanced_search_form') }}" method="POST"> <form role="form" id="search" action="{{ url_for('search.advanced_search_form') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group"> <div class="form-group">
<label for="book_title">{{_('Book Title')}}</label> <label for="title">{{_('Book Title')}}</label>
<input type="text" class="form-control" name="book_title" id="book_title" value=""> <input type="text" class="form-control" name="title" id="title" value="">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="bookAuthor">{{_('Author')}}</label> <label for="bookAuthor">{{_('Author')}}</label>
<input type="text" class="form-control typeahead" name="author_name" id="bookAuthor" value="" autocomplete="off"> <input type="text" class="form-control typeahead" name="authors" id="authors" value="" autocomplete="off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="Publisher">{{_('Publisher')}}</label> <label for="Publisher">{{_('Publisher')}}</label>

View File

@ -77,24 +77,25 @@ except ImportError as e:
use_audio_meta = False use_audio_meta = False
def process(tmp_file_path, original_file_name, original_file_extension, rar_executable): def process(tmp_file_path, original_file_name, original_file_extension, rar_executable, no_cover=False):
meta = default_meta(tmp_file_path, original_file_name, original_file_extension) meta = default_meta(tmp_file_path, original_file_name, original_file_extension)
extension_upper = original_file_extension.upper() extension_upper = original_file_extension.upper()
try: try:
if ".PDF" == extension_upper: if ".PDF" == extension_upper:
meta = pdf_meta(tmp_file_path, original_file_name, original_file_extension) meta = pdf_meta(tmp_file_path, original_file_name, original_file_extension, no_cover)
elif extension_upper in [".KEPUB", ".EPUB"] and use_epub_meta is True: elif extension_upper in [".KEPUB", ".EPUB"] and use_epub_meta is True:
meta = epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension) meta = epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension, no_cover)
elif ".FB2" == extension_upper and use_fb2_meta is True: elif ".FB2" == extension_upper and use_fb2_meta is True:
meta = fb2.get_fb2_info(tmp_file_path, original_file_extension) meta = fb2.get_fb2_info(tmp_file_path, original_file_extension)
elif extension_upper in ['.CBZ', '.CBT', '.CBR', ".CB7"]: elif extension_upper in ['.CBZ', '.CBT', '.CBR', ".CB7"]:
meta = comic.get_comic_info(tmp_file_path, meta = comic.get_comic_info(tmp_file_path,
original_file_name, original_file_name,
original_file_extension, original_file_extension,
rar_executable) rar_executable,
no_cover)
elif extension_upper in [".MP3", ".OGG", ".FLAC", ".WAV", ".AAC", ".AIFF", ".ASF", ".MP4", elif extension_upper in [".MP3", ".OGG", ".FLAC", ".WAV", ".AAC", ".AIFF", ".ASF", ".MP4",
".M4A", ".M4B", ".OGV", ".OPUS"] and use_audio_meta: ".M4A", ".M4B", ".OGV", ".OPUS"] and use_audio_meta:
meta = audio.get_audio_file_info(tmp_file_path, original_file_extension, original_file_name) meta = audio.get_audio_file_info(tmp_file_path, original_file_extension, original_file_name, no_cover)
except Exception as ex: except Exception as ex:
log.warning('cannot parse metadata, using default: %s', ex) log.warning('cannot parse metadata, using default: %s', ex)
@ -168,7 +169,7 @@ def parse_xmp(pdf_file):
} }
def pdf_meta(tmp_file_path, original_file_name, original_file_extension): def pdf_meta(tmp_file_path, original_file_name, original_file_extension, no_cover_processing):
doc_info = None doc_info = None
xmp_info = None xmp_info = None
@ -216,7 +217,7 @@ def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
extension=original_file_extension, extension=original_file_extension,
title=title, title=title,
author=author, author=author,
cover=pdf_preview(tmp_file_path, original_file_name), cover=pdf_preview(tmp_file_path, original_file_name) if not no_cover_processing else None,
description=subject, description=subject,
tags=tags, tags=tags,
series="", series="",
@ -231,7 +232,7 @@ def pdf_preview(tmp_file_path, tmp_dir):
if use_generic_pdf_cover: if use_generic_pdf_cover:
return None return None
try: try:
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" cover_file_name = os.path.join(os.path.dirname(tmp_file_path), "cover.jpg")
with Image() as img: with Image() as img:
img.options["pdf:use-cropbox"] = "true" img.options["pdf:use-cropbox"] = "true"
img.read(filename=tmp_file_path + '[0]', resolution=150) img.read(filename=tmp_file_path + '[0]', resolution=150)

View File

@ -299,9 +299,10 @@ def get_languages_json():
def get_matching_tags(): def get_matching_tags():
tag_dict = {'tags': []} tag_dict = {'tags': []}
q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True)) q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True))
calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase) calibre_db.create_functions()
author_input = request.args.get('author_name') or '' # calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
title_input = request.args.get('book_title') or '' author_input = request.args.get('authors') or ''
title_input = request.args.get('title') or ''
include_tag_inputs = request.args.getlist('include_tag') or '' include_tag_inputs = request.args.getlist('include_tag') or ''
exclude_tag_inputs = request.args.getlist('exclude_tag') or '' exclude_tag_inputs = request.args.getlist('exclude_tag') or ''
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")), q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_input + "%")),

View File

@ -37,6 +37,7 @@ beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18,<2.1.20 faust-cchardet>=2.1.18,<2.1.20
py7zr>=0.15.0,<0.21.0 py7zr>=0.15.0,<0.21.0
mutagen>=1.40.0,<1.50.0 mutagen>=1.40.0,<1.50.0
pycountry>=20.0.0,<25.0.0
# Comics # Comics
natsort>=2.2.0,<8.5.0 natsort>=2.2.0,<8.5.0

View File

@ -101,6 +101,7 @@ metadata =
faust-cchardet>=2.1.18,<2.1.20 faust-cchardet>=2.1.18,<2.1.20
py7zr>=0.15.0,<0.21.0 py7zr>=0.15.0,<0.21.0
mutagen>=1.40.0,<1.50.0 mutagen>=1.40.0,<1.50.0
pycountry>=20.0.0,<25.0.0
comics = comics =
natsort>=2.2.0,<8.5.0 natsort>=2.2.0,<8.5.0
comicapi>=2.2.0,<3.3.0 comicapi>=2.2.0,<3.3.0

File diff suppressed because it is too large Load Diff