1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-11-17 23:34:53 +00:00

Catch additional error on not existing custom column linked to read column (#2341)

Prevent metadata changes are lost on edit books with errors (#2326)
Better log output
Renamed log message on database delete
This commit is contained in:
Ozzie Isaacs 2022-03-20 11:21:15 +01:00
parent 39459603d4
commit 8cb5989c97
10 changed files with 312 additions and 241 deletions

View File

@ -1237,7 +1237,7 @@ def _db_configuration_update_helper():
config.store_calibre_uuid(calibre_db, db.LibraryId) config.store_calibre_uuid(calibre_db, db.LibraryId)
# if db changed -> delete shelfs, delete download books, delete read books, kobo sync... # if db changed -> delete shelfs, delete download books, delete read books, kobo sync...
if db_change: if db_change:
log.info("Calibre Database changed, delete all Calibre-Web info related to old Database") log.info("Calibre Database changed, all Calibre-Web info related to old Database gets deleted")
ub.session.query(ub.Downloads).delete() ub.session.query(ub.Downloads).delete()
ub.session.query(ub.ArchivedBook).delete() ub.session.query(ub.ArchivedBook).delete()
ub.session.query(ub.ReadBook).delete() ub.session.query(ub.ReadBook).delete()

View File

@ -620,8 +620,8 @@ class CalibreDB:
bd = (self.session.query(Books, read_column.value, ub.ArchivedBook.is_archived).select_from(Books) bd = (self.session.query(Books, read_column.value, ub.ArchivedBook.is_archived).select_from(Books)
.join(read_column, read_column.book == book_id, .join(read_column, read_column.book == book_id,
isouter=True)) isouter=True))
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.%d is not existing in calibre database", read_column) log.error("Custom Column No.{} is not existing in calibre database".format(read_column))
# Skip linking read column and return None instead of read status # Skip linking read column and return None instead of read status
bd = self.session.query(Books, None, ub.ArchivedBook.is_archived) bd = self.session.query(Books, None, ub.ArchivedBook.is_archived)
return (bd.filter(Books.id == book_id) return (bd.filter(Books.id == book_id)
@ -665,11 +665,11 @@ class CalibreDB:
neg_content_cc_filter = false() if neg_cc_list == [''] else \ neg_content_cc_filter = false() if neg_cc_list == [''] else \
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \ getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list)) any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
pos_content_cc_filter = false() pos_content_cc_filter = false()
neg_content_cc_filter = true() neg_content_cc_filter = true()
log.error(u"Custom Column No.%d is not existing in calibre database", log.error("Custom Column No.{} is not existing in calibre database".format(
self.config.config_restricted_column) self.config.config_restricted_column))
flash(_("Custom Column No.%(column)d is not existing in calibre database", flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=self.config.config_restricted_column), column=self.config.config_restricted_column),
category="error") category="error")
@ -727,8 +727,8 @@ class CalibreDB:
query = (self.session.query(database, read_column.value, ub.ArchivedBook.is_archived) query = (self.session.query(database, read_column.value, ub.ArchivedBook.is_archived)
.select_from(Books) .select_from(Books)
.outerjoin(read_column, read_column.book == Books.id)) .outerjoin(read_column, read_column.book == Books.id))
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.%d is not existing in calibre database", read_column) log.error("Custom Column No.{} is not existing in calibre database".format(read_column))
# Skip linking read column and return None instead of read status # Skip linking read column and return None instead of read status
query = self.session.query(database, None, ub.ArchivedBook.is_archived) query = self.session.query(database, None, ub.ArchivedBook.is_archived)
query = query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id, query = query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id,
@ -839,8 +839,8 @@ class CalibreDB:
read_column = cc_classes[config_read_column] read_column = cc_classes[config_read_column]
query = (self.session.query(Books, ub.ArchivedBook.is_archived, read_column.value).select_from(Books) query = (self.session.query(Books, ub.ArchivedBook.is_archived, read_column.value).select_from(Books)
.outerjoin(read_column, read_column.book == Books.id)) .outerjoin(read_column, read_column.book == Books.id))
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.%d is not existing in calibre database", config_read_column) log.error("Custom Column No.{} is not existing in calibre database".format(config_read_column))
# Skip linking read column # Skip linking read column
query = self.session.query(Books, ub.ArchivedBook.is_archived, None) query = self.session.query(Books, ub.ArchivedBook.is_archived, None)
query = query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id, query = query.outerjoin(ub.ArchivedBook, and_(Books.id == ub.ArchivedBook.book_id,

View File

@ -289,13 +289,13 @@ def delete_whole_book(book_id, book):
def render_delete_book_result(book_format, json_response, warning, book_id): def render_delete_book_result(book_format, json_response, warning, book_id):
if book_format: if book_format:
if json_response: if json_response:
return json.dumps([warning, {"location": url_for("edit-book.edit_book", book_id=book_id), return json.dumps([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "success", "type": "success",
"format": book_format, "format": book_format,
"message": _('Book Format Successfully Deleted')}]) "message": _('Book Format Successfully Deleted')}])
else: else:
flash(_('Book Format Successfully Deleted'), category="success") flash(_('Book Format Successfully Deleted'), category="success")
return redirect(url_for('edit-book.edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
else: else:
if json_response: if json_response:
return json.dumps([warning, {"location": url_for('web.index'), return json.dumps([warning, {"location": url_for('web.index'),
@ -316,16 +316,16 @@ def delete_book_from_table(book_id, book_format, json_response):
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result: if not result:
if json_response: if json_response:
return json.dumps([{"location": url_for("edit-book.edit_book", book_id=book_id), return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger", "type": "danger",
"format": "", "format": "",
"message": error}]) "message": error}])
else: else:
flash(error, category="error") flash(error, category="error")
return redirect(url_for('edit-book.edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
if error: if error:
if json_response: if json_response:
warning = {"location": url_for("edit-book.edit_book", book_id=book_id), warning = {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "warning", "type": "warning",
"format": "", "format": "",
"message": error} "message": error}
@ -343,13 +343,13 @@ def delete_book_from_table(book_id, book_format, json_response):
log.error_or_exception(ex) log.error_or_exception(ex)
calibre_db.session.rollback() calibre_db.session.rollback()
if json_response: if json_response:
return json.dumps([{"location": url_for("edit-book.edit_book", book_id=book_id), return json.dumps([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger", "type": "danger",
"format": "", "format": "",
"message": ex}]) "message": ex}])
else: else:
flash(str(ex), category="error") flash(str(ex), category="error")
return redirect(url_for('edit-book.edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
else: else:
# book not found # book not found
@ -357,13 +357,13 @@ def delete_book_from_table(book_id, book_format, json_response):
return render_delete_book_result(book_format, json_response, warning, book_id) return render_delete_book_result(book_format, json_response, warning, book_id)
message = _("You are missing permissions to delete books") message = _("You are missing permissions to delete books")
if json_response: if json_response:
return json.dumps({"location": url_for("edit-book.edit_book", book_id=book_id), return json.dumps({"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger", "type": "danger",
"format": "", "format": "",
"message": message}) "message": message})
else: else:
flash(message, category="error") flash(message, category="error")
return redirect(url_for('edit-book.edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
def render_edit_book(book_id): def render_edit_book(book_id):
@ -413,18 +413,18 @@ def render_edit_book(book_id):
def edit_book_ratings(to_save, book): def edit_book_ratings(to_save, book):
changed = False changed = False
if to_save["rating"].strip(): if to_save.get("rating","").strip():
old_rating = False old_rating = False
if len(book.ratings) > 0: if len(book.ratings) > 0:
old_rating = book.ratings[0].rating old_rating = book.ratings[0].rating
ratingx2 = int(float(to_save["rating"]) * 2) rating_x2 = int(float(to_save.get("rating","")) * 2)
if ratingx2 != old_rating: if rating_x2 != old_rating:
changed = True changed = True
is_rating = calibre_db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first() is_rating = calibre_db.session.query(db.Ratings).filter(db.Ratings.rating == rating_x2).first()
if is_rating: if is_rating:
book.ratings.append(is_rating) book.ratings.append(is_rating)
else: else:
new_rating = db.Ratings(rating=ratingx2) new_rating = db.Ratings(rating=rating_x2)
book.ratings.append(new_rating) book.ratings.append(new_rating)
if old_rating: if old_rating:
book.ratings.remove(book.ratings[0]) book.ratings.remove(book.ratings[0])
@ -622,24 +622,26 @@ def edit_cc_data(book_id, book, to_save, cc):
'custom') 'custom')
return changed return changed
# returns None if no file is uploaded
# returns False if an error occours, in all other cases the ebook metadata is returned
def upload_single_file(file_request, book, book_id): def upload_single_file(file_request, book, book_id):
# Check and handle Uploaded file # Check and handle Uploaded file
if 'btn-upload-format' in file_request.files: requested_file = file_request.files.get('btn-upload-format', None)
requested_file = file_request.files['btn-upload-format'] if requested_file:
# check for empty request # check for empty request
if requested_file.filename != '': if requested_file.filename != '':
if not current_user.role_upload(): if not current_user.role_upload():
abort(403) flash(_(u"User has no rights to upload additional file formats"), category="error")
return False
if '.' in requested_file.filename: if '.' in requested_file.filename:
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD: if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
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 redirect(url_for('web.show_book', book_id=book.id)) return False
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 redirect(url_for('web.show_book', book_id=book.id)) return False
file_name = book.path.rsplit('/', 1)[-1] file_name = book.path.rsplit('/', 1)[-1]
filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path))
@ -651,12 +653,12 @@ def upload_single_file(file_request, book, book_id):
os.makedirs(filepath) os.makedirs(filepath)
except OSError: except OSError:
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return False
try: try:
requested_file.save(saved_filename) requested_file.save(saved_filename)
except OSError: except OSError:
flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error") flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return False
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()) is_format = calibre_db.get_book_format(book_id, file_ext.upper())
@ -674,7 +676,7 @@ def upload_single_file(file_request, book, book_id):
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(_(u"Database error: %(error)s.", error=e.orig), category="error") flash(_(u"Database error: %(error)s.", error=e.orig), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return False # return redirect(url_for('web.show_book', book_id=book.id))
# 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))
@ -684,15 +686,16 @@ def upload_single_file(file_request, book, book_id):
return uploader.process( return uploader.process(
saved_filename, *os.path.splitext(requested_file.filename), saved_filename, *os.path.splitext(requested_file.filename),
rarExecutable=config.config_rarfile_location) rarExecutable=config.config_rarfile_location)
return None
def upload_cover(cover_request, book): def upload_cover(cover_request, book):
if 'btn-upload-cover' in cover_request.files: requested_file = cover_request.files.get('btn-upload-cover', None)
requested_file = cover_request.files['btn-upload-cover'] if requested_file:
# check for empty request # check for empty request
if requested_file.filename != '': if requested_file.filename != '':
if not current_user.role_upload(): if not current_user.role_upload():
abort(403) flash(_(u"User has no rights to upload cover"), category="error")
return False
ret, message = helper.save_cover(requested_file, book.path) ret, message = helper.save_cover(requested_file, book.path)
if ret is True: if ret is True:
return True return True
@ -716,25 +719,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):
# handle author(s) # handle author(s)
input_authors, renamed = prepare_authors(author_name) input_authors, renamed = prepare_authors(author_name)
'''input_authors = author_name.split('&')
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
# Remove duplicates in authors list
input_authors = helper.uniq(input_authors)
# we have all author names now
if input_authors == ['']:
input_authors = [_(u'Unknown')] # prevent empty Author
renamed = list()
for in_aut in input_authors:
renamed_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == in_aut).first()
if renamed_author and in_aut != renamed_author.name:
renamed.append(renamed_author.name)
all_books = calibre_db.session.query(db.Books) \
.filter(db.Books.authors.any(db.Authors.name == renamed_author.name)).all()
sorted_renamed_author = helper.get_sorted_author(renamed_author.name)
sorted_old_author = helper.get_sorted_author(in_aut)
for one_book in all_books:
one_book.author_sort = one_book.author_sort.replace(sorted_renamed_author, sorted_old_author)'''
change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author') change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
@ -754,12 +738,19 @@ def handle_author_on_edit(book, author_name, update_stored=True):
change = True change = True
return input_authors, change, renamed return input_authors, change, renamed
@EditBook.route("/admin/book/<int:book_id>", methods=['GET'])
@login_required_if_no_ano
@edit_required
def show_edit_book(book_id):
return render_edit_book(book_id)
@EditBook.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
@EditBook.route("/admin/book/<int:book_id>", methods=['POST'])
@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 modify_date = False
edit_error = False
# create the function for sorting... # create the function for sorting...
try: try:
@ -768,109 +759,120 @@ def edit_book(book_id):
log.error_or_exception(e) log.error_or_exception(e)
calibre_db.session.rollback() calibre_db.session.rollback()
# Show form
if request.method != 'POST':
return render_edit_book(book_id)
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
# Book not found # Book not found
if not book: if not book:
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
category="error") category="error")
return redirect(url_for("web.index")) return redirect(url_for("web.index"))
meta = upload_single_file(request, book, book_id) to_save = request.form.to_dict()
if upload_cover(request, book) is True:
book.has_cover = 1
modify_date = True
try: try:
to_save = request.form.to_dict() # Update folder of book on local disk
merge_metadata(to_save, meta)
# Update book
edited_books_id = None edited_books_id = None
title_author_error = None
# handle book title # handle book title change
title_change = handle_title_on_edit(book, to_save["book_title"]) title_change = handle_title_on_edit(book, to_save["book_title"])
# handle book author change
input_authors, authorchange, renamed = handle_author_on_edit(book, to_save["author_name"]) input_authors, author_change, renamed = handle_author_on_edit(book, to_save["author_name"])
if authorchange or title_change: if author_change or title_change:
edited_books_id = book.id edited_books_id = book.id
modify_date = True modify_date = True
title_author_error = helper.update_dir_structure(edited_books_id,
config.config_calibre_dir,
input_authors[0],
renamed_author=renamed)
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)
if meta:
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: if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal() gdriveutils.updateGdriveCalibreFromLocal()
error = "" if to_save.get("cover_url", None):
if edited_books_id: if not current_user.role_upload():
error = helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], edit_error = True
renamed_author=renamed) flash(_(u"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"], book.path)
if result is True:
book.has_cover = 1
modify_date = True
else:
flash(error, category="error")
if not error: # Add default series_index to book
if "cover_url" in to_save: modify_date |= edit_book_series_index(to_save["series_index"], book)
if to_save["cover_url"]: # Handle book comments/description
if not current_user.role_upload(): modify_date |= edit_book_comments(Markup(to_save['description']).unescape(), book)
calibre_db.session.rollback() # Handle identifiers
return "", 403 input_identifiers = identifier_list(to_save, book)
if to_save["cover_url"].endswith('/static/generic_cover.jpg'): modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
book.has_cover = 0 if warning:
else: flash(_("Identifiers are not Case Sensitive, Overwriting Old Identifier"), category="warning")
result, error = helper.save_cover_from_url(to_save["cover_url"], book.path) modify_date |= modification
if result is True: # Handle book tags
book.has_cover = 1 modify_date |= edit_book_tags(to_save['tags'], book)
modify_date = True # Handle book series
else: modify_date |= edit_book_series(to_save["series"], book)
flash(error, category="error") # handle book publisher
modify_date |= edit_book_publisher(to_save['publisher'], book)
# Add default series_index to book # handle book languages
modify_date |= edit_book_series_index(to_save["series_index"], book) try:
# 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
modify_date |= edit_book_languages(to_save['languages'], book) modify_date |= edit_book_languages(to_save['languages'], book)
# handle book ratings except ValueError as e:
modify_date |= edit_book_ratings(to_save, book) flash(str(e), category="error")
# handle cc data edit_error = True
modify_date |= edit_all_cc_data(book_id, book, to_save) # 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["pubdate"]: if to_save.get("pubdate", None):
try: try:
book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d") book.pubdate = datetime.strptime(to_save["pubdate"], "%Y-%m-%d")
except ValueError: except ValueError as e:
book.pubdate = db.Books.DEFAULT_PUBDATE
else:
book.pubdate = db.Books.DEFAULT_PUBDATE book.pubdate = db.Books.DEFAULT_PUBDATE
flash(str(e), category="error")
if modify_date: edit_error = True
book.last_modified = datetime.utcnow() else:
kobo_sync_status.remove_synced_book(edited_books_id, all=True) book.pubdate = db.Books.DEFAULT_PUBDATE
calibre_db.session.merge(book) if modify_date:
calibre_db.session.commit() book.last_modified = datetime.utcnow()
if config.config_use_google_drive: kobo_sync_status.remove_synced_book(edited_books_id, all=True)
gdriveutils.updateGdriveCalibreFromLocal()
if "detail_view" in to_save: calibre_db.session.merge(book)
return redirect(url_for('web.show_book', book_id=book.id)) calibre_db.session.commit()
else: if config.config_use_google_drive:
flash(_("Metadata successfully updated"), category="success") gdriveutils.updateGdriveCalibreFromLocal()
return render_edit_book(book_id) 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: else:
calibre_db.session.rollback()
flash(error, category="error")
return render_edit_book(book_id) return render_edit_book(book_id)
except ValueError as e: except ValueError as e:
log.error_or_exception("Error: {}".format(e))
calibre_db.session.rollback() calibre_db.session.rollback()
flash(str(e), category="error") flash(str(e), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return redirect(url_for('web.show_book', book_id=book.id))
@ -882,14 +884,14 @@ def edit_book(book_id):
except Exception as ex: except Exception as ex:
log.error_or_exception(ex) log.error_or_exception(ex)
calibre_db.session.rollback() calibre_db.session.rollback()
flash(_("Error editing book, please check logfile for details"), category="error") flash(_("Error editing book: {}".format(ex)), category="error")
return redirect(url_for('web.show_book', book_id=book.id)) return redirect(url_for('web.show_book', book_id=book.id))
def merge_metadata(to_save, meta): def merge_metadata(to_save, meta):
if to_save['author_name'] == _(u'Unknown'): if to_save.get('author_name', "") == _(u'Unknown'):
to_save['author_name'] = '' to_save['author_name'] = ''
if to_save['book_title'] == _(u'Unknown'): if to_save.get('book_title', "") == _(u'Unknown'):
to_save['book_title'] = '' to_save['book_title'] = ''
for s_field, m_field in [ for s_field, m_field in [
('tags', 'tags'), ('author_name', 'author'), ('series', 'series'), ('tags', 'tags'), ('author_name', 'author'), ('series', 'series'),
@ -1117,7 +1119,7 @@ def upload():
if len(request.files.getlist("btn-upload")) < 2: if len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin(): if current_user.role_edit() or current_user.role_admin():
resp = {"location": url_for('edit-book.edit_book', book_id=book_id)} resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
return Response(json.dumps(resp), mimetype='application/json') return Response(json.dumps(resp), mimetype='application/json')
else: else:
resp = {"location": url_for('web.show_book', book_id=book_id)} resp = {"location": url_for('web.show_book', book_id=book_id)}
@ -1139,7 +1141,7 @@ def convert_bookformat(book_id):
if (book_format_from is None) or (book_format_to is None): if (book_format_from is None) or (book_format_to is None):
flash(_(u"Source or destination format for conversion missing"), category="error") flash(_(u"Source or destination format for conversion missing"), category="error")
return redirect(url_for('edit-book.edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(),
@ -1151,7 +1153,7 @@ def convert_bookformat(book_id):
category="success") category="success")
else: else:
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
return redirect(url_for('edit-book.edit_book', book_id=book_id)) return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
@EditBook.route("/ajax/getcustomenum/<int:c_id>") @EditBook.route("/ajax/getcustomenum/<int:c_id>")
@ -1211,10 +1213,15 @@ def edit_list_book(param):
mimetype='application/json') mimetype='application/json')
elif param == 'title': elif param == 'title':
sort_param = book.sort sort_param = book.sort
handle_title_on_edit(book, vals.get('value', "")) if handle_title_on_edit(book, vals.get('value', "")):
helper.update_dir_structure(book.id, config.config_calibre_dir) rename_error = helper.update_dir_structure(book.id, config.config_calibre_dir)
ret = Response(json.dumps({'success': True, 'newValue': book.title}), if not rename_error:
mimetype='application/json') ret = Response(json.dumps({'success': True, 'newValue': book.title}),
mimetype='application/json')
else:
ret = Response(json.dumps({'success': False,
'msg': rename_error}),
mimetype='application/json')
elif param == 'sort': elif param == 'sort':
book.sort = vals['value'] book.sort = vals['value']
ret = Response(json.dumps({'success': True, 'newValue': book.sort}), ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
@ -1225,11 +1232,17 @@ def edit_list_book(param):
mimetype='application/json') mimetype='application/json')
elif param == 'authors': elif param == 'authors':
input_authors, __, renamed = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") input_authors, __, renamed = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
helper.update_dir_structure(book.id, config.config_calibre_dir, input_authors[0], renamed_author=renamed) rename_error = helper.update_dir_structure(book.id, config.config_calibre_dir, input_authors[0],
ret = Response(json.dumps({ renamed_author=renamed)
'success': True, if not rename_error:
'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}), ret = Response(json.dumps({
mimetype='application/json') 'success': True,
'newValue': ' & '.join([author.replace('|', ',') for author in input_authors])}),
mimetype='application/json')
else:
ret = Response(json.dumps({'success': False,
'msg': rename_error}),
mimetype='application/json')
elif param == 'is_archived': elif param == 'is_archived':
is_archived = change_archived_books(book.id, vals['value'] == "True", is_archived = change_archived_books(book.id, vals['value'] == "True",
message="Book {} archive bit set to: {}".format(book.id, vals['value'])) message="Book {} archive bit set to: {}".format(book.id, vals['value']))
@ -1356,8 +1369,8 @@ def table_xchange_author_title():
author_names.append(authr.name.replace('|', ',')) author_names.append(authr.name.replace('|', ','))
title_change = handle_title_on_edit(book, " ".join(author_names)) title_change = handle_title_on_edit(book, " ".join(author_names))
input_authors, authorchange, renamed = handle_author_on_edit(book, authors) input_authors, author_change, renamed = handle_author_on_edit(book, authors)
if authorchange or title_change: if author_change or title_change:
edited_books_id = book.id edited_books_id = book.id
modify_date = True modify_date = True
@ -1365,8 +1378,9 @@ def table_xchange_author_title():
gdriveutils.updateGdriveCalibreFromLocal() gdriveutils.updateGdriveCalibreFromLocal()
if edited_books_id: if edited_books_id:
helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0], # toDo: Handle error
renamed_author=renamed) edit_error = helper.update_dir_structure(edited_books_id, config.config_calibre_dir, input_authors[0],
renamed_author=renamed)
if modify_date: if modify_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
try: try:

