From 9bcbe523d7660c4d14bf2742ee998212cbe230d6 Mon Sep 17 00:00:00 2001 From: Thore Schillmann Date: Fri, 22 Jul 2022 08:58:28 +0000 Subject: [PATCH 1/9] (draft) metadata embedding when sending to device --- cps/constants.py | 2 +- cps/helper.py | 13 ++++++++++++- cps/tasks/convert.py | 10 ++++++++++ cps/tasks/mail.py | 27 ++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/cps/constants.py b/cps/constants.py index b613d0aa..d8b05cb3 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -154,7 +154,7 @@ EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr' _extension = "" if sys.platform == "win32": _extension = ".exe" -SUPPORTED_CALIBRE_BINARIES = {binary:binary + _extension for binary in ["ebook-convert", "calibredb"]} +SUPPORTED_CALIBRE_BINARIES = {binary:binary + _extension for binary in ["ebook-convert", "calibredb", "ebook-meta"]} def has_flag(value, bit_flag): diff --git a/cps/helper.py b/cps/helper.py index a16245fa..80056244 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -54,7 +54,7 @@ from .tasks.convert import TaskConvert from . import logger, config, db, ub, fs from . import gdriveutils as gd from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES, SUPPORTED_CALIBRE_BINARIES -from .subproc_wrapper import process_wait +from .subproc_wrapper import process_wait, process_open from .services.worker import WorkerThread from .tasks.mail import TaskEmail from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails @@ -213,6 +213,17 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id): # returns None if success, otherwise errormessage return convert_book_format(book_id, calibrepath, u'azw3', book_format.lower(), user_id, kindle_mail) + # ToDo: Delete when OPF creation has been implemented + if config.config_binariesdir: + quotes = [3, 5] + calibredb_binarypath = os.path.join(config.config_binariesdir, SUPPORTED_CALIBRE_BINARIES["calibredb"]) + opf_command = [calibredb_binarypath, 'show_metadata', '--as-opf', str(book_id), '--with-library', config.config_calibre_dir] + p = process_open(opf_command, quotes) + p.wait() + path_opf = os.path.join(config.config_calibre_dir, book.path, "metadata.opf") + with open(path_opf, 'w') as fd: + shutil.copyfileobj(p.stdout, fd) + for entry in iter(book.data): if entry.format.upper() == book_format.upper(): converted_file_name = entry.name + '.' + book_format.lower() diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py index 79d7ddbd..23af302a 100644 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -93,6 +93,16 @@ class TaskConvert(CalibreTask): # todo: figure out how to incorporate this into the progress try: EmailText = N_(u"%(book)s send to Kindle", book=escape(self.title)) + # ToDo: Delete when OPF creation has been implemented + if config.config_binariesdir: + quotes = [3, 5] + calibredb_binarypath = os.path.join(config.config_binariesdir, SUPPORTED_CALIBRE_BINARIES["calibredb"]) + opf_command = [calibredb_binarypath, 'show_metadata', '--as-opf', str(self.book_id), '--with-library', config.config_calibre_dir] + p = process_open(opf_command, quotes) + p.wait() + path_opf = os.path.join(config.config_calibre_dir, cur_book.path, "metadata.opf") + with open(path_opf, 'w') as fd: + copyfileobj(p.stdout, fd) worker_thread.add(self.user, TaskEmail(self.settings['subject'], self.results["path"], filename, diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py index be240c79..6c94faee 100755 --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -20,6 +20,7 @@ import os import smtplib import threading import socket +from shutil import copy import mimetypes from io import StringIO @@ -32,6 +33,8 @@ from email.utils import formatdate from cps.services.worker import CalibreTask from cps.services import gmail from cps import logger, config +from cps.subproc_wrapper import process_open +from cps.constants import SUPPORTED_CALIBRE_BINARIES from cps import gdriveutils import uuid @@ -245,15 +248,23 @@ class TaskEmail(CalibreTask): df.GetContentFile(datafile) else: return None + if config.config_binariesdir: + datafile = cls._embed_metadata(calibre_path, book_path, filename, datafile) + os.remove(os.path.join(calibre_path, book_path, filename)) file_ = open(datafile, 'rb') data = file_.read() file_.close() os.remove(datafile) else: + datafile = os.path.join(calibre_path, book_path, filename) try: - file_ = open(os.path.join(calibre_path, book_path, filename), 'rb') + if config.config_binariesdir: + datafile = cls._embed_metadata(calibre_path, book_path, filename, datafile) + file_ = open(datafile, 'rb') data = file_.read() file_.close() + if config.config_binariesdir: + os.remove(datafile) except IOError as e: log.error_or_exception(e, stacklevel=3) log.error(u'The requested file could not be read. Maybe wrong permissions?') @@ -270,3 +281,17 @@ class TaskEmail(CalibreTask): def __str__(self): return "E-mail {}, {}".format(self.name, self.subject) + + def _embed_metadata(self, calibre_path, book_path, filename, datafile): + datafile_tmp = os.path.join(calibre_path, book_path, "tmp_" + filename) + path_opf = os.path.join(calibre_path, book_path, "metadata.opf") + copy(datafile, datafile_tmp) + + calibredb_binarypath = os.path.join(config.config_binariesdir, SUPPORTED_CALIBRE_BINARIES["ebook-meta"]) + opf_command = [calibredb_binarypath, datafile_tmp, "--from-opf", path_opf] + p = process_open(opf_command) + _, err = p.communicate() + if err: + # ToDo: Improve error handling + log.error('Metadata embedder encountered an error: %s', err) + return datafile_tmp From ff9e1ed7c8d2a810c0ddd371fffbe029724b32d9 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 24 Feb 2024 10:57:10 +0100 Subject: [PATCH 2/9] Implemented embed metadata on send to ereader --- cps/admin.py | 3 ++- cps/embed_helper.py | 55 ++++++++++++++++++++++++++++++++++++++++++++ cps/helper.py | 37 +++-------------------------- cps/tasks/convert.py | 1 + cps/tasks/mail.py | 54 +++++++++++++++---------------------------- 5 files changed, 80 insertions(+), 70 deletions(-) create mode 100644 cps/embed_helper.py diff --git a/cps/admin.py b/cps/admin.py index fa29759e..9b504094 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -47,7 +47,8 @@ from . import constants, logger, helper, services, cli_param from . import db, calibre_db, ub, web_server, config, updater_thread, gdriveutils, \ kobo_sync_status, schedule from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \ - valid_email, check_username, get_calibre_binarypath + valid_email, check_username +from .embed_helper import get_calibre_binarypath from .gdriveutils import is_gdrive_ready, gdrive_support from .render_template import render_title_template, get_sidebar_config from .services.worker import WorkerThread diff --git a/cps/embed_helper.py b/cps/embed_helper.py new file mode 100644 index 00000000..f5c3a084 --- /dev/null +++ b/cps/embed_helper.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2024 OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from uuid import uuid4 +import os + +from .file_helper import get_temp_dir +from .subproc_wrapper import process_open +from . import logger, config +from .constants import SUPPORTED_CALIBRE_BINARIES + +log = logger.create() + +def do_calibre_export(book_id, book_format): + try: + quotes = [3, 5, 7, 9] + tmp_dir = get_temp_dir() + calibredb_binarypath = get_calibre_binarypath("calibredb") + temp_file_name = str(uuid4()) + opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', config.config_calibre_dir, + '--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name), + str(book_id)] + p = process_open(opf_command, quotes) + _, err = p.communicate() + if err: + log.error('Metadata embedder encountered an error: %s', err) + return tmp_dir, temp_file_name + except OSError as ex: + # ToDo real error handling + log.error_or_exception(ex) + return None, None + +def get_calibre_binarypath(binary): + binariesdir = config.config_binariesdir + if binariesdir: + try: + return os.path.join(binariesdir, SUPPORTED_CALIBRE_BINARIES[binary]) + except KeyError as ex: + log.error("Binary not supported by Calibre-Web: %s", SUPPORTED_CALIBRE_BINARIES[binary]) + pass + return "" diff --git a/cps/helper.py b/cps/helper.py index 975a2523..33da0f37 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -28,7 +28,6 @@ from datetime import datetime, timedelta import requests import unidecode from uuid import uuid4 -from lxml import etree from flask import send_from_directory, make_response, redirect, abort, url_for from flask_babel import gettext as _ @@ -56,13 +55,14 @@ from .tasks.convert import TaskConvert from . import logger, config, db, ub, fs from . import gdriveutils as gd from .constants import STATIC_DIR as _STATIC_DIR, CACHE_TYPE_THUMBNAILS, THUMBNAIL_TYPE_COVER, THUMBNAIL_TYPE_SERIES, SUPPORTED_CALIBRE_BINARIES -from .subproc_wrapper import process_wait, process_open +from .subproc_wrapper import process_wait from .services.worker import WorkerThread from .tasks.mail import TaskEmail from .tasks.thumbnail import TaskClearCoverThumbnailCache, TaskGenerateCoverThumbnails from .tasks.metadata_backup import TaskBackupMetadata from .file_helper import get_temp_dir from .epub_helper import get_content_opf, create_new_metadata_backup, updateEpub, replace_metadata +from .embed_helper import do_calibre_export log = logger.create() @@ -225,7 +225,7 @@ def send_mail(book_id, book_format, convert, ereader_mail, calibrepath, user_id) email_text = N_("%(book)s send to eReader", book=link) WorkerThread.add(user_id, TaskEmail(_("Send to eReader"), book.path, converted_file_name, config.get_mail_settings(), ereader_mail, - email_text, _('This Email has been sent via Calibre-Web.'))) + email_text, _('This Email has been sent via Calibre-Web.'),book.id)) return return _("The requested file could not be read. Maybe wrong permissions?") @@ -1001,26 +1001,6 @@ def do_kepubify_metadata_replace(book, file_path): return tmp_dir, temp_file_name -def do_calibre_export(book_id, book_format, ): - try: - quotes = [3, 5, 7, 9] - tmp_dir = get_temp_dir() - calibredb_binarypath = get_calibre_binarypath("calibredb") - temp_file_name = str(uuid4()) - opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--with-library', config.config_calibre_dir, - '--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name), - str(book_id)] - p = process_open(opf_command, quotes) - _, err = p.communicate() - if err: - log.error('Metadata embedder encountered an error: %s', err) - return tmp_dir, temp_file_name - except OSError as ex: - # ToDo real error handling - log.error_or_exception(ex) - return None, None - - ################################## @@ -1142,17 +1122,6 @@ def get_download_link(book_id, book_format, client): abort(404) -def get_calibre_binarypath(binary): - binariesdir = config.config_binariesdir - if binariesdir: - try: - return os.path.join(binariesdir, SUPPORTED_CALIBRE_BINARIES[binary]) - except KeyError as ex: - log.error("Binary not supported by Calibre-Web: %s", SUPPORTED_CALIBRE_BINARIES[binary]) - pass - return "" - - def clear_cover_thumbnail_cache(book_id): if config.schedule_generate_book_covers: WorkerThread.add(None, TaskClearCoverThumbnailCache(book_id), hidden=True) diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py index 8cb29197..f32c4581 100644 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -110,6 +110,7 @@ class TaskConvert(CalibreTask): self.ereader_mail, EmailText, self.settings['body'], + id=self.book_id, internal=True) ) except Exception as ex: diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py index 257df4e9..39ad919f 100644 --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -21,7 +21,6 @@ import smtplib import ssl import threading import socket -from shutil import copy import mimetypes from io import StringIO @@ -29,14 +28,11 @@ from email.message import EmailMessage from email.utils import formatdate, parseaddr from email.generator import Generator from flask_babel import lazy_gettext as N_ -from email.utils import formatdate from cps.services.worker import CalibreTask from cps.services import gmail +from cps.embed_helper import do_calibre_export from cps import logger, config -from cps.subproc_wrapper import process_open -from cps.constants import SUPPORTED_CALIBRE_BINARIES - from cps import gdriveutils import uuid @@ -113,7 +109,7 @@ class EmailSSL(EmailBase, smtplib.SMTP_SSL): class TaskEmail(CalibreTask): - def __init__(self, subject, filepath, attachment, settings, recipient, task_message, text, internal=False): + def __init__(self, subject, filepath, attachment, settings, recipient, task_message, text, id=0, internal=False): super(TaskEmail, self).__init__(task_message) self.subject = subject self.attachment = attachment @@ -122,6 +118,7 @@ class TaskEmail(CalibreTask): self.recipient = recipient self.text = text self.asyncSMTP = None + self.book_id = id self.results = dict() # from calibre code: @@ -144,7 +141,7 @@ class TaskEmail(CalibreTask): message['To'] = self.recipient message['Subject'] = self.subject message['Date'] = formatdate(localtime=True) - message['Message-Id'] = "{}@{}".format(uuid.uuid4(), self.get_msgid_domain()) # f"<{uuid.uuid4()}@{get_msgid_domain(from_)}>" # make_msgid('calibre-web') + message['Message-Id'] = "{}@{}".format(uuid.uuid4(), self.get_msgid_domain()) message.set_content(self.text.encode('UTF-8'), "text", "plain") if self.attachment: data = self._get_attachment(self.filepath, self.attachment) @@ -164,6 +161,8 @@ class TaskEmail(CalibreTask): try: # create MIME message msg = self.prepare_message() + if not msg: + return if self.settings['mail_server_type'] == 0: self.send_standard_email(msg) else: @@ -239,10 +238,10 @@ class TaskEmail(CalibreTask): self.asyncSMTP = None self._progress = x - @classmethod - def _get_attachment(cls, book_path, filename): + def _get_attachment(self, book_path, filename): """Get file as MIMEBase message""" calibre_path = config.get_book_path() + extension = os.path.splitext(filename)[1][1:] if config.config_use_google_drive: df = gdriveutils.getFileFromEbooksFolder(book_path, filename) if df: @@ -252,22 +251,21 @@ class TaskEmail(CalibreTask): df.GetContentFile(datafile) else: return None - if config.config_binariesdir: - datafile = cls._embed_metadata(calibre_path, book_path, filename, datafile) - os.remove(os.path.join(calibre_path, book_path, filename)) - file_ = open(datafile, 'rb') - data = file_.read() - file_.close() + if config.config_binariesdir and config.config_embed_metadata: + data_path, data_file = do_calibre_export(self.book_id, extension) + datafile = os.path.join(data_path, data_file + "." + extension) + with open(datafile, 'rb') as file_: + data = file_.read() os.remove(datafile) else: datafile = os.path.join(calibre_path, book_path, filename) try: - if config.config_binariesdir: - datafile = cls._embed_metadata(calibre_path, book_path, filename, datafile) - file_ = open(datafile, 'rb') - data = file_.read() - file_.close() - if config.config_binariesdir: + if config.config_binariesdir and config.config_embed_metadata: + data_path, data_file = do_calibre_export(self.book_id, extension) + datafile = os.path.join(data_path, data_file + "." + extension) + with open(datafile, 'rb') as file_: + data = file_.read() + if config.config_binariesdir and config.config_embed_metadata: os.remove(datafile) except IOError as e: log.error_or_exception(e, stacklevel=3) @@ -285,17 +283,3 @@ class TaskEmail(CalibreTask): def __str__(self): return "E-mail {}, {}".format(self.name, self.subject) - - def _embed_metadata(self, calibre_path, book_path, filename, datafile): - datafile_tmp = os.path.join(calibre_path, book_path, "tmp_" + filename) - path_opf = os.path.join(calibre_path, book_path, "metadata.opf") - copy(datafile, datafile_tmp) - - calibredb_binarypath = os.path.join(config.config_binariesdir, SUPPORTED_CALIBRE_BINARIES["ebook-meta"]) - opf_command = [calibredb_binarypath, datafile_tmp, "--from-opf", path_opf] - p = process_open(opf_command) - _, err = p.communicate() - if err: - # ToDo: Improve error handling - log.error('Metadata embedder encountered an error: %s', err) - return datafile_tmp From 117c92233d1a498dbe006ad0f7f43fa846b8b33c Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 24 Feb 2024 18:45:57 +0100 Subject: [PATCH 3/9] Added sending email to embed metadata text Updated test result --- cps/templates/config_db.html | 2 +- cps/templates/config_edit.html | 2 +- test/Calibre-Web TestSummary_Linux.html | 563 +++++++++++++++--------- 3 files changed, 354 insertions(+), 213 deletions(-) diff --git a/cps/templates/config_db.html b/cps/templates/config_db.html index 5b6ac7ff..fc0606c9 100644 --- a/cps/templates/config_db.html +++ b/cps/templates/config_db.html @@ -18,7 +18,7 @@
- +
diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index d83831db..89d4abcb 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -105,7 +105,7 @@
- +
diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 077d4836..bf5558ad 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2024-02-12 21:01:14

