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:
commit
6a504673e5
@ -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
|
||||||
|
@ -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")
|
||||||
|
35
cps/audio.py
35
cps/audio.py
@ -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:
|
||||||
|
15
cps/comic.py
15
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)
|
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="",
|
||||||
|
23
cps/db.py
23
cps/db.py
@ -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
|
||||||
|
|
||||||
|
574
cps/editbooks.py
574
cps/editbooks.py
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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("")
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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))
|
||||||
|
@ -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">
|
||||||
|
@ -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 }}';
|
||||||
|
@ -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 %}
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
@ -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 + "%")),
|
||||||
|
@ -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
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user