View File

@ -327,8 +327,9 @@ def edit_book_read_status(book_id, read_status=None):
new_cc = cc_class(value=read_status or 1, book=book_id) new_cc = cc_class(value=read_status or 1, book=book_id)
calibre_db.session.add(new_cc) calibre_db.session.add(new_cc)
calibre_db.session.commit() calibre_db.session.commit()
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column) log.error(
"Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
return "Custom Column No.{} is not existing in calibre database".format(config.config_read_column) return "Custom Column No.{} is not existing in calibre database".format(config.config_read_column)
except (OperationalError, InvalidRequestError) as ex: except (OperationalError, InvalidRequestError) as ex:
calibre_db.session.rollback() calibre_db.session.rollback()
@ -435,7 +436,8 @@ def rename_all_authors(first_author, renamed_author, calibre_path="", localbook=
new_author_path = os.path.join(calibre_path, new_author_rename_dir) new_author_path = os.path.join(calibre_path, new_author_rename_dir)
shutil.move(os.path.normcase(old_author_path), os.path.normcase(new_author_path)) shutil.move(os.path.normcase(old_author_path), os.path.normcase(new_author_path))
except OSError as ex: except OSError as ex:
log.error_or_exception("Rename author from: %s to %s: %s", old_author_path, new_author_path, ex) log.error("Rename author from: %s to %s: %s", old_author_path, new_author_path, ex)
log.debug(ex, exc_info=True)
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=old_author_path, dest=new_author_path, error=str(ex)) src=old_author_path, dest=new_author_path, error=str(ex))
else: else:
@ -446,31 +448,31 @@ def rename_all_authors(first_author, renamed_author, calibre_path="", localbook=
# Moves files in file storage during author/title rename, or from temp dir to file storage # Moves files in file storage during author/title rename, or from temp dir to file storage
def update_dir_structure_file(book_id, calibre_path, first_author, original_filepath, db_filename, renamed_author): def update_dir_structure_file(book_id, calibre_path, first_author, original_filepath, db_filename, renamed_author):
# get book database entry from id, if original path overwrite source with original_filepath # get book database entry from id, if original path overwrite source with original_filepath
localbook = calibre_db.get_book(book_id) local_book = calibre_db.get_book(book_id)
if original_filepath: if original_filepath:
path = original_filepath path = original_filepath
else: else:
path = os.path.join(calibre_path, localbook.path) path = os.path.join(calibre_path, local_book.path)
# Create (current) authordir and titledir from database # Create (current) author_dir and title_dir from database
authordir = localbook.path.split('/')[0] author_dir = local_book.path.split('/')[0]
titledir = localbook.path.split('/')[1] title_dir = local_book.path.split('/')[1]
# Create new_authordir from parameter or from database # Create new_author_dir from parameter or from database
# Create new titledir from database and add id # Create new title_dir from database and add id
new_authordir = rename_all_authors(first_author, renamed_author, calibre_path, localbook) new_author_dir = rename_all_authors(first_author, renamed_author, calibre_path, local_book)
if first_author: if first_author:
if first_author.lower() in [r.lower() for r in renamed_author]: if first_author.lower() in [r.lower() for r in renamed_author]:
if os.path.isdir(os.path.join(calibre_path, new_authordir)): if os.path.isdir(os.path.join(calibre_path, new_author_dir)):
path = os.path.join(calibre_path, new_authordir, titledir) path = os.path.join(calibre_path, new_author_dir, title_dir)
new_titledir = get_valid_filename(localbook.title, chars=96) + " (" + str(book_id) + ")" new_title_dir = get_valid_filename(local_book.title, chars=96) + " (" + str(book_id) + ")"
if titledir != new_titledir or authordir != new_authordir or original_filepath: if title_dir != new_title_dir or author_dir != new_author_dir or original_filepath:
error = move_files_on_change(calibre_path, error = move_files_on_change(calibre_path,
new_authordir, new_author_dir,
new_titledir, new_title_dir,
localbook, local_book,
db_filename, db_filename,
original_filepath, original_filepath,
path) path)
@ -478,7 +480,7 @@ def update_dir_structure_file(book_id, calibre_path, first_author, original_file
return error return error
# Rename all files from old names to new names # Rename all files from old names to new names
return rename_files_on_change(first_author, renamed_author, localbook, original_filepath, path, calibre_path) return rename_files_on_change(first_author, renamed_author, local_book, original_filepath, path, calibre_path)
def upload_new_file_gdrive(book_id, first_author, renamed_author, title, title_dir, original_filepath, filename_ext): def upload_new_file_gdrive(book_id, first_author, renamed_author, title, title_dir, original_filepath, filename_ext):
@ -490,7 +492,7 @@ def upload_new_file_gdrive(book_id, first_author, renamed_author, title, title_d
title_dir + " (" + str(book_id) + ")") title_dir + " (" + str(book_id) + ")")
book.path = gdrive_path.replace("\\", "/") book.path = gdrive_path.replace("\\", "/")
gd.uploadFileToEbooksFolder(os.path.join(gdrive_path, file_name).replace("\\", "/"), original_filepath) gd.uploadFileToEbooksFolder(os.path.join(gdrive_path, file_name).replace("\\", "/"), original_filepath)
return rename_files_on_change(first_author, renamed_author, localbook=book, gdrive=True) return rename_files_on_change(first_author, renamed_author, local_book=book, gdrive=True)
def update_dir_structure_gdrive(book_id, first_author, renamed_author): def update_dir_structure_gdrive(book_id, first_author, renamed_author):
@ -549,7 +551,7 @@ def move_files_on_change(calibre_path, new_authordir, new_titledir, localbook, d
# change location in database to new author/title path # change location in database to new author/title path
localbook.path = os.path.join(new_authordir, new_titledir).replace('\\', '/') localbook.path = os.path.join(new_authordir, new_titledir).replace('\\', '/')
except OSError as ex: except OSError as ex:
log.error_or_exception("Rename title from: %s to %s: %s", path, new_path, ex) log.error_or_exception("Rename title from {} to {} failed with error: {}".format(path, new_path, ex))
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_path, error=str(ex)) src=path, dest=new_path, error=str(ex))
return False return False
@ -557,8 +559,8 @@ def move_files_on_change(calibre_path, new_authordir, new_titledir, localbook, d
def rename_files_on_change(first_author, def rename_files_on_change(first_author,
renamed_author, renamed_author,
localbook, local_book,
orignal_filepath="", original_filepath="",
path="", path="",
calibre_path="", calibre_path="",
gdrive=False): gdrive=False):
@ -566,12 +568,12 @@ def rename_files_on_change(first_author,
try: try:
clean_author_database(renamed_author, calibre_path, gdrive=gdrive) clean_author_database(renamed_author, calibre_path, gdrive=gdrive)
if first_author and first_author not in renamed_author: if first_author and first_author not in renamed_author:
clean_author_database([first_author], calibre_path, localbook, gdrive) clean_author_database([first_author], calibre_path, local_book, gdrive)
if not gdrive and not renamed_author and not orignal_filepath and len(os.listdir(os.path.dirname(path))) == 0: if not gdrive and not renamed_author and not original_filepath and len(os.listdir(os.path.dirname(path))) == 0:
shutil.rmtree(os.path.dirname(path)) shutil.rmtree(os.path.dirname(path))
except (OSError, FileNotFoundError) as ex: except (OSError, FileNotFoundError) as ex:
log.error_or_exception("Error in rename file in path %s", ex) log.error_or_exception("Error in rename file in path {}".format(ex))
return _("Error in rename file in path: %(error)s", error=str(ex)) return _("Error in rename file in path: {}".format(str(ex)))
return False return False

View File

@ -110,8 +110,8 @@ def get_readbooks_ids():
readBooks = calibre_db.session.query(db.cc_classes[config.config_read_column])\ readBooks = calibre_db.session.query(db.cc_classes[config.config_read_column])\
.filter(db.cc_classes[config.config_read_column].value == True).all() .filter(db.cc_classes[config.config_read_column].value == True).all()
return frozenset([x.book for x in readBooks]) return frozenset([x.book for x in readBooks])
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column) log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
return [] return []
# Returns the template for rendering and includes the instance name # Returns the template for rendering and includes the instance name

View File

@ -295,7 +295,7 @@
{% if g.user.role_edit() %} {% if g.user.role_edit() %}
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Edit/Delete book"> <div class="btn-group" role="group" aria-label="Edit/Delete book">
<a href="{{ url_for('edit-book.edit_book', book_id=entry.id) }}" class="btn btn-sm btn-primary" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a> <a href="{{ url_for('edit-book.show_edit_book', book_id=entry.id) }}" class="btn btn-sm btn-primary" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -67,6 +67,7 @@ logged_in = dict()
def signal_store_user_session(object, user): def signal_store_user_session(object, user):
store_user_session() store_user_session()
def store_user_session(): def store_user_session():
if flask_session.get('user_id', ""): if flask_session.get('user_id', ""):
flask_session['_user_id'] = flask_session.get('user_id', "") flask_session['_user_id'] = flask_session.get('user_id', "")
@ -85,15 +86,16 @@ def store_user_session():
else: else:
log.error("No user id in session") log.error("No user id in session")
def delete_user_session(user_id, session_key): def delete_user_session(user_id, session_key):
try: try:
log.debug("Deleted session_key: " + session_key) log.debug("Deleted session_key: " + session_key)
session.query(User_Sessions).filter(User_Sessions.user_id==user_id, session.query(User_Sessions).filter(User_Sessions.user_id == user_id,
User_Sessions.session_key==session_key).delete() User_Sessions.session_key == session_key).delete()
session.commit() session.commit()
except (exc.OperationalError, exc.InvalidRequestError) as e: except (exc.OperationalError, exc.InvalidRequestError) as ex:
session.rollback() session.rollback()
log.exception(e) log.exception(ex)
def check_user_session(user_id, session_key): def check_user_session(user_id, session_key):
@ -209,9 +211,9 @@ class UserBase:
pass pass
try: try:
session.commit() session.commit()
except (exc.OperationalError, exc.InvalidRequestError): except (exc.OperationalError, exc.InvalidRequestError) as e:
session.rollback() session.rollback()
# ToDo: Error message log.error_or_exception(e)
def __repr__(self): def __repr__(self):
return '<User %r>' % self.name return '<User %r>' % self.name

View File

@ -87,7 +87,7 @@ def add_security_headers(resp):
csp += ''.join([' ' + host for host in config.config_trustedhosts.strip().split(',')]) csp += ''.join([' ' + host for host in config.config_trustedhosts.strip().split(',')])
csp += " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:" csp += " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:"
resp.headers['Content-Security-Policy'] = csp resp.headers['Content-Security-Policy'] = csp
if request.endpoint == "edit-book.edit_book" or config.config_use_google_drive: if request.endpoint == "edit-book.show_edit_book" or config.config_use_google_drive:
resp.headers['Content-Security-Policy'] += " *" resp.headers['Content-Security-Policy'] += " *"
elif request.endpoint == "web.read_book": elif request.endpoint == "web.read_book":
resp.headers['Content-Security-Policy'] += " blob:;style-src-elem 'self' blob: 'unsafe-inline';" resp.headers['Content-Security-Policy'] += " blob:;style-src-elem 'self' blob: 'unsafe-inline';"
@ -646,8 +646,8 @@ def render_read_books(page, are_read, as_xml=False, order=None):
db.Books.id == db.books_series_link.c.book, db.Books.id == db.books_series_link.c.book,
db.Series, db.Series,
db.cc_classes[config.config_read_column]) db.cc_classes[config.config_read_column])
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column) log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
if not as_xml: if not as_xml:
flash(_("Custom Column No.%(column)d is not existing in calibre database", flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=config.config_read_column), column=config.config_read_column),
@ -826,8 +826,9 @@ def list_books():
books = (calibre_db.session.query(db.Books, read_column.value, ub.ArchivedBook.is_archived) books = (calibre_db.session.query(db.Books, read_column.value, ub.ArchivedBook.is_archived)
.select_from(db.Books) .select_from(db.Books)
.outerjoin(read_column, read_column.book == db.Books.id)) .outerjoin(read_column, read_column.book == db.Books.id))
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.%d is not existing in calibre database", read_column) log.error(
"Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
# Skip linking read column and return None instead of read status # Skip linking read column and return None instead of read status
books = calibre_db.session.query(db.Books, None, ub.ArchivedBook.is_archived) books = calibre_db.session.query(db.Books, None, ub.ArchivedBook.is_archived)
books = (books.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id, books = (books.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
@ -1139,8 +1140,9 @@ def adv_search_read_status(q, read_status):
else: else:
q = q.join(db.cc_classes[config.config_read_column], isouter=True) \ q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
.filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True) .filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True)
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column) log.error(
"Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
flash(_("Custom Column No.%(column)d is not existing in calibre database", flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=config.config_read_column), column=config.config_read_column),
category="error") category="error")
@ -1262,8 +1264,8 @@ def render_adv_search_results(term, offset=None, order=None, limit=None):
query = (calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, read_column.value) query = (calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, read_column.value)
.select_from(db.Books) .select_from(db.Books)
.outerjoin(read_column, read_column.book == db.Books.id)) .outerjoin(read_column, read_column.book == db.Books.id))
except (KeyError, AttributeError): except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column) log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
# Skip linking read column # Skip linking read column
query = calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, None) query = calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, None)
query = query.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id, query = query.outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,