+

Start Time: 2024-02-24 19:10:31

-

Stop Time: 2024-02-13 03:55:47

+

Stop Time: 2024-02-25 02:03:41

-

Duration: 5h 44 min

+

Duration: 5h 43 min

@@ -237,8 +237,8 @@ TestBackupMetadata 21 - 19 - 2 + 20 + 1 0 0 @@ -266,11 +266,31 @@ - +
TestBackupMetadata - test_backup_change_book_description
- PASS + +
+ FAIL +
+ + + + @@ -365,60 +385,20 @@ - +
TestBackupMetadata - test_backup_change_custom_bool
- -
- FAIL -
- - - - + PASS - +
TestBackupMetadata - test_backup_change_custom_categories
- -
- FAIL -
- - - - + PASS @@ -2007,12 +1987,12 @@ IndexError: list index out of range - + TestEditBooksOnGdrive 18 - 18 - 0 + 17 0 + 1 0 Detail @@ -2030,11 +2010,31 @@ IndexError: list index out of range - +
TestEditBooksOnGdrive - test_edit_author
- PASS + +
+ ERROR +
+ + + + @@ -2330,13 +2330,13 @@ IndexError: list index out of range TestEmbedMetadata - 5 - 5 + 6 + 6 0 0 0 - Detail + Detail @@ -2386,6 +2386,15 @@ IndexError: list index out of range + + + +
TestEmbedMetadata - test_email_epub_embed_metadata
+ + PASS + + + @@ -2998,13 +3007,13 @@ IndexError: list index out of range TestCalibreWebListOrders - 10 - 10 + 16 + 16 0 0 0 - Detail + Detail @@ -3039,7 +3048,7 @@ IndexError: list index out of range -
TestCalibreWebListOrders - test_lang_sort
+
TestCalibreWebListOrders - test_formats_click_none
PASS @@ -3048,7 +3057,7 @@ IndexError: list index out of range -
TestCalibreWebListOrders - test_order_authors_all_links
+
TestCalibreWebListOrders - test_lang_sort
PASS @@ -3057,7 +3066,7 @@ IndexError: list index out of range -
TestCalibreWebListOrders - test_order_series_all_links
+
TestCalibreWebListOrders - test_language_click_none
PASS @@ -3066,7 +3075,7 @@ IndexError: list index out of range -
TestCalibreWebListOrders - test_publisher_sort
+
TestCalibreWebListOrders - test_order_authors_all_links
PASS @@ -3075,7 +3084,7 @@ IndexError: list index out of range -
TestCalibreWebListOrders - test_ratings_sort
+
TestCalibreWebListOrders - test_order_series_all_links
PASS @@ -3084,7 +3093,7 @@ IndexError: list index out of range -
TestCalibreWebListOrders - test_series_sort
+
TestCalibreWebListOrders - test_publisher_click_none
PASS @@ -3092,6 +3101,60 @@ IndexError: list index out of range + +
TestCalibreWebListOrders - test_publisher_sort
+ + PASS + + + + + + +
TestCalibreWebListOrders - test_ratings_click_none
+ + PASS + + + + + + +
TestCalibreWebListOrders - test_ratings_sort
+ + PASS + + + + + + +
TestCalibreWebListOrders - test_series_click_none
+ + PASS + + + + + + +
TestCalibreWebListOrders - test_series_sort
+ + PASS + + + + + + +
TestCalibreWebListOrders - test_tags_click_none
+ + PASS + + + + +
TestCalibreWebListOrders - test_tags_sort
@@ -4232,6 +4295,84 @@ AssertionError: False is not true + + TestSplitLibrary + 7 + 7 + 0 + 0 + 0 + + Detail + + + + + + + +
TestSplitLibrary - test_change_ebook
+ + PASS + + + + + + +
TestSplitLibrary - test_convert_ebook
+ + PASS + + + + + + +
TestSplitLibrary - test_download_book
+ + PASS + + + + + + +
TestSplitLibrary - test_email_ebook
+ + PASS + + + + + + +
TestSplitLibrary - test_kobo
+ + PASS + + + + + + +
TestSplitLibrary - test_thumbnails
+ + PASS + + + + + + +
TestSplitLibrary - test_upload_ebook
+ + PASS + + + + + TestSystemdActivation 1 @@ -4240,13 +4381,13 @@ AssertionError: False is not true 0 0 - Detail + Detail - +
TestSystemdActivation - test_systemd_activation
@@ -4264,13 +4405,13 @@ AssertionError: False is not true 0 0 - Detail + Detail - +
TestThumbnailsEnv - test_cover_cache_env_on_database_change
@@ -4288,13 +4429,13 @@ AssertionError: False is not true 0 1 - Detail + Detail - +
TestThumbnails - test_cache_non_writable
@@ -4303,7 +4444,7 @@ AssertionError: False is not true - +
TestThumbnails - test_cache_of_deleted_book
@@ -4312,7 +4453,7 @@ AssertionError: False is not true - +
TestThumbnails - test_cover_cache_on_database_change
@@ -4321,7 +4462,7 @@ AssertionError: False is not true - +
TestThumbnails - test_cover_change_on_upload_new_cover
@@ -4330,7 +4471,7 @@ AssertionError: False is not true - +
TestThumbnails - test_cover_for_series
@@ -4339,7 +4480,7 @@ AssertionError: False is not true - +
TestThumbnails - test_cover_on_upload_book
@@ -4348,7 +4489,7 @@ AssertionError: False is not true - +
TestThumbnails - test_remove_cover_from_cache
@@ -4357,7 +4498,7 @@ AssertionError: False is not true - +
TestThumbnails - test_sideloaded_book
@@ -4375,13 +4516,13 @@ AssertionError: False is not true 0 1 - Detail + Detail - +
TestUpdater - test_check_update_nightly_errors
@@ -4390,7 +4531,7 @@ AssertionError: False is not true - +
TestUpdater - test_check_update_nightly_request_errors
@@ -4399,7 +4540,7 @@ AssertionError: False is not true - +
TestUpdater - test_check_update_stable_errors
@@ -4408,7 +4549,7 @@ AssertionError: False is not true - +
TestUpdater - test_check_update_stable_versions
@@ -4417,7 +4558,7 @@ AssertionError: False is not true - +
TestUpdater - test_perform_update
@@ -4426,7 +4567,7 @@ AssertionError: False is not true - +
TestUpdater - test_perform_update_stable_errors
@@ -4435,19 +4576,19 @@ AssertionError: False is not true - +
TestUpdater - test_perform_update_timeout
- SKIP + SKIP
-