View File

@ -1,5 +1,5 @@
# GDrive Integration # GDrive Integration
google-api-python-client>=1.7.11,<2.41.0 google-api-python-client>=1.7.11,<2.42.0
gevent>20.6.0,<22.0.0 gevent>20.6.0,<22.0.0
greenlet>=0.4.17,<1.2.0 greenlet>=0.4.17,<1.2.0
httplib2>=0.9.2,<0.21.0 httplib2>=0.9.2,<0.21.0
@ -13,7 +13,7 @@ rsa>=3.4.2,<4.9.0
# Gmail # Gmail
google-auth-oauthlib>=0.4.3,<0.6.0 google-auth-oauthlib>=0.4.3,<0.6.0
google-api-python-client>=1.7.11,<2.41.0 google-api-python-client>=1.7.11,<2.42.0
# goodreads # goodreads
goodreads>=0.3.2,<0.4.0 goodreads>=0.3.2,<0.4.0

View File

@ -37,20 +37,20 @@
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> <div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
<p class='text-justify attribute'><strong>Start Time: </strong>2022-03-14 21:13:33</p> <p class='text-justify attribute'><strong>Start Time: </strong>2022-03-19 22:04:05</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Stop Time: </strong>2022-03-15 02:05:20</p> <p class='text-justify attribute'><strong>Stop Time: </strong>2022-03-20 02:50:09</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Duration: </strong>4h 3 min</p> <p class='text-justify attribute'><strong>Duration: </strong>3h 58 min</p>
</div> </div>
</div> </div>
</div> </div>
@ -1589,8 +1589,8 @@
<tr id="su" class="failClass"> <tr id="su" class="failClass">
<td>TestEditBooksOnGdrive</td> <td>TestEditBooksOnGdrive</td>
<td class="text-center">20</td> <td class="text-center">20</td>
<td class="text-center">17</td> <td class="text-center">16</td>
<td class="text-center">3</td> <td class="text-center">4</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
@ -1735,11 +1735,31 @@
<tr id='pt16.16' class='hiddenRow bg-success'> <tr id="ft16.16" class="none bg-danger">
<td> <td>
<div class='testcase'>TestEditBooksOnGdrive - test_edit_title</div> <div class='testcase'>TestEditBooksOnGdrive - test_edit_title</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft16.16')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft16.16" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_ft16.16').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 145, in test_edit_title
self.assertTrue(self.check_element_on_page((By.ID, &#39;flash_success&#39;)))
AssertionError: False is not true</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr> </tr>
@ -1761,7 +1781,7 @@
</div> </div>
<div class="text-left pull-left"> <div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last): <pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 853, in test_upload_book_epub File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 855, in test_upload_book_epub
self.assertEqual(&#39;8936&#39;, resp.headers[&#39;Content-Length&#39;]) self.assertEqual(&#39;8936&#39;, resp.headers[&#39;Content-Length&#39;])
AssertionError: &#39;8936&#39; != &#39;1103&#39; AssertionError: &#39;8936&#39; != &#39;1103&#39;
- 8936 - 8936
@ -1801,7 +1821,7 @@ AssertionError: &#39;8936&#39; != &#39;1103&#39;
</div> </div>
<div class="text-left pull-left"> <div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last): <pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 766, in test_upload_cover_hdd File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 768, in test_upload_cover_hdd
self.assertGreater(diff(&#39;original.png&#39;, &#39;jpeg.png&#39;, delete_diff_file=True), 0.02) self.assertGreater(diff(&#39;original.png&#39;, &#39;jpeg.png&#39;, delete_diff_file=True), 0.02)
AssertionError: 0.0 not greater than 0.02</pre> AssertionError: 0.0 not greater than 0.02</pre>
</div> </div>
@ -1830,9 +1850,9 @@ AssertionError: 0.0 not greater than 0.02</pre>
</div> </div>
<div class="text-left pull-left"> <div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last): <pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 935, in test_watch_metadata File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py&#34;, line 937, in test_watch_metadata
self.assertNotIn(&#39;series&#39;, book) self.assertNotIn(&#39;series&#39;, book)
AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;reader&#39;: [], &#39;title&#39;: &#39;testbook&#39;, &#39;author&#39;: [&#39;John Döe&#39;], &#39;rating&#39;: 0, &#39;languages&#39;: [&#39;English&#39;], &#39;identifier&#39;: [], &#39;cover&#39;: &#39;/cover/5?edit=2e081d1c-86d2-461f-a309-e51e1e378161&#39;, &#39;tag&#39;: [], &#39;publisher&#39;: [&#39;Randomhäus&#39;], &#39;pubdate&#39;: &#39;Jan 19, 2017&#39;, &#39;comment&#39;: &#39;Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Aenean commodo ligula eget dolor.Aenean massa.Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.Nulla consequat massa quis enim.Donec pede justo, fringilla vel, aliquet nec, vulputate&#39;, &#39;add_shelf&#39;: [], &#39;del_shelf&#39;: [], &#39;edit_enable&#39;: True, &#39;kindle&#39;: None, &#39;kindlebtn&#39;: None, &#39;download&#39;: [&#39;EPUB (6.7 kB)&#39;], &#39;read&#39;: False, &#39;archived&#39;: False, &#39;series_all&#39;: &#39;Book 1 of test&#39;, &#39;series_index&#39;: &#39;1&#39;, &#39;series&#39;: &#39;test&#39;, &#39;cust_columns&#39;: []}</pre> AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;reader&#39;: [], &#39;title&#39;: &#39;testbook&#39;, &#39;author&#39;: [&#39;John Döe&#39;], &#39;rating&#39;: 0, &#39;languages&#39;: [&#39;English&#39;], &#39;identifier&#39;: [], &#39;cover&#39;: &#39;/cover/5?edit=34e51cc2-2413-4a23-8324-26d568a421ba&#39;, &#39;tag&#39;: [], &#39;publisher&#39;: [&#39;Randomhäus&#39;], &#39;pubdate&#39;: &#39;Jan 19, 2017&#39;, &#39;comment&#39;: &#39;Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Aenean commodo ligula eget dolor.Aenean massa.Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.Nulla consequat massa quis enim.Donec pede justo, fringilla vel, aliquet nec, vulputate&#39;, &#39;add_shelf&#39;: [], &#39;del_shelf&#39;: [], &#39;edit_enable&#39;: True, &#39;kindle&#39;: None, &#39;kindlebtn&#39;: None, &#39;download&#39;: [&#39;EPUB (6.7 kB)&#39;], &#39;read&#39;: False, &#39;archived&#39;: False, &#39;series_all&#39;: &#39;Book 1 of test&#39;, &#39;series_index&#39;: &#39;1&#39;, &#39;series&#39;: &#39;test&#39;, &#39;cust_columns&#39;: []}</pre>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
@ -2825,34 +2845,65 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
<tr id="su" class="passClass"> <tr id="su" class="errorClass">
<td>TestMergeBooksList</td> <td>_ErrorHolder</td>
<td class="text-center">2</td> <td class="text-center">1</td>
<td class="text-center">2</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c31', 2)">Detail</a> <a onclick="showClassDetail('c31', 1)">Detail</a>
</td> </td>
</tr> </tr>
<tr id='pt31.1' class='hiddenRow bg-success'> <tr id="et31.1" class="none bg-info">
<td> <td>
<div class='testcase'>TestMergeBooksList - test_book_merge</div> <div class='testcase'>setUpClass (test_merge_books_list)</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6'>
</tr> <div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et31.1')">ERROR</a>
</div>
<!--css div popup start-->
<div id="div_et31.1" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_et31.1').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_merge_books_list.py&#34;, line 24, in setUpClass
startup(cls, cls.py_version, {&#39;config_calibre_dir&#39;: TEST_DB})
File &#34;/home/ozzie/Development/calibre-web-test/test/helper_func.py&#34;, line 175, in startup
inst.driver = webdriver.Firefox()
File &#34;/home/ozzie/Development/calibre-web-test/venv/lib/python3.8/site-packages/selenium/webdriver/firefox/webdriver.py&#34;, line 178, in __init__
RemoteWebDriver.__init__(
File &#34;/home/ozzie/Development/calibre-web-test/venv/lib/python3.8/site-packages/selenium/webdriver/remote/webdriver.py&#34;, line 269, in __init__
self.start_session(capabilities, browser_profile)
File &#34;/home/ozzie/Development/calibre-web-test/venv/lib/python3.8/site-packages/selenium/webdriver/remote/webdriver.py&#34;, line 360, in start_session
response = self.execute(Command.NEW_SESSION, parameters)
File &#34;/home/ozzie/Development/calibre-web-test/venv/lib/python3.8/site-packages/selenium/webdriver/remote/webdriver.py&#34;, line 425, in execute
self.error_handler.check_response(response)
File &#34;/home/ozzie/Development/calibre-web-test/venv/lib/python3.8/site-packages/selenium/webdriver/remote/errorhandler.py&#34;, line 247, in check_response
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.WebDriverException: Message: Process unexpectedly closed with status 0
During handling of the above exception, another exception occurred:
<tr id='pt31.2' class='hiddenRow bg-success'> Traceback (most recent call last):
<td> File &#34;/home/ozzie/Development/calibre-web-test/test/test_merge_books_list.py&#34;, line 28, in setUpClass
<div class='testcase'>TestMergeBooksList - test_delete_book</div> cls.driver.quit()
AttributeError: &#39;NoneType&#39; object has no attribute &#39;quit&#39;</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td> </td>
<td colspan='6' align='center'>PASS</td>
</tr> </tr>
@ -4600,10 +4651,10 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
<tr id='total_row' class="text-center bg-grey"> <tr id='total_row' class="text-center bg-grey">
<td>Total</td> <td>Total</td>
<td>405</td> <td>404</td>
<td>396</td> <td>393</td>
<td>3</td> <td>4</td>
<td>0</td> <td>1</td>
<td>6</td> <td>6</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
@ -4776,7 +4827,7 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.40.0</td> <td>2.41.0</td>
<td>TestCliGdrivedb</td> <td>TestCliGdrivedb</td>
</tr> </tr>
@ -4806,7 +4857,7 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.40.0</td> <td>2.41.0</td>
<td>TestEbookConvertCalibreGDrive</td> <td>TestEbookConvertCalibreGDrive</td>
</tr> </tr>
@ -4836,7 +4887,7 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.40.0</td> <td>2.41.0</td>
<td>TestEbookConvertGDriveKepubify</td> <td>TestEbookConvertGDriveKepubify</td>
</tr> </tr>
@ -4878,7 +4929,7 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.40.0</td> <td>2.41.0</td>
<td>TestEditAuthorsGdrive</td> <td>TestEditAuthorsGdrive</td>
</tr> </tr>
@ -4914,7 +4965,7 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.40.0</td> <td>2.41.0</td>
<td>TestEditBooksOnGdrive</td> <td>TestEditBooksOnGdrive</td>
</tr> </tr>
@ -4956,7 +5007,7 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.40.0</td> <td>2.41.0</td>
<td>TestSetupGdrive</td> <td>TestSetupGdrive</td>
</tr> </tr>
@ -5046,7 +5097,7 @@ AssertionError: &#39;series&#39; unexpectedly found in {&#39;id&#39;: 5, &#39;re
</div> </div>
<script> <script>
drawCircle(396, 3, 0, 6); drawCircle(393, 4, 1, 6);
showCase(5); showCase(5);
</script> </script>