' % self.id
+def filename(context):
+ file_format = context.get_current_parameters()['format']
+ if file_format == 'jpeg':
+ return context.get_current_parameters()['uuid'] + '.jpg'
+ else:
+ return context.get_current_parameters()['uuid'] + '.' + file_format
+
+
+class Thumbnail(Base):
+ __tablename__ = 'thumbnail'
+
+ id = Column(Integer, primary_key=True)
+ entity_id = Column(Integer)
+ uuid = Column(String, default=lambda: str(uuid.uuid4()), unique=True)
+ format = Column(String, default='jpeg')
+ type = Column(SmallInteger, default=constants.THUMBNAIL_TYPE_COVER)
+ resolution = Column(SmallInteger, default=constants.COVER_THUMBNAIL_SMALL)
+ filename = Column(String, default=filename)
+ generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow())
+ expiration = Column(DateTime, nullable=True)
+
+
# Add missing tables during migration of database
-def add_missing_tables(engine, session):
+def add_missing_tables(engine, _session):
if not engine.dialect.has_table(engine.connect(), "book_read_link"):
ReadBook.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "bookmark"):
@@ -523,30 +549,32 @@ def add_missing_tables(engine, session):
KoboStatistics.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "archived_book"):
ArchivedBook.__table__.create(bind=engine)
+ if not engine.dialect.has_table(engine.connect(), "thumbnail"):
+ Thumbnail.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "registration"):
Registration.__table__.create(bind=engine)
with engine.connect() as conn:
conn.execute("insert into registration (domain, allow) values('%.%',1)")
- session.commit()
+ _session.commit()
# migrate all settings missing in registration table
-def migrate_registration_table(engine, session):
+def migrate_registration_table(engine, _session):
try:
- session.query(exists().where(Registration.allow)).scalar()
- session.commit()
+ _session.query(exists().where(Registration.allow)).scalar()
+ _session.commit()
except exc.OperationalError: # Database is not compatible, some columns are missing
with engine.connect() as conn:
conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER")
conn.execute("update registration set 'allow' = 1")
- session.commit()
+ _session.commit()
try:
# Handle table exists, but no content
- cnt = session.query(Registration).count()
+ cnt = _session.query(Registration).count()
if not cnt:
with engine.connect() as conn:
conn.execute("insert into registration (domain, allow) values('%.%',1)")
- session.commit()
+ _session.commit()
except exc.OperationalError: # Database is not writeable
print('Settings database is not writeable. Exiting...')
sys.exit(2)
@@ -564,9 +592,9 @@ def migrate_guest_password(engine):
sys.exit(2)
-def migrate_shelfs(engine, session):
+def migrate_shelfs(engine, _session):
try:
- session.query(exists().where(Shelf.uuid)).scalar()
+ _session.query(exists().where(Shelf.uuid)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE shelf ADD column 'uuid' STRING")
@@ -574,33 +602,33 @@ def migrate_shelfs(engine, session):
conn.execute("ALTER TABLE shelf ADD column 'last_modified' DATETIME")
conn.execute("ALTER TABLE book_shelf_link ADD column 'date_added' DATETIME")
conn.execute("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false")
- for shelf in session.query(Shelf).all():
+ for shelf in _session.query(Shelf).all():
shelf.uuid = str(uuid.uuid4())
shelf.created = datetime.datetime.now()
shelf.last_modified = datetime.datetime.now()
- for book_shelf in session.query(BookShelf).all():
+ for book_shelf in _session.query(BookShelf).all():
book_shelf.date_added = datetime.datetime.now()
- session.commit()
+ _session.commit()
try:
- session.query(exists().where(Shelf.kobo_sync)).scalar()
+ _session.query(exists().where(Shelf.kobo_sync)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE shelf ADD column 'kobo_sync' BOOLEAN DEFAULT false")
- session.commit()
+ _session.commit()
try:
- session.query(exists().where(BookShelf.order)).scalar()
+ _session.query(exists().where(BookShelf.order)).scalar()
except exc.OperationalError: # Database is not compatible, some columns are missing
with engine.connect() as conn:
conn.execute("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1")
- session.commit()
+ _session.commit()
-def migrate_readBook(engine, session):
+def migrate_readBook(engine, _session):
try:
- session.query(exists().where(ReadBook.read_status)).scalar()
+ _session.query(exists().where(ReadBook.read_status)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0")
@@ -608,46 +636,46 @@ def migrate_readBook(engine, session):
conn.execute("ALTER TABLE book_read_link ADD column 'last_modified' DATETIME")
conn.execute("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME")
conn.execute("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0")
- session.commit()
- test = session.query(ReadBook).filter(ReadBook.last_modified == None).all()
+ _session.commit()
+ test = _session.query(ReadBook).filter(ReadBook.last_modified == None).all()
for book in test:
book.last_modified = datetime.datetime.utcnow()
- session.commit()
+ _session.commit()
-def migrate_remoteAuthToken(engine, session):
+def migrate_remoteAuthToken(engine, _session):
try:
- session.query(exists().where(RemoteAuthToken.token_type)).scalar()
- session.commit()
+ _session.query(exists().where(RemoteAuthToken.token_type)).scalar()
+ _session.commit()
except exc.OperationalError: # Database is not compatible, some columns are missing
with engine.connect() as conn:
conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0")
conn.execute("update remote_auth_token set 'token_type' = 0")
- session.commit()
+ _session.commit()
# Migrate database to current version, has to be updated after every database change. Currently migration from
# everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding
# rows with SQL commands
-def migrate_Database(session):
- engine = session.bind
- add_missing_tables(engine, session)
- migrate_registration_table(engine, session)
- migrate_readBook(engine, session)
- migrate_remoteAuthToken(engine, session)
- migrate_shelfs(engine, session)
+def migrate_Database(_session):
+ engine = _session.bind
+ add_missing_tables(engine, _session)
+ migrate_registration_table(engine, _session)
+ migrate_readBook(engine, _session)
+ migrate_remoteAuthToken(engine, _session)
+ migrate_shelfs(engine, _session)
try:
create = False
- session.query(exists().where(User.sidebar_view)).scalar()
+ _session.query(exists().where(User.sidebar_view)).scalar()
except exc.OperationalError: # Database is not compatible, some columns are missing
with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1")
- session.commit()
+ _session.commit()
create = True
try:
if create:
with engine.connect() as conn:
conn.execute("SELECT language_books FROM user")
- session.commit()
+ _session.commit()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("UPDATE user SET 'sidebar_view' = (random_books* :side_random + language_books * :side_lang "
@@ -657,32 +685,32 @@ def migrate_Database(session):
'side_series': constants.SIDEBAR_SERIES, 'side_category': constants.SIDEBAR_CATEGORY,
'side_hot': constants.SIDEBAR_HOT, 'side_autor': constants.SIDEBAR_AUTHOR,
'detail_random': constants.DETAIL_RANDOM})
- session.commit()
+ _session.commit()
try:
- session.query(exists().where(User.denied_tags)).scalar()
+ _session.query(exists().where(User.denied_tags)).scalar()
except exc.OperationalError: # Database is not compatible, some columns are missing
with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `denied_tags` String DEFAULT ''")
conn.execute("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''")
conn.execute("ALTER TABLE user ADD column `denied_column_value` String DEFAULT ''")
conn.execute("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''")
- session.commit()
+ _session.commit()
try:
- session.query(exists().where(User.view_settings)).scalar()
+ _session.query(exists().where(User.view_settings)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'")
- session.commit()
+ _session.commit()
try:
- session.query(exists().where(User.kobo_only_shelves_sync)).scalar()
+ _session.query(exists().where(User.kobo_only_shelves_sync)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `kobo_only_shelves_sync` SMALLINT DEFAULT 0")
- session.commit()
+ _session.commit()
try:
# check if name is in User table instead of nickname
- session.query(exists().where(User.name)).scalar()
+ _session.query(exists().where(User.name)).scalar()
except exc.OperationalError:
# Create new table user_id and copy contents of table user into it
with engine.connect() as conn:
@@ -712,20 +740,20 @@ def migrate_Database(session):
# delete old user table and rename new user_id table to user:
conn.execute(text("DROP TABLE user"))
conn.execute(text("ALTER TABLE user_id RENAME TO user"))
- session.commit()
- if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
+ _session.commit()
+ if _session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
is None:
- create_anonymous_user(session)
+ create_anonymous_user(_session)
migrate_guest_password(engine)
-def clean_database(session):
+def clean_database(_session):
# Remove expired remote login tokens
now = datetime.datetime.now()
- session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\
+ _session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\
filter(RemoteAuthToken.token_type != 1).delete()
- session.commit()
+ _session.commit()
# Save downloaded books per user in calibre-web's own database
@@ -750,22 +778,22 @@ def delete_download(book_id):
session.rollback()
# Generate user Guest (translated text), as anonymous user, no rights
-def create_anonymous_user(session):
+def create_anonymous_user(_session):
user = User()
user.name = "Guest"
user.email = 'no@email'
user.role = constants.ROLE_ANONYMOUS
user.password = ''
- session.add(user)
+ _session.add(user)
try:
- session.commit()
+ _session.commit()
except Exception:
- session.rollback()
+ _session.rollback()
# Generate User admin with admin123 password, and access to everything
-def create_admin_user(session):
+def create_admin_user(_session):
user = User()
user.name = "admin"
user.role = constants.ADMIN_USER_ROLES
@@ -773,14 +801,22 @@ def create_admin_user(session):
user.password = generate_password_hash(constants.DEFAULT_PASSWORD)
- session.add(user)
+ _session.add(user)
try:
- session.commit()
+ _session.commit()
except Exception:
- session.rollback()
+ _session.rollback()
+
+def init_db_thread():
+ global app_DB_path
+ engine = create_engine(u'sqlite:///{0}'.format(app_DB_path), echo=False)
+
+ Session = scoped_session(sessionmaker())
+ Session.configure(bind=engine)
+ return Session()
-def init_db(app_db_path):
+def init_db(app_db_path, user_credentials=None):
# Open session for database connection
global session
global app_DB_path
@@ -801,8 +837,8 @@ def init_db(app_db_path):
create_admin_user(session)
create_anonymous_user(session)
- if cli.user_credentials:
- username, password = cli.user_credentials.split(':', 1)
+ if user_credentials:
+ username, password = user_credentials.split(':', 1)
user = session.query(User).filter(func.lower(User.name) == username.lower()).first()
if user:
if not password:
@@ -820,6 +856,16 @@ def init_db(app_db_path):
sys.exit(3)
+def get_new_session_instance():
+ new_engine = create_engine(u'sqlite:///{0}'.format(app_DB_path), echo=False)
+ new_session = scoped_session(sessionmaker())
+ new_session.configure(bind=new_engine)
+
+ atexit.register(lambda: new_session.remove() if new_session else True)
+
+ return new_session
+
+
def dispose():
global session
@@ -836,12 +882,13 @@ def dispose():
except Exception:
pass
-def session_commit(success=None):
+def session_commit(success=None, _session=None):
+ s = _session if _session else session
try:
- session.commit()
+ s.commit()
if success:
log.info(success)
except (exc.OperationalError, exc.InvalidRequestError) as e:
- session.rollback()
- log.debug_or_exception(e)
+ s.rollback()
+ log.error_or_exception(e)
return ""
diff --git a/cps/updater.py b/cps/updater.py
index 9090263f..13d774bf 100644
--- a/cps/updater.py
+++ b/cps/updater.py
@@ -28,10 +28,10 @@ from io import BytesIO
from tempfile import gettempdir
import requests
-from babel.dates import format_datetime
+from flask_babel import format_datetime
from flask_babel import gettext as _
-from . import constants, logger, config, web_server
+from . import constants, logger # config, web_server
log = logger.create()
@@ -53,22 +53,24 @@ class Updater(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.paused = False
- # self.pause_cond = threading.Condition(threading.Lock())
self.can_run = threading.Event()
self.pause()
self.status = -1
self.updateIndex = None
- # self.run()
+
+ def init_updater(self, config, web_server):
+ self.config = config
+ self.web_server = web_server
def get_current_version_info(self):
- if config.config_updatechannel == constants.UPDATE_STABLE:
+ if self.config.config_updatechannel == constants.UPDATE_STABLE:
return self._stable_version_info()
return self._nightly_version_info()
- def get_available_updates(self, request_method, locale):
- if config.config_updatechannel == constants.UPDATE_STABLE:
+ def get_available_updates(self, request_method):
+ if self.config.config_updatechannel == constants.UPDATE_STABLE:
return self._stable_available_updates(request_method)
- return self._nightly_available_updates(request_method, locale)
+ return self._nightly_available_updates(request_method)
def do_work(self):
try:
@@ -85,19 +87,19 @@ class Updater(threading.Thread):
log.debug(u'Extracting zipfile')
tmp_dir = gettempdir()
z.extractall(tmp_dir)
- foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1]
- if not os.path.isdir(foldername):
+ folder_name = os.path.join(tmp_dir, z.namelist()[0])[:-1]
+ if not os.path.isdir(folder_name):
self.status = 11
log.info(u'Extracted contents of zipfile not found in temp folder')
self.pause()
return False
self.status = 4
log.debug(u'Replacing files')
- if self.update_source(foldername, constants.BASE_DIR):
+ if self.update_source(folder_name, constants.BASE_DIR):
self.status = 6
log.debug(u'Preparing restart of server')
time.sleep(2)
- web_server.stop(True)
+ self.web_server.stop(True)
self.status = 7
time.sleep(2)
return True
@@ -119,7 +121,7 @@ class Updater(threading.Thread):
except (IOError, OSError) as ex:
self.status = 12
log.error(u'Possible Reason for error: update file could not be saved in temp dir')
- log.debug_or_exception(ex)
+ log.error_or_exception(ex)
self.pause()
return False
@@ -184,29 +186,30 @@ class Updater(threading.Thread):
return rf
@classmethod
- def check_permissions(cls, root_src_dir, root_dst_dir):
+ def check_permissions(cls, root_src_dir, root_dst_dir, log_function):
access = True
remove_path = len(root_src_dir) + 1
for src_dir, __, files in os.walk(root_src_dir):
root_dir = os.path.join(root_dst_dir, src_dir[remove_path:])
- # Skip non existing folders on check
- if not os.path.isdir(root_dir): # root_dir.lstrip(os.sep).startswith('.') or
+ # Skip non-existing folders on check
+ if not os.path.isdir(root_dir):
continue
- if not os.access(root_dir, os.R_OK|os.W_OK):
- log.debug("Missing permissions for {}".format(root_dir))
+ if not os.access(root_dir, os.R_OK | os.W_OK):
+ log_function("Missing permissions for {}".format(root_dir))
access = False
for file_ in files:
curr_file = os.path.join(root_dir, file_)
- # Skip non existing files on check
- if not os.path.isfile(curr_file): # or curr_file.startswith('.'):
+ # Skip non-existing files on check
+ if not os.path.isfile(curr_file): # or curr_file.startswith('.'):
continue
- if not os.access(curr_file, os.R_OK|os.W_OK):
- log.debug("Missing permissions for {}".format(curr_file))
+ if not os.access(curr_file, os.R_OK | os.W_OK):
+ log_function("Missing permissions for {}".format(curr_file))
access = False
return access
@classmethod
- def moveallfiles(cls, root_src_dir, root_dst_dir):
+ def move_all_files(cls, root_src_dir, root_dst_dir):
+ permission = None
new_permissions = os.stat(root_dst_dir)
log.debug('Performing Update on OS-System: %s', sys.platform)
change_permissions = not (sys.platform == "win32" or sys.platform == "darwin")
@@ -215,7 +218,7 @@ class Updater(threading.Thread):
if not os.path.exists(dst_dir):
try:
os.makedirs(dst_dir)
- log.debug('Create directory: {}', dst_dir)
+ log.debug('Create directory: {}'.format(dst_dir))
except OSError as e:
log.error('Failed creating folder: {} with error {}'.format(dst_dir, e))
if change_permissions:
@@ -234,7 +237,7 @@ class Updater(threading.Thread):
permission = os.stat(dst_file)
try:
os.remove(dst_file)
- log.debug('Remove file before copy: %s', dst_file)
+ log.debug('Remove file before copy: {}'.format(dst_file))
except OSError as e:
log.error('Failed removing file: {} with error {}'.format(dst_file, e))
else:
@@ -258,20 +261,14 @@ class Updater(threading.Thread):
def update_source(self, source, destination):
# destination files
old_list = list()
- exclude = (
- os.sep + 'app.db', os.sep + 'calibre-web.log1', os.sep + 'calibre-web.log2', os.sep + 'gdrive.db',
- os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep + 'client_secrets.json',
- os.sep + 'gdrive_credentials', os.sep + 'settings.yaml', os.sep + 'venv', os.sep + 'virtualenv',
- os.sep + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2',
- os.sep + '.calibre-web.log.swp', os.sep + '_sqlite3.so', os.sep + 'cps' + os.sep + '.HOMEDIR',
- os.sep + 'gmail.json'
- )
+ exclude = self._add_excluded_files(log.info)
additional_path = self.is_venv()
if additional_path:
- exclude = exclude + (additional_path,)
-
- # check if we are in a package, rename cps.py to __init__.py
+ exclude.append(additional_path)
+ exclude = tuple(exclude)
+ # check if we are in a package, rename cps.py to __init__.py and __main__.py
if constants.HOME_CONFIG:
+ shutil.copy(os.path.join(source, 'cps.py'), os.path.join(source, '__main__.py'))
shutil.move(os.path.join(source, 'cps.py'), os.path.join(source, '__init__.py'))
for root, dirs, files in os.walk(destination, topdown=True):
@@ -293,8 +290,8 @@ class Updater(threading.Thread):
remove_items = self.reduce_dirs(rf, new_list)
- if self.check_permissions(source, destination):
- self.moveallfiles(source, destination)
+ if self.check_permissions(source, destination, log.debug):
+ self.move_all_files(source, destination)
for item in remove_items:
item_path = os.path.join(destination, item[1:])
@@ -332,14 +329,21 @@ class Updater(threading.Thread):
log.debug("Stable version: {}".format(constants.STABLE_VERSION))
return constants.STABLE_VERSION # Current version
+ @classmethod
+ def dry_run(cls):
+ cls._add_excluded_files(print)
+ cls.check_permissions(constants.BASE_DIR, constants.BASE_DIR, print)
+ print("\n*** Finished ***")
+
@staticmethod
- def _populate_parent_commits(update_data, status, locale, tz, parents):
+ def _populate_parent_commits(update_data, status, tz, parents):
try:
parent_commit = update_data['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except (IndexError, KeyError):
remaining_parents_cnt = None
+ parent_commit = None
if remaining_parents_cnt is not None:
while True:
@@ -357,7 +361,7 @@ class Updater(threading.Thread):
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
- parent_commit_date, format='short', locale=locale)
+ parent_commit_date, format='short')
parents.append([parent_commit_date,
parent_data['message'].replace('\r\n', '').replace('\n', '
')])
@@ -391,7 +395,31 @@ class Updater(threading.Thread):
status['message'] = _(u'General error')
return status, update_data
- def _nightly_available_updates(self, request_method, locale):
+ @staticmethod
+ def _add_excluded_files(log_function):
+ excluded_files = [
+ os.sep + 'app.db', os.sep + 'calibre-web.log1', os.sep + 'calibre-web.log2', os.sep + 'gdrive.db',
+ os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep + 'client_secrets.json',
+ os.sep + 'gdrive_credentials', os.sep + 'settings.yaml', os.sep + 'venv', os.sep + 'virtualenv',
+ os.sep + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2',
+ os.sep + '.calibre-web.log.swp', os.sep + '_sqlite3.so', os.sep + 'cps' + os.sep + '.HOMEDIR',
+ os.sep + 'gmail.json', os.sep + 'exclude.txt', os.sep + 'cps' + os.sep + 'cache'
+ ]
+ try:
+ with open(os.path.join(constants.BASE_DIR, "exclude.txt"), "r") as f:
+ lines = f.readlines()
+ for line in lines:
+ processed_line = line.strip("\n\r ").strip("\"'").lstrip("\\/ ").\
+ replace("\\", os.sep).replace("/", os.sep)
+ if os.path.exists(os.path.join(constants.BASE_DIR, processed_line)):
+ excluded_files.append(os.sep + processed_line)
+ else:
+ log_function("File list for updater: {} not found".format(line))
+ except (PermissionError, FileNotFoundError):
+ log_function("Excluded file list for updater not found, or not accessible")
+ return excluded_files
+
+ def _nightly_available_updates(self, request_method):
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
if request_method == "GET":
repository_url = _REPOSITORY_API_URL
@@ -432,14 +460,14 @@ class Updater(threading.Thread):
update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parents.append(
[
- format_datetime(new_commit_date, format='short', locale=locale),
+ format_datetime(new_commit_date, format='short'),
update_data['message'],
update_data['sha']
]
)
# it only makes sense to analyze the parents if we know the current commit hash
if status['current_commit_hash'] != '':
- parents = self._populate_parent_commits(update_data, status, locale, tz, parents)
+ parents = self._populate_parent_commits(update_data, status, tz, parents)
status['history'] = parents[::-1]
except (IndexError, KeyError):
status['success'] = False
@@ -449,7 +477,7 @@ class Updater(threading.Thread):
return ''
def _stable_updater_set_status(self, i, newer, status, parents, commit):
- if i == -1 and newer == False:
+ if i == -1 and newer is False:
status.update({
'update': True,
'success': True,
@@ -458,7 +486,7 @@ class Updater(threading.Thread):
'history': parents
})
self.updateFile = commit[0]['zipball_url']
- elif i == -1 and newer == True:
+ elif i == -1 and newer is True:
status.update({
'update': True,
'success': True,
@@ -495,6 +523,7 @@ class Updater(threading.Thread):
return status, parents
def _stable_available_updates(self, request_method):
+ status = None
if request_method == "GET":
parents = []
# repository_url = 'https://api.github.com/repos/flatpak/flatpak/releases' # test URL
@@ -539,7 +568,7 @@ class Updater(threading.Thread):
except ValueError:
current_version[2] = int(current_version[2].split(' ')[0])-1
- # Check if major versions are identical search for newest non equal commit and update to this one
+ # Check if major versions are identical search for newest non-equal commit and update to this one
if major_version_update == current_version[0]:
if (minor_version_update == current_version[1] and
patch_version_update > current_version[2]) or \
@@ -552,7 +581,7 @@ class Updater(threading.Thread):
i -= 1
continue
if major_version_update > current_version[0]:
- # found update update to last version before major update, unless current version is on last version
+ # found update to last version before major update, unless current version is on last version
# before major update
if i == (len(commit) - 1):
i -= 1
@@ -567,7 +596,7 @@ class Updater(threading.Thread):
return json.dumps(status)
def _get_request_path(self):
- if config.config_updatechannel == constants.UPDATE_STABLE:
+ if self.config.config_updatechannel == constants.UPDATE_STABLE:
return self.updateFile
return _REPOSITORY_API_URL + '/zipball/master'
@@ -595,7 +624,7 @@ class Updater(threading.Thread):
status['message'] = _(u'HTTP Error') + ': ' + commit['message']
else:
status['message'] = _(u'HTTP Error') + ': ' + str(e)
- except requests.exceptions.ConnectionError:
+ except requests.exceptions.ConnectionError as e:
status['message'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['message'] = _(u'Timeout while establishing connection')
diff --git a/cps/uploader.py b/cps/uploader.py
index f238b89d..5dbd1249 100644
--- a/cps/uploader.py
+++ b/cps/uploader.py
@@ -27,12 +27,6 @@ from .helper import split_authors
log = logger.create()
-
-try:
- from lxml.etree import LXML_VERSION as lxmlversion
-except ImportError:
- lxmlversion = None
-
try:
from wand.image import Image, Color
from wand import version as ImageVersion
@@ -101,58 +95,16 @@ def default_meta(tmp_file_path, original_file_name, original_file_extension):
extension=original_file_extension,
title=original_file_name,
author=_(u'Unknown'),
- cover=None, #pdf_preview(tmp_file_path, original_file_name),
+ cover=None,
description="",
tags="",
series="",
series_id="",
languages="",
- publisher="")
-
-
-def parse_xmp(pdf_file):
- """
- Parse XMP Metadata and prepare for BookMeta object
- """
- try:
- xmp_info = pdf_file.getXmpMetadata()
- except Exception as ex:
- log.debug('Can not read XMP metadata {}'.format(ex))
- return None
-
- if xmp_info:
- try:
- xmp_author = xmp_info.dc_creator # list
- except AttributeError:
- xmp_author = ['']
-
- if xmp_info.dc_title:
- xmp_title = xmp_info.dc_title['x-default']
- else:
- xmp_title = ''
-
- if xmp_info.dc_description:
- xmp_description = xmp_info.dc_description['x-default']
- else:
- xmp_description = ''
-
- languages = []
- try:
- for i in xmp_info.dc_language:
- #calibre-web currently only takes one language.
- languages.append(isoLanguages.get_lang3(i))
- except AttributeError:
- languages.append('')
-
- xmp_tags = ', '.join(xmp_info.dc_subject)
- xmp_publisher = ', '.join(xmp_info.dc_publisher)
-
- return {'author': xmp_author,
- 'title': xmp_title,
- 'subject': xmp_description,
- 'tags': xmp_tags, 'languages': languages,
- 'publisher': xmp_publisher
- }
+ publisher="",
+ pubdate="",
+ identifiers=[]
+ )
def parse_xmp(pdf_file):
@@ -231,9 +183,12 @@ def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
if title == '':
title = doc_info.title if doc_info.title else original_file_name
if subject == '':
- subject = doc_info.subject
+ subject = doc_info.subject or ""
if tags == '' and '/Keywords' in doc_info:
- tags = doc_info['/Keywords']
+ if isinstance(doc_info['/Keywords'], bytes):
+ tags = doc_info['/Keywords'].decode('utf-8')
+ else:
+ tags = doc_info['/Keywords']
else:
title = original_file_name
@@ -248,7 +203,9 @@ def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
series="",
series_id="",
languages=','.join(languages),
- publisher=publisher)
+ publisher=publisher,
+ pubdate="",
+ identifiers=[])
def pdf_preview(tmp_file_path, tmp_dir):
@@ -274,29 +231,12 @@ def pdf_preview(tmp_file_path, tmp_dir):
return None
-def get_versions(all=True):
+def get_versions():
ret = dict()
if not use_generic_pdf_cover:
ret['Image Magick'] = ImageVersion.MAGICK_VERSION
else:
ret['Image Magick'] = u'not installed'
- if all:
- if not use_generic_pdf_cover:
- ret['Wand'] = ImageVersion.VERSION
- else:
- ret['Wand'] = u'not installed'
- if use_pdf_meta:
- ret['PyPdf'] = PyPdfVersion
- else:
- ret['PyPdf'] = u'not installed'
- if lxmlversion:
- ret['lxml'] = '.'.join(map(str, lxmlversion))
- else:
- ret['lxml'] = u'not installed'
- if comic.use_comic_meta:
- ret['Comic_API'] = comic.comic_version or u'installed'
- else:
- ret['Comic_API'] = u'not installed'
return ret
diff --git a/cps/usermanagement.py b/cps/usermanagement.py
index 71da7701..62fe6f77 100644
--- a/cps/usermanagement.py
+++ b/cps/usermanagement.py
@@ -18,6 +18,7 @@
import base64
import binascii
+from functools import wraps
from sqlalchemy.sql.expression import func
from werkzeug.security import check_password_hash
@@ -25,10 +26,6 @@ from flask_login import login_required, login_user
from . import lm, ub, config, constants, services
-try:
- from functools import wraps
-except ImportError:
- pass # We're not using Python 3
def login_required_if_no_ano(func):
@wraps(func)
diff --git a/cps/web.py b/cps/web.py
index ded72ad0..1aa4cc1b 100644
--- a/cps/web.py
+++ b/cps/web.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
@@ -21,38 +19,37 @@
# along with this program. If not, see .
import os
-from datetime import datetime
import json
import mimetypes
import chardet # dependency of requests
import copy
-from babel.dates import format_date
-from babel import Locale as LC
from flask import Blueprint, jsonify
from flask import request, redirect, send_from_directory, make_response, flash, abort, url_for
from flask import session as flask_session
from flask_babel import gettext as _
+from flask_babel import get_locale
from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
-from sqlalchemy.sql.expression import text, func, false, not_, and_, or_
+from sqlalchemy.sql.expression import text, func, false, not_, and_
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.sql.functions import coalesce
-from .services.worker import WorkerThread
-
from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash, check_password_hash
from . import constants, logger, isoLanguages, services
-from . import babel, db, ub, config, get_locale, app
+from . import db, ub, config, app
from . import calibre_db, kobo_sync_status
+from .search import render_search_results, render_adv_search_results
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
-from .helper import check_valid_domain, render_task_status, check_email, check_username, \
- get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \
- send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email
+from .helper import check_valid_domain, check_email, check_username, \
+ get_book_cover, get_series_cover_thumbnail, get_download_link, send_mail, generate_random_password, \
+ send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password, valid_email, \
+ edit_book_read_status
from .pagination import Pagination
from .redirect import redirect_back
+from .babel import get_available_locale
from .usermanagement import login_required_if_no_ano
from .kobo_sync_status import remove_synced_book
from .render_template import render_title_template
@@ -66,15 +63,14 @@ feature_support = {
try:
from .oauth_bb import oauth_check, register_user_with_oauth, logout_oauth_user, get_oauth_status
+
feature_support['oauth'] = True
except ImportError:
feature_support['oauth'] = False
oauth_check = {}
+ register_user_with_oauth = logout_oauth_user = get_oauth_status = None
-try:
- from functools import wraps
-except ImportError:
- pass # We're not using Python 3
+from functools import wraps
try:
from natsort import natsorted as sort
@@ -84,8 +80,14 @@ except ImportError:
@app.after_request
def add_security_headers(resp):
- resp.headers['Content-Security-Policy'] = "default-src 'self'" + ''.join([' '+host for host in config.config_trustedhosts.strip().split(',')]) + " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:"
- if request.endpoint == "editbook.edit_book" or config.config_use_google_drive:
+ csp = "default-src 'self'"
+ csp += ''.join([' ' + host for host in config.config_trustedhosts.strip().split(',')])
+ csp += " 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' "
+ if request.path.startswith("/author/") and config.config_use_goodreads:
+ csp += "images.gr-assets.com i.gr-assets.com s.gr-assets.com"
+ csp += " data:"
+ resp.headers['Content-Security-Policy'] = csp
+ if request.endpoint == "edit-book.show_edit_book" or config.config_use_google_drive:
resp.headers['Content-Security-Policy'] += " *"
elif request.endpoint == "web.read_book":
resp.headers['Content-Security-Policy'] += " blob:;style-src-elem 'self' blob: 'unsafe-inline';"
@@ -95,7 +97,9 @@ def add_security_headers(resp):
resp.headers['Strict-Transport-Security'] = 'max-age=31536000;'
return resp
+
web = Blueprint('web', __name__)
+
log = logger.create()
@@ -121,19 +125,20 @@ def viewer_required(f):
return inner
+
# ################################### data provider functions #########################################################
@web.route("/ajax/emailstat")
@login_required
def get_email_status_json():
- tasks = WorkerThread.getInstance().tasks
+ tasks = WorkerThread.get_instance().tasks
return jsonify(render_task_status(tasks))
@web.route("/ajax/bookmark//", methods=['POST'])
@login_required
-def bookmark(book_id, book_format):
+def set_bookmark(book_id, book_format):
bookmark_key = request.form["bookmark"]
ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id),
ub.Bookmark.book_id == book_id,
@@ -142,11 +147,11 @@ def bookmark(book_id, book_format):
ub.session_commit()
return "", 204
- lbookmark = ub.Bookmark(user_id=current_user.id,
- book_id=book_id,
- format=book_format,
- bookmark_key=bookmark_key)
- ub.session.merge(lbookmark)
+ l_bookmark = ub.Bookmark(user_id=current_user.id,
+ book_id=book_id,
+ format=book_format,
+ bookmark_key=bookmark_key)
+ ub.session.merge(l_bookmark)
ub.session_commit("Bookmark for user {} in book {} created".format(current_user.id, book_id))
return "", 201
@@ -154,51 +159,17 @@ def bookmark(book_id, book_format):
@web.route("/ajax/toggleread/", methods=['POST'])
@login_required
def toggle_read(book_id):
- if not config.config_read_column:
- book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id),
- ub.ReadBook.book_id == book_id)).first()
- if book:
- if book.read_status == ub.ReadBook.STATUS_FINISHED:
- book.read_status = ub.ReadBook.STATUS_UNREAD
- else:
- book.read_status = ub.ReadBook.STATUS_FINISHED
- else:
- readBook = ub.ReadBook(user_id=current_user.id, book_id = book_id)
- readBook.read_status = ub.ReadBook.STATUS_FINISHED
- book = readBook
- if not book.kobo_reading_state:
- kobo_reading_state = ub.KoboReadingState(user_id=current_user.id, book_id=book_id)
- kobo_reading_state.current_bookmark = ub.KoboBookmark()
- kobo_reading_state.statistics = ub.KoboStatistics()
- book.kobo_reading_state = kobo_reading_state
- ub.session.merge(book)
- ub.session_commit("Book {} readbit toggled".format(book_id))
+ message = edit_book_read_status(book_id)
+ if message:
+ return message, 400
else:
- try:
- calibre_db.update_title_sort(config)
- book = calibre_db.get_filtered_book(book_id)
- read_status = getattr(book, 'custom_column_' + str(config.config_read_column))
- if len(read_status):
- read_status[0].value = not read_status[0].value
- calibre_db.session.commit()
- else:
- cc_class = db.cc_classes[config.config_read_column]
- new_cc = cc_class(value=1, book=book_id)
- calibre_db.session.add(new_cc)
- calibre_db.session.commit()
- except (KeyError, AttributeError):
- log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column)
- return "Custom Column No.{} is not existing in calibre database".format(config.config_read_column), 400
- except (OperationalError, InvalidRequestError) as e:
- calibre_db.session.rollback()
- log.error(u"Read status could not set: {}".format(e))
- return "Read status could not set: {}".format(e), 400
- return ""
+ return message
+
@web.route("/ajax/togglearchived/", methods=['POST'])
@login_required
def toggle_archived(book_id):
- is_archived = change_archived_books(book_id, message="Book {} archivebit toggled".format(book_id))
+ is_archived = change_archived_books(book_id, message="Book {} archive bit toggled".format(book_id))
if is_archived:
remove_synced_book(book_id)
return ""
@@ -266,6 +237,7 @@ def get_comic_book(book_id, book_format, page):
return "", 204
'''
+
# ################################### Typeahead ##################################################################
@@ -333,47 +305,63 @@ def get_matching_tags():
return json_dumps
-def get_sort_function(sort, data):
+def generate_char_list(entries): # data_colum, db_link):
+ char_list = list()
+ for entry in entries:
+ upper_char = entry[0].name[0].upper()
+ if upper_char not in char_list:
+ char_list.append(upper_char)
+ return char_list
+
+
+def query_char_list(data_colum, db_link):
+ results = (calibre_db.session.query(func.upper(func.substr(data_colum, 1, 1)).label('char'))
+ .join(db_link).join(db.Books).filter(calibre_db.common_filters())
+ .group_by(func.upper(func.substr(data_colum, 1, 1))).all())
+ return results
+
+
+def get_sort_function(sort_param, data):
order = [db.Books.timestamp.desc()]
- if sort == 'stored':
- sort = current_user.get_view_property(data, 'stored')
+ if sort_param == 'stored':
+ sort_param = current_user.get_view_property(data, 'stored')
else:
- current_user.set_view_property(data, 'stored', sort)
- if sort == 'pubnew':
+ current_user.set_view_property(data, 'stored', sort_param)
+ if sort_param == 'pubnew':
order = [db.Books.pubdate.desc()]
- if sort == 'pubold':
+ if sort_param == 'pubold':
order = [db.Books.pubdate]
- if sort == 'abc':
+ if sort_param == 'abc':
order = [db.Books.sort]
- if sort == 'zyx':
+ if sort_param == 'zyx':
order = [db.Books.sort.desc()]
- if sort == 'new':
+ if sort_param == 'new':
order = [db.Books.timestamp.desc()]
- if sort == 'old':
+ if sort_param == 'old':
order = [db.Books.timestamp]
- if sort == 'authaz':
+ if sort_param == 'authaz':
order = [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index]
- if sort == 'authza':
+ if sort_param == 'authza':
order = [db.Books.author_sort.desc(), db.Series.name.desc(), db.Books.series_index.desc()]
- if sort == 'seriesasc':
+ if sort_param == 'seriesasc':
order = [db.Books.series_index.asc()]
- if sort == 'seriesdesc':
+ if sort_param == 'seriesdesc':
order = [db.Books.series_index.desc()]
- if sort == 'hotdesc':
+ if sort_param == 'hotdesc':
order = [func.count(ub.Downloads.book_id).desc()]
- if sort == 'hotasc':
+ if sort_param == 'hotasc':
order = [func.count(ub.Downloads.book_id).asc()]
- if sort is None:
- sort = "new"
- return order, sort
+ if sort_param is None:
+ sort_param = "new"
+ return order, sort_param
-def render_books_list(data, sort, book_id, page):
- order = get_sort_function(sort, data)
+def render_books_list(data, sort_param, book_id, page):
+ order = get_sort_function(sort_param, data)
if data == "rated":
return render_rated_books(page, book_id, order=order)
elif data == "discover":
- return render_discover_books(page, book_id)
+ return render_discover_books(book_id)
elif data == "unread":
return render_read_books(page, False, order=order)
elif data == "read":
@@ -403,13 +391,13 @@ def render_books_list(data, sort, book_id, page):
offset = int(int(config.config_books_per_page) * (page - 1))
return render_search_results(term, offset, order, config.config_books_per_page)
elif data == "advsearch":
- term = json.loads(flask_session['query'])
+ term = json.loads(flask_session.get('query', '{}'))
offset = int(int(config.config_books_per_page) * (page - 1))
return render_adv_search_results(term, offset, order, config.config_books_per_page)
else:
website = data or "newest"
entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order[0],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
@@ -423,7 +411,7 @@ def render_rated_books(page, book_id, order):
db.Books,
db.Books.ratings.any(db.Ratings.rating > 9),
order[0],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
@@ -434,41 +422,48 @@ def render_rated_books(page, book_id, order):
abort(404)
-def render_discover_books(page, book_id):
+def render_discover_books(book_id):
if current_user.check_visibility(constants.SIDEBAR_RANDOM):
- entries, __, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, [func.randomblob(2)])
+ entries, __, ___ = calibre_db.fill_indexpage(1, 0, db.Books, True, [func.randomblob(2)],
+ join_archive_read=True,
+ config_read_column=config.config_read_column)
pagination = Pagination(1, config.config_books_per_page, config.config_books_per_page)
- return render_title_template('discover.html', entries=entries, pagination=pagination, id=book_id,
+ return render_title_template('index.html', random=false(), entries=entries, pagination=pagination, id=book_id,
title=_(u"Discover (Random Books)"), page="discover")
else:
abort(404)
+
def render_hot_books(page, order):
if current_user.check_visibility(constants.SIDEBAR_HOT):
if order[1] not in ['hotasc', 'hotdesc']:
- # Unary expression comparsion only working (for this expression) in sqlalchemy 1.4+
- #if not (order[0][0].compare(func.count(ub.Downloads.book_id).desc()) or
- # order[0][0].compare(func.count(ub.Downloads.book_id).asc())):
+ # Unary expression comparsion only working (for this expression) in sqlalchemy 1.4+
+ # if not (order[0][0].compare(func.count(ub.Downloads.book_id).desc()) or
+ # order[0][0].compare(func.count(ub.Downloads.book_id).asc())):
order = [func.count(ub.Downloads.book_id).desc()], 'hotdesc'
if current_user.show_detail_random():
- random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
- .order_by(func.random()).limit(config.config_random_books)
+ random_query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
+ random = (random_query.filter(calibre_db.common_filters())
+ .order_by(func.random())
+ .limit(config.config_random_books).all())
else:
random = false()
+
off = int(int(config.config_books_per_page) * (page - 1))
- all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id))\
+ all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)) \
.order_by(*order[0]).group_by(ub.Downloads.book_id)
hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list()
for book in hot_books:
- downloadBook = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).filter(
- db.Books.id == book.Downloads.book_id).first()
- if downloadBook:
- entries.append(downloadBook)
+ query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
+ download_book = query.filter(calibre_db.common_filters()).filter(
+ book.Downloads.book_id == db.Books.id).first()
+ if download_book:
+ entries.append(download_book)
else:
ub.delete_download(book.Downloads.book_id)
- numBooks = entries.__len__()
- pagination = Pagination(page, config.config_books_per_page, numBooks)
+ num_books = entries.__len__()
+ pagination = Pagination(page, config.config_books_per_page, num_books)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Hot Books (Most Downloaded)"), page="hot", order=order[1])
else:
@@ -481,33 +476,27 @@ def render_downloaded_books(page, order, user_id):
else:
user_id = current_user.id
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD):
- if current_user.show_detail_random():
- random = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
- .order_by(func.random()).limit(config.config_random_books)
- else:
- random = false()
-
- entries, __, pagination = calibre_db.fill_indexpage(page,
+ entries, random, pagination = calibre_db.fill_indexpage(page,
0,
db.Books,
ub.Downloads.user_id == user_id,
order[0],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series,
ub.Downloads, db.Books.id == ub.Downloads.book_id)
for book in entries:
- if not calibre_db.session.query(db.Books).filter(calibre_db.common_filters()) \
- .filter(db.Books.id == book.id).first():
- ub.delete_download(book.id)
+ if not (calibre_db.session.query(db.Books).filter(calibre_db.common_filters())
+ .filter(db.Books.id == book.Books.id).first()):
+ ub.delete_download(book.Books.id)
user = ub.session.query(ub.User).filter(ub.User.id == user_id).first()
return render_title_template('index.html',
random=random,
entries=entries,
pagination=pagination,
id=user_id,
- title=_(u"Downloaded books by %(user)s",user=user.name),
+ title=_(u"Downloaded books by %(user)s", user=user.name),
page="download",
order=order[1])
else:
@@ -519,9 +508,9 @@ def render_author_books(page, author_id, order):
db.Books,
db.Books.authors.any(db.Authors.id == author_id),
[order[0][0], db.Series.name, db.Books.series_index],
- False, 0,
+ True, config.config_read_column,
db.books_series_link,
- db.Books.id == db.books_series_link.c.book,
+ db.books_series_link.c.book == db.Books.id,
db.Series)
if entries is None or not len(entries):
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
@@ -537,55 +526,100 @@ def render_author_books(page, author_id, order):
other_books = []
if services.goodreads_support and config.config_use_goodreads:
author_info = services.goodreads_support.get_author_info(author_name)
- other_books = services.goodreads_support.get_other_books(author_info, entries)
+ book_entries = [entry.Books for entry in entries]
+ other_books = services.goodreads_support.get_other_books(author_info, book_entries)
return render_title_template('author.html', entries=entries, pagination=pagination, id=author_id,
title=_(u"Author: %(name)s", name=author_name), author=author_info,
other_books=other_books, page="author", order=order[1])
def render_publisher_books(page, book_id, order):
- publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first()
- if publisher:
+ if book_id == '-1':
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
- db.Books.publishers.any(db.Publishers.id == book_id),
+ db.Publishers.name == None,
[db.Series.name, order[0][0], db.Books.series_index],
- False, 0,
+ True, config.config_read_column,
+ db.books_publishers_link,
+ db.Books.id == db.books_publishers_link.c.book,
+ db.Publishers,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
- return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
- title=_(u"Publisher: %(name)s", name=publisher.name),
- page="publisher",
- order=order[1])
+ publisher = _("None")
else:
- abort(404)
+ publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first()
+ if publisher:
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db.Books.publishers.any(
+ db.Publishers.id == book_id),
+ [db.Series.name, order[0][0],
+ db.Books.series_index],
+ True, config.config_read_column,
+ db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
+ db.Series)
+ publisher = publisher.name
+ else:
+ abort(404)
+
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
+ title=_(u"Publisher: %(name)s", name=publisher),
+ page="publisher",
+ order=order[1])
def render_series_books(page, book_id, order):
- name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first()
- if name:
+ if book_id == '-1':
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
- db.Books.series.any(db.Series.id == book_id),
- [order[0][0]])
- return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
- title=_(u"Series: %(serie)s", serie=name.name), page="series", order=order[1])
+ db.Series.name == None,
+ [order[0][0]],
+ True, config.config_read_column,
+ db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
+ db.Series)
+ series_name = _("None")
else:
- abort(404)
+ series_name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first()
+ if series_name:
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db.Books.series.any(db.Series.id == book_id),
+ [order[0][0]],
+ True, config.config_read_column)
+ series_name = series_name.name
+ else:
+ abort(404)
+ return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
+ title=_(u"Series: %(serie)s", serie=series_name), page="series", order=order[1])
def render_ratings_books(page, book_id, order):
- name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first()
- entries, random, pagination = calibre_db.fill_indexpage(page, 0,
- db.Books,
- db.Books.ratings.any(db.Ratings.id == book_id),
- [order[0][0]])
- if name and name.rating <= 10:
+ if book_id == '-1':
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db.Books.ratings == None,
+ [order[0][0]],
+ True, config.config_read_column,
+ db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
+ db.Series)
+ title = _(u"Rating: None")
+ rating = -1
+ else:
+ name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first()
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db.Books.ratings.any(db.Ratings.id == book_id),
+ [order[0][0]],
+ True, config.config_read_column)
+ title = _(u"Rating: %(rating)s stars", rating=int(name.rating / 2))
+ rating = name.rating
+ if title and rating <= 10:
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
- title=_(u"Rating: %(rating)s stars", rating=int(name.rating / 2)),
- page="ratings",
- order=order[1])
+ title=title, page="ratings", order=order[1])
else:
abort(404)
@@ -596,7 +630,8 @@ def render_formats_books(page, book_id, order):
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db.Books.data.any(db.Data.format == book_id.upper()),
- [order[0][0]])
+ [order[0][0]],
+ True, config.config_read_column)
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id,
title=_(u"File format: %(format)s", format=name.format),
page="formats",
@@ -606,97 +641,116 @@ def render_formats_books(page, book_id, order):
def render_category_books(page, book_id, order):
- name = calibre_db.session.query(db.Tags).filter(db.Tags.id == book_id).first()
- if name:
+ if book_id == '-1':
entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
- db.Books.tags.any(db.Tags.id == book_id),
+ db.Tags.name == None,
[order[0][0], db.Series.name, db.Books.series_index],
- False, 0,
+ True, config.config_read_column,
+ db.books_tags_link,
+ db.Books.id == db.books_tags_link.c.book,
+ db.Tags,
db.books_series_link,
db.Books.id == db.books_series_link.c.book,
db.Series)
- return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
- title=_(u"Category: %(name)s", name=name.name), page="category", order=order[1])
+ tagsname = _("None")
else:
- abort(404)
+ tagsname = calibre_db.session.query(db.Tags).filter(db.Tags.id == book_id).first()
+ if tagsname:
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db.Books.tags.any(db.Tags.id == book_id),
+ [order[0][0], db.Series.name,
+ db.Books.series_index],
+ True, config.config_read_column,
+ db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
+ db.Series)
+ tagsname = tagsname.name
+ else:
+ abort(404)
+ return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=book_id,
+ title=_(u"Category: %(name)s", name=tagsname), page="category", order=order[1])
def render_language_books(page, name, order):
try:
- lang_name = isoLanguages.get_language_name(get_locale(), name)
+ if name.lower() != "none":
+ lang_name = isoLanguages.get_language_name(get_locale(), name)
+ else:
+ lang_name = _("None")
except KeyError:
abort(404)
-
- entries, random, pagination = calibre_db.fill_indexpage(page, 0,
- db.Books,
- db.Books.languages.any(db.Languages.lang_code == name),
- [order[0][0]])
+ if name == "none":
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db.Languages.lang_code == None,
+ [order[0][0]],
+ True, config.config_read_column,
+ db.books_languages_link,
+ db.Books.id == db.books_languages_link.c.book,
+ db.Languages)
+ else:
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db.Books.languages.any(db.Languages.lang_code == name),
+ [order[0][0]],
+ True, config.config_read_column)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
title=_(u"Language: %(name)s", name=lang_name), page="language", order=order[1])
def render_read_books(page, are_read, as_xml=False, order=None):
- sort = order[0] if order else []
+ sort_param = order[0] if order else []
if not config.config_read_column:
if are_read:
db_filter = and_(ub.ReadBook.user_id == int(current_user.id),
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
else:
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
- entries, random, pagination = calibre_db.fill_indexpage(page, 0,
- db.Books,
- db_filter,
- sort,
- False, 0,
- db.books_series_link,
- db.Books.id == db.books_series_link.c.book,
- db.Series,
- ub.ReadBook, db.Books.id == ub.ReadBook.book_id)
else:
try:
if are_read:
db_filter = db.cc_classes[config.config_read_column].value == True
else:
db_filter = coalesce(db.cc_classes[config.config_read_column].value, False) != True
- entries, random, pagination = calibre_db.fill_indexpage(page, 0,
- db.Books,
- db_filter,
- sort,
- False, 0,
- db.books_series_link,
- db.Books.id == db.books_series_link.c.book,
- db.Series,
- db.cc_classes[config.config_read_column])
- except (KeyError, AttributeError):
- log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column)
+ except (KeyError, AttributeError, IndexError):
+ log.error("Custom Column No.{} is not existing in calibre database".format(config.config_read_column))
if not as_xml:
flash(_("Custom Column No.%(column)d is not existing in calibre database",
column=config.config_read_column),
category="error")
return redirect(url_for("web.index"))
- # ToDo: Handle error Case for opds
+ return [] # ToDo: Handle error Case for opds
+
+ entries, random, pagination = calibre_db.fill_indexpage(page, 0,
+ db.Books,
+ db_filter,
+ sort_param,
+ True, config.config_read_column,
+ db.books_series_link,
+ db.Books.id == db.books_series_link.c.book,
+ db.Series)
+
if as_xml:
return entries, pagination
else:
if are_read:
name = _(u'Read Books') + ' (' + str(pagination.total_count) + ')'
- pagename = "read"
+ page_name = "read"
else:
name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
- pagename = "unread"
+ page_name = "unread"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
- title=name, page=pagename, order=order[1])
+ title=name, page=page_name, order=order[1])
-def render_archived_books(page, sort):
- order = sort[0] or []
- archived_books = (
- ub.session.query(ub.ArchivedBook)
- .filter(ub.ArchivedBook.user_id == int(current_user.id))
- .filter(ub.ArchivedBook.is_archived == True)
- .all()
- )
+def render_archived_books(page, sort_param):
+ order = sort_param[0] or []
+ archived_books = (ub.session.query(ub.ArchivedBook)
+ .filter(ub.ArchivedBook.user_id == int(current_user.id))
+ .filter(ub.ArchivedBook.is_archived == True)
+ .all())
archived_book_ids = [archived_book.book_id for archived_book in archived_books]
archived_filter = db.Books.id.in_(archived_book_ids)
@@ -706,64 +760,12 @@ def render_archived_books(page, sort):
archived_filter,
order,
True,
- False, 0)
+ True, config.config_read_column)
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
- pagename = "archived"
+ page_name = "archived"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
- title=name, page=pagename, order=sort[1])
-
-
-def render_prepare_search_form(cc):
- # prepare data for search-form
- tags = calibre_db.session.query(db.Tags)\
- .join(db.books_tags_link)\
- .join(db.Books)\
- .filter(calibre_db.common_filters()) \
- .group_by(text('books_tags_link.tag'))\
- .order_by(db.Tags.name).all()
- series = calibre_db.session.query(db.Series)\
- .join(db.books_series_link)\
- .join(db.Books)\
- .filter(calibre_db.common_filters()) \
- .group_by(text('books_series_link.series'))\
- .order_by(db.Series.name)\
- .filter(calibre_db.common_filters()).all()
- shelves = ub.session.query(ub.Shelf)\
- .filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == int(current_user.id)))\
- .order_by(ub.Shelf.name).all()
- extensions = calibre_db.session.query(db.Data)\
- .join(db.Books)\
- .filter(calibre_db.common_filters()) \
- .group_by(db.Data.format)\
- .order_by(db.Data.format).all()
- if current_user.filter_language() == u"all":
- languages = calibre_db.speaking_language()
- else:
- languages = None
- return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
- series=series,shelves=shelves, title=_(u"Advanced Search"), cc=cc, page="advsearch")
-
-
-def render_search_results(term, offset=None, order=None, limit=None):
- join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
- entries, result_count, pagination = calibre_db.get_search_results(term,
- offset,
- order,
- limit,
- False,
- config.config_read_column,
- *join)
- return render_title_template('search.html',
- searchterm=term,
- pagination=pagination,
- query=term,
- adv_searchterm=term,
- entries=entries,
- result_count=result_count,
- title=_(u"Search"),
- page="search",
- order=order[1])
+ title=name, page=page_name, order=sort_param[1])
# ################################### View Books list ##################################################################
@@ -790,78 +792,62 @@ def books_list(data, sort_param, book_id, page):
@login_required
def books_table():
visibility = current_user.view_settings.get('table', {})
- cc = get_cc_columns(filter_config_custom_read=True)
+ cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_title_template('book_table.html', title=_(u"Books List"), cc=cc, page="book_table",
visiblility=visibility)
+
@web.route("/ajax/listbooks")
@login_required
def list_books():
off = int(request.args.get("offset") or 0)
limit = int(request.args.get("limit") or config.config_books_per_page)
- search = request.args.get("search")
- sort = request.args.get("sort", "id")
+ search_param = request.args.get("search")
+ sort_param = request.args.get("sort", "id")
order = request.args.get("order", "").lower()
state = None
join = tuple()
- if sort == "state":
+ if sort_param == "state":
state = json.loads(request.args.get("state", "[]"))
- elif sort == "tags":
+ elif sort_param == "tags":
order = [db.Tags.name.asc()] if order == "asc" else [db.Tags.name.desc()]
join = db.books_tags_link, db.Books.id == db.books_tags_link.c.book, db.Tags
- elif sort == "series":
+ elif sort_param == "series":
order = [db.Series.name.asc()] if order == "asc" else [db.Series.name.desc()]
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
- elif sort == "publishers":
+ elif sort_param == "publishers":
order = [db.Publishers.name.asc()] if order == "asc" else [db.Publishers.name.desc()]
join = db.books_publishers_link, db.Books.id == db.books_publishers_link.c.book, db.Publishers
- elif sort == "authors":
+ elif sort_param == "authors":
order = [db.Authors.name.asc(), db.Series.name, db.Books.series_index] if order == "asc" \
else [db.Authors.name.desc(), db.Series.name.desc(), db.Books.series_index.desc()]
- join = db.books_authors_link, db.Books.id == db.books_authors_link.c.book, db.Authors, \
- db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
- elif sort == "languages":
+ join = db.books_authors_link, db.Books.id == db.books_authors_link.c.book, db.Authors, db.books_series_link, \
+ db.Books.id == db.books_series_link.c.book, db.Series
+ elif sort_param == "languages":
order = [db.Languages.lang_code.asc()] if order == "asc" else [db.Languages.lang_code.desc()]
join = db.books_languages_link, db.Books.id == db.books_languages_link.c.book, db.Languages
- elif order and sort in ["sort", "title", "authors_sort", "series_index"]:
- order = [text(sort + " " + order)]
+ elif order and sort_param in ["sort", "title", "authors_sort", "series_index"]:
+ order = [text(sort_param + " " + order)]
elif not state:
order = [db.Books.timestamp.desc()]
- total_count = filtered_count = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(allow_show_archived=True)).count()
+ total_count = filtered_count = calibre_db.session.query(db.Books).filter(
+ calibre_db.common_filters(allow_show_archived=True)).count()
if state is not None:
- if search:
- books = calibre_db.search_query(search, config.config_read_column).all()
+ if search_param:
+ books = calibre_db.search_query(search_param, config).all()
filtered_count = len(books)
else:
- if not config.config_read_column:
- books = (calibre_db.session.query(db.Books, ub.ReadBook.read_status, ub.ArchivedBook.is_archived)
- .select_from(db.Books)
- .outerjoin(ub.ReadBook,
- and_(ub.ReadBook.user_id == int(current_user.id),
- ub.ReadBook.book_id == db.Books.id)))
- else:
- try:
- read_column = db.cc_classes[config.config_read_column]
- books = (calibre_db.session.query(db.Books, read_column.value, ub.ArchivedBook.is_archived)
- .select_from(db.Books)
- .outerjoin(read_column, read_column.book == db.Books.id))
- except (KeyError, AttributeError):
- log.error("Custom Column No.%d is not existing in calibre database", read_column)
- # Skip linking read column and return None instead of read status
- 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,
- int(current_user.id) == ub.ArchivedBook.user_id))
- .filter(calibre_db.common_filters(allow_show_archived=True)).all())
+ query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
+ books = query.filter(calibre_db.common_filters(allow_show_archived=True)).all()
entries = calibre_db.get_checkbox_sorted(books, state, off, limit, order, True)
- elif search:
- entries, filtered_count, __ = calibre_db.get_search_results(search,
+ elif search_param:
+ entries, filtered_count, __ = calibre_db.get_search_results(search_param,
+ config,
off,
- [order,''],
+ [order, ''],
limit,
- True,
- config.config_read_column,
*join)
else:
entries, __, __ = calibre_db.fill_indexpage_with_archived_books((int(off) / (int(limit)) + 1),
@@ -877,11 +863,11 @@ def list_books():
result = list()
for entry in entries:
val = entry[0]
- val.read_status = entry[1] == ub.ReadBook.STATUS_FINISHED
- val.is_archived = entry[2] is True
- for index in range(0, len(val.languages)):
- val.languages[index].language_name = isoLanguages.get_language_name(get_locale(), val.languages[
- index].lang_code)
+ val.is_archived = entry[1] is True
+ val.read_status = entry[2] == ub.ReadBook.STATUS_FINISHED
+ for lang_index in range(0, len(val.languages)):
+ val.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), val.languages[
+ lang_index].lang_code)
result.append(val)
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": result}
@@ -891,6 +877,7 @@ def list_books():
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
+
@web.route("/ajax/table_settings", methods=['POST'])
@login_required
def update_table_settings():
@@ -920,19 +907,18 @@ def author_list():
entries = calibre_db.session.query(db.Authors, func.count('books_authors_link.book').label('count')) \
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_authors_link.author')).order_by(order).all()
- charlist = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('char')) \
- .join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
- .group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all()
+ char_list = query_char_list(db.Authors.sort, db.books_authors_link)
# If not creating a copy, readonly databases can not display authornames with "|" in it as changing the name
# starts a change session
- autor_copy = copy.deepcopy(entries)
- for entry in autor_copy:
+ author_copy = copy.deepcopy(entries)
+ for entry in author_copy:
entry.Authors.name = entry.Authors.name.replace('|', ',')
- return render_title_template('list.html', entries=autor_copy, folder='web.books_list', charlist=charlist,
+ return render_title_template('list.html', entries=author_copy, folder='web.books_list', charlist=char_list,
title=u"Authors", page="authorlist", data='author', order=order_no)
else:
abort(404)
+
@web.route("/downloadlist")
@login_required_if_no_ano
def download_list():
@@ -943,12 +929,12 @@ def download_list():
order = ub.User.name.asc()
order_no = 1
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD) and current_user.role_admin():
- entries = ub.session.query(ub.User, func.count(ub.Downloads.book_id).label('count'))\
+ entries = ub.session.query(ub.User, func.count(ub.Downloads.book_id).label('count')) \
.join(ub.Downloads).group_by(ub.Downloads.user_id).order_by(order).all()
- charlist = ub.session.query(func.upper(func.substr(ub.User.name, 1, 1)).label('char')) \
+ char_list = ub.session.query(func.upper(func.substr(ub.User.name, 1, 1)).label('char')) \
.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) \
.group_by(func.upper(func.substr(ub.User.name, 1, 1))).all()
- return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
+ return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Downloads"), page="downloadlist", data="download", order=order_no)
else:
abort(404)
@@ -967,10 +953,16 @@ def publisher_list():
entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \
.join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_publishers_link.publisher')).order_by(order).all()
- charlist = calibre_db.session.query(func.upper(func.substr(db.Publishers.name, 1, 1)).label('char')) \
- .join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
- .group_by(func.upper(func.substr(db.Publishers.name, 1, 1))).all()
- return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
+ no_publisher_count = (calibre_db.session.query(db.Books)
+ .outerjoin(db.books_publishers_link).outerjoin(db.Publishers)
+ .filter(db.Publishers.name == None)
+ .filter(calibre_db.common_filters())
+ .count())
+ if no_publisher_count:
+ entries.append([db.Category(_("None"), "-1"), no_publisher_count])
+ entries = sorted(entries, key=lambda x: x[0].name.lower(), reverse=not order_no)
+ char_list = generate_char_list(entries)
+ return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Publishers"), page="publisherlist", data="publisher", order=order_no)
else:
abort(404)
@@ -986,25 +978,27 @@ def series_list():
else:
order = db.Series.sort.asc()
order_no = 1
+ char_list = query_char_list(db.Series.sort, db.books_series_link)
if current_user.get_view_property('series', 'series_view') == 'list':
entries = calibre_db.session.query(db.Series, func.count('books_series_link.book').label('count')) \
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series')).order_by(order).all()
- charlist = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('char')) \
- .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
- .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
- return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
+ no_series_count = (calibre_db.session.query(db.Books)
+ .outerjoin(db.books_series_link).outerjoin(db.Series)
+ .filter(db.Series.name == None)
+ .filter(calibre_db.common_filters())
+ .count())
+ if no_series_count:
+ entries.append([db.Category(_("None"), "-1"), no_series_count])
+ entries = sorted(entries, key=lambda x: x[0].name.lower(), reverse=not order_no)
+ return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Series"), page="serieslist", data="series", order=order_no)
else:
entries = calibre_db.session.query(db.Books, func.count('books_series_link').label('count'),
func.max(db.Books.series_index), db.Books.id) \
- .join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters())\
+ .join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series')).order_by(order).all()
- charlist = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('char')) \
- .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
- .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
-
- return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist,
+ return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Series"), page="serieslist", data="series", bodyClass="grid-view",
order=order_no)
else:
@@ -1022,9 +1016,16 @@ def ratings_list():
order = db.Ratings.rating.asc()
order_no = 1
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
- (db.Ratings.rating / 2).label('name')) \
+ (db.Ratings.rating / 2).label('name')) \
.join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_ratings_link.rating')).order_by(order).all()
+ no_rating_count = (calibre_db.session.query(db.Books)
+ .outerjoin(db.books_ratings_link).outerjoin(db.Ratings)
+ .filter(db.Ratings.rating == None)
+ .filter(calibre_db.common_filters())
+ .count())
+ entries.append([db.Category(_("None"), "-1", -1), no_rating_count])
+ entries = sorted(entries, key=lambda x: x[0].rating, reverse=not order_no)
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
title=_(u"Ratings list"), page="ratingslist", data="ratings", order=order_no)
else:
@@ -1046,6 +1047,12 @@ def formats_list():
db.Data.format.label('format')) \
.join(db.Books).filter(calibre_db.common_filters()) \
.group_by(db.Data.format).order_by(order).all()
+ no_format_count = (calibre_db.session.query(db.Books).outerjoin(db.Data)
+ .filter(db.Data.format == None)
+ .filter(calibre_db.common_filters())
+ .count())
+ if no_format_count:
+ entries.append([db.Category(_("None"), "-1"), no_format_count])
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
title=_(u"File formats list"), page="formatslist", data="formats", order=order_no)
else:
@@ -1057,15 +1064,10 @@ def formats_list():
def language_overview():
if current_user.check_visibility(constants.SIDEBAR_LANGUAGE) and current_user.filter_language() == u"all":
order_no = 0 if current_user.get_view_property('language', 'dir') == 'desc' else 1
- charlist = list()
languages = calibre_db.speaking_language(reverse_order=not order_no, with_count=True)
- for lang in languages:
- upper_lang = lang[0].name[0].upper()
- if upper_lang not in charlist:
- charlist.append(upper_lang)
- return render_title_template('languages.html', languages=languages,
- charlist=charlist, title=_(u"Languages"), page="langlist",
- data="language", order=order_no)
+ char_list = generate_char_list(languages)
+ return render_title_template('list.html', entries=languages, folder='web.books_list', charlist=char_list,
+ title=_(u"Languages"), page="langlist", data="language", order=order_no)
else:
abort(404)
@@ -1083,380 +1085,63 @@ def category_list():
entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \
.join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag')).all()
- charlist = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('char')) \
- .join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters()) \
- .group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
- return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist,
+ no_tag_count = (calibre_db.session.query(db.Books)
+ .outerjoin(db.books_tags_link).outerjoin(db.Tags)
+ .filter(db.Tags.name == None)
+ .filter(calibre_db.common_filters())
+ .count())
+ if no_tag_count:
+ entries.append([db.Category(_("None"), "-1"), no_tag_count])
+ entries = sorted(entries, key=lambda x: x[0].name.lower(), reverse=not order_no)
+ char_list = generate_char_list(entries)
+ return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=char_list,
title=_(u"Categories"), page="catlist", data="category", order=order_no)
else:
abort(404)
-# ################################### Task functions ################################################################
-
-
-@web.route("/tasks")
-@login_required
-def get_tasks_status():
- # if current user admin, show all email, otherwise only own emails
- tasks = WorkerThread.getInstance().tasks
- answer = render_task_status(tasks)
- return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks")
-
-
-@app.route("/reconnect")
-def reconnect():
- calibre_db.reconnect_db(config, ub.app_DB_path)
- return json.dumps({})
-
-
-# ################################### Search functions ################################################################
-
-@web.route("/search", methods=["GET"])
-@login_required_if_no_ano
-def search():
- term = request.args.get("query")
- if term:
- return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term))
- else:
- return render_title_template('search.html',
- searchterm="",
- result_count=0,
- title=_(u"Search"),
- page="search")
-
-
-@web.route("/advsearch", methods=['POST'])
-@login_required_if_no_ano
-def advanced_search():
- values = dict(request.form)
- params = ['include_tag', 'exclude_tag', 'include_serie', 'exclude_serie', 'include_shelf', 'exclude_shelf',
- 'include_language', 'exclude_language', 'include_extension', 'exclude_extension']
- for param in params:
- values[param] = list(request.form.getlist(param))
- flask_session['query'] = json.dumps(values)
- return redirect(url_for('web.books_list', data="advsearch", sort_param='stored', query=""))
-
-
-def adv_search_custom_columns(cc, term, q):
- for c in cc:
- if c.datatype == "datetime":
- custom_start = term.get('custom_column_' + str(c.id) + '_start')
- custom_end = term.get('custom_column_' + str(c.id) + '_end')
- if custom_start:
- q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
- func.datetime(db.cc_classes[c.id].value) >= func.datetime(custom_start)))
- if custom_end:
- q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
- func.datetime(db.cc_classes[c.id].value) <= func.datetime(custom_end)))
- else:
- custom_query = term.get('custom_column_' + str(c.id))
- if custom_query != '' and custom_query is not None:
- if c.datatype == 'bool':
- q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
- db.cc_classes[c.id].value == (custom_query == "True")))
- elif c.datatype == 'int' or c.datatype == 'float':
- q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
- db.cc_classes[c.id].value == custom_query))
- elif c.datatype == 'rating':
- q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
- db.cc_classes[c.id].value == int(float(custom_query) * 2)))
- else:
- q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
- func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%")))
- return q
-
-
-def adv_search_language(q, include_languages_inputs, exclude_languages_inputs):
- if current_user.filter_language() != "all":
- q = q.filter(db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()))
- else:
- for language in include_languages_inputs:
- q = q.filter(db.Books.languages.any(db.Languages.id == language))
- for language in exclude_languages_inputs:
- q = q.filter(not_(db.Books.series.any(db.Languages.id == language)))
- return q
-
-
-def adv_search_ratings(q, rating_high, rating_low):
- if rating_high:
- rating_high = int(rating_high) * 2
- q = q.filter(db.Books.ratings.any(db.Ratings.rating <= rating_high))
- if rating_low:
- rating_low = int(rating_low) * 2
- q = q.filter(db.Books.ratings.any(db.Ratings.rating >= rating_low))
- return q
-
-
-def adv_search_read_status(q, read_status):
- if read_status:
- if config.config_read_column:
- try:
- if read_status == "True":
- q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
- .filter(db.cc_classes[config.config_read_column].value == True)
- else:
- q = q.join(db.cc_classes[config.config_read_column], isouter=True) \
- .filter(coalesce(db.cc_classes[config.config_read_column].value, False) != True)
- except (KeyError, AttributeError):
- log.error(u"Custom Column No.%d is not existing in calibre database", config.config_read_column)
- flash(_("Custom Column No.%(column)d is not existing in calibre database",
- column=config.config_read_column),
- category="error")
- return q
- else:
- if read_status == "True":
- q = q.join(ub.ReadBook, db.Books.id == ub.ReadBook.book_id, isouter=True) \
- .filter(ub.ReadBook.user_id == int(current_user.id),
- ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
- else:
- q = q.join(ub.ReadBook, db.Books.id == ub.ReadBook.book_id, isouter=True) \
- .filter(ub.ReadBook.user_id == int(current_user.id),
- coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED)
- return q
-
-
-def adv_search_extension(q, include_extension_inputs, exclude_extension_inputs):
- for extension in include_extension_inputs:
- q = q.filter(db.Books.data.any(db.Data.format == extension))
- for extension in exclude_extension_inputs:
- q = q.filter(not_(db.Books.data.any(db.Data.format == extension)))
- return q
-
-
-def adv_search_tag(q, include_tag_inputs, exclude_tag_inputs):
- for tag in include_tag_inputs:
- q = q.filter(db.Books.tags.any(db.Tags.id == tag))
- for tag in exclude_tag_inputs:
- q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
- return q
-
-
-def adv_search_serie(q, include_series_inputs, exclude_series_inputs):
- for serie in include_series_inputs:
- q = q.filter(db.Books.series.any(db.Series.id == serie))
- for serie in exclude_series_inputs:
- q = q.filter(not_(db.Books.series.any(db.Series.id == serie)))
- return q
-
-def adv_search_shelf(q, include_shelf_inputs, exclude_shelf_inputs):
- q = q.outerjoin(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)\
- .filter(or_(ub.BookShelf.shelf == None, ub.BookShelf.shelf.notin_(exclude_shelf_inputs)))
- if len(include_shelf_inputs) > 0:
- q = q.filter(ub.BookShelf.shelf.in_(include_shelf_inputs))
- return q
-
-def extend_search_term(searchterm,
- author_name,
- book_title,
- publisher,
- pub_start,
- pub_end,
- tags,
- rating_high,
- rating_low,
- read_status,
- ):
- searchterm.extend((author_name.replace('|', ','), book_title, publisher))
- if pub_start:
- try:
- searchterm.extend([_(u"Published after ") +
- format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
- format='medium', locale=get_locale())])
- except ValueError:
- pub_start = u""
- if pub_end:
- try:
- searchterm.extend([_(u"Published before ") +
- format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
- format='medium', locale=get_locale())])
- except ValueError:
- pub_end = u""
- elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
- for key, db_element in elements.items():
- tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
- searchterm.extend(tag.name for tag in tag_names)
- tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['exclude_' + key])).all()
- searchterm.extend(tag.name for tag in tag_names)
- language_names = calibre_db.session.query(db.Languages). \
- filter(db.Languages.id.in_(tags['include_language'])).all()
- if language_names:
- language_names = calibre_db.speaking_language(language_names)
- searchterm.extend(language.name for language in language_names)
- language_names = calibre_db.session.query(db.Languages). \
- filter(db.Languages.id.in_(tags['exclude_language'])).all()
- if language_names:
- language_names = calibre_db.speaking_language(language_names)
- searchterm.extend(language.name for language in language_names)
- if rating_high:
- searchterm.extend([_(u"Rating <= %(rating)s", rating=rating_high)])
- if rating_low:
- searchterm.extend([_(u"Rating >= %(rating)s", rating=rating_low)])
- if read_status:
- searchterm.extend([_(u"Read Status = %(status)s", status=read_status)])
- searchterm.extend(ext for ext in tags['include_extension'])
- searchterm.extend(ext for ext in tags['exclude_extension'])
- # handle custom columns
- searchterm = " + ".join(filter(None, searchterm))
- return searchterm, pub_start, pub_end
-
-
-def render_adv_search_results(term, offset=None, order=None, limit=None):
- sort = order[0] if order else [db.Books.sort]
- pagination = None
-
- cc = get_cc_columns(filter_config_custom_read=True)
- calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
- if not config.config_read_column:
- query = (calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, ub.ReadBook).select_from(db.Books)
- .outerjoin(ub.ReadBook, and_(db.Books.id == ub.ReadBook.book_id,
- int(current_user.id) == ub.ReadBook.user_id)))
- else:
- try:
- read_column = cc[config.config_read_column]
- query = (calibre_db.session.query(db.Books, ub.ArchivedBook.is_archived, read_column.value)
- .select_from(db.Books)
- .outerjoin(read_column, read_column.book == db.Books.id))
- except (KeyError, AttributeError):
- log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column)
- # Skip linking read column
- 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,
- int(current_user.id) == ub.ArchivedBook.user_id))
-
- q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
- .outerjoin(db.Series)\
- .filter(calibre_db.common_filters(True))
-
- # parse multiselects to a complete dict
- tags = dict()
- elements = ['tag', 'serie', 'shelf', 'language', 'extension']
- for element in elements:
- tags['include_' + element] = term.get('include_' + element)
- tags['exclude_' + element] = term.get('exclude_' + element)
-
- author_name = term.get("author_name")
- book_title = term.get("book_title")
- publisher = term.get("publisher")
- pub_start = term.get("publishstart")
- pub_end = term.get("publishend")
- rating_low = term.get("ratinghigh")
- rating_high = term.get("ratinglow")
- description = term.get("comment")
- read_status = term.get("read_status")
- if author_name:
- author_name = author_name.strip().lower().replace(',', '|')
- if book_title:
- book_title = book_title.strip().lower()
- if publisher:
- publisher = publisher.strip().lower()
-
- searchterm = []
- cc_present = False
- for c in cc:
- if c.datatype == "datetime":
- column_start = term.get('custom_column_' + str(c.id) + '_start')
- column_end = term.get('custom_column_' + str(c.id) + '_end')
- if column_start:
- searchterm.extend([u"{} >= {}".format(c.name,
- format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
- format='medium',
- locale=get_locale())
- )])
- cc_present = True
- if column_end:
- searchterm.extend([u"{} <= {}".format(c.name,
- format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
- format='medium',
- locale=get_locale())
- )])
- cc_present = True
- elif term.get('custom_column_' + str(c.id)):
- searchterm.extend([(u"{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
- cc_present = True
-
-
- if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
- or rating_high or description or cc_present or read_status:
- searchterm, pub_start, pub_end = extend_search_term(searchterm,
- author_name,
- book_title,
- publisher,
- pub_start,
- pub_end,
- tags,
- rating_high,
- rating_low,
- read_status)
- # q = q.filter()
- if author_name:
- q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_name + "%")))
- if book_title:
- q = q.filter(func.lower(db.Books.title).ilike("%" + book_title + "%"))
- if pub_start:
- q = q.filter(func.datetime(db.Books.pubdate) > func.datetime(pub_start))
- if pub_end:
- q = q.filter(func.datetime(db.Books.pubdate) < func.datetime(pub_end))
- q = adv_search_read_status(q, read_status)
- if publisher:
- q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
- q = adv_search_tag(q, tags['include_tag'], tags['exclude_tag'])
- q = adv_search_serie(q, tags['include_serie'], tags['exclude_serie'])
- q = adv_search_shelf(q, tags['include_shelf'], tags['exclude_shelf'])
- q = adv_search_extension(q, tags['include_extension'], tags['exclude_extension'])
- q = adv_search_language(q, tags['include_language'], tags['exclude_language'])
- q = adv_search_ratings(q, rating_high, rating_low)
-
- if description:
- q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
-
- # search custom culumns
- try:
- q = adv_search_custom_columns(cc, term, q)
- except AttributeError as ex:
- log.debug_or_exception(ex)
- flash(_("Error on search for custom columns, please restart Calibre-Web"), category="error")
-
- q = q.order_by(*sort).all()
- flask_session['query'] = json.dumps(term)
- ub.store_combo_ids(q)
- result_count = len(q)
- if offset is not None and limit is not None:
- offset = int(offset)
- limit_all = offset + int(limit)
- pagination = Pagination((offset / (int(limit)) + 1), limit, result_count)
- else:
- offset = 0
- limit_all = result_count
- entries = calibre_db.order_authors(q[offset:limit_all], list_return=True, combined=True)
- return render_title_template('search.html',
- adv_searchterm=searchterm,
- pagination=pagination,
- entries=entries,
- result_count=result_count,
- title=_(u"Advanced Search"), page="advsearch",
- order=order[1])
-
-
-@web.route("/advsearch", methods=['GET'])
-@login_required_if_no_ano
-def advanced_search_form():
- # Build custom columns names
- cc = get_cc_columns(filter_config_custom_read=True)
- return render_prepare_search_form(cc)
# ################################### Download/Send ##################################################################
@web.route("/cover/")
+@web.route("/cover//")
@login_required_if_no_ano
-def get_cover(book_id):
- return get_book_cover(book_id)
+def get_cover(book_id, resolution=None):
+ resolutions = {
+ 'og': constants.COVER_THUMBNAIL_ORIGINAL,
+ 'sm': constants.COVER_THUMBNAIL_SMALL,
+ 'md': constants.COVER_THUMBNAIL_MEDIUM,
+ 'lg': constants.COVER_THUMBNAIL_LARGE,
+ }
+ cover_resolution = resolutions.get(resolution, None)
+ return get_book_cover(book_id, cover_resolution)
+
+
+@web.route("/series_cover/")
+@web.route("/series_cover//")
+@login_required_if_no_ano
+def get_series_cover(series_id, resolution=None):
+ resolutions = {
+ 'og': constants.COVER_THUMBNAIL_ORIGINAL,
+ 'sm': constants.COVER_THUMBNAIL_SMALL,
+ 'md': constants.COVER_THUMBNAIL_MEDIUM,
+ 'lg': constants.COVER_THUMBNAIL_LARGE,
+ }
+ cover_resolution = resolutions.get(resolution, None)
+ return get_series_cover_thumbnail(series_id, cover_resolution)
+
+
@web.route("/robots.txt")
def get_robots():
- return send_from_directory(constants.STATIC_DIR, "robots.txt")
+ try:
+ return send_from_directory(constants.STATIC_DIR, "robots.txt")
+ except PermissionError:
+ log.error("No permission to access robots.txt file.")
+ abort(403)
+
@web.route("/show//", defaults={'anyname': 'None'})
@web.route("/show///")
@@ -1476,7 +1161,7 @@ def serve_book(book_id, book_format, anyname):
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
except AttributeError as ex:
- log.debug_or_exception(ex)
+ log.error_or_exception(ex)
return "File Not Found"
else:
if book_format.upper() == 'TXT':
@@ -1501,7 +1186,7 @@ def download_link(book_id, book_format, anyname):
return get_download_link(book_id, book_format, client)
-@web.route('/send///')
+@web.route('/send///', methods=["POST"])
@login_required
@download_required
def send_to_kindle(book_id, book_format, convert):
@@ -1539,13 +1224,13 @@ def register():
if request.method == "POST":
to_save = request.form.to_dict()
- nickname = to_save["email"].strip() if config.config_register_email else to_save.get('name')
+ nickname = to_save.get("email", "").strip() if config.config_register_email else to_save.get('name')
if not nickname or not to_save.get("email"):
flash(_(u"Please fill out all fields!"), category="error")
return render_title_template('register.html', title=_("Register"), page="register")
try:
nickname = check_username(nickname)
- email = check_email(to_save["email"])
+ email = check_email(to_save.get("email", ""))
except Exception as ex:
flash(str(ex), category="error")
return render_title_template('register.html', title=_("Register"), page="register")
@@ -1563,14 +1248,15 @@ def register():
ub.session.commit()
if feature_support['oauth']:
register_user_with_oauth(content)
- send_registration_mail(to_save["email"].strip(), nickname, password)
+ send_registration_mail(to_save.get("email", "").strip(), nickname, password)
except Exception:
ub.session.rollback()
flash(_(u"An unknown error occurred. Please try again later."), category="error")
return render_title_template('register.html', title=_("Register"), page="register")
else:
flash(_(u"Your e-mail is not allowed to register"), category="error")
- log.warning('Registering failed for user "%s" e-mail address: %s', nickname, to_save["email"])
+ log.warning('Registering failed for user "{}" e-mail address: {}'.format(nickname,
+ to_save.get("email","")))
return render_title_template('register.html', title=_("Register"), page="register")
flash(_(u"Confirmation e-mail was send to your e-mail account."), category="success")
return redirect(url_for('web.login'))
@@ -1596,15 +1282,15 @@ def login():
if login_result:
login_user(user, remember=bool(form.get('remember_me')))
ub.store_user_session()
- log.debug(u"You are now logged in as: '%s'", user.name)
+ log.debug(u"You are now logged in as: '{}'".format(user.name))
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name),
category="success")
return redirect_back(url_for("web.index"))
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
- and user.name != "Guest":
+ and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me')))
ub.store_user_session()
- log.info("Local Fallback Login as: '%s'", user.name)
+ log.info("Local Fallback Login as: '{}'".format(user.name))
flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known",
nickname=user.name),
category="warning")
@@ -1613,23 +1299,23 @@ def login():
log.info(error)
flash(_(u"Could not login: %(message)s", message=error), category="error")
else:
- ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
- log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ip_Address)
+ ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
+ log.warning('LDAP Login failed for user "%s" IP-address: %s', form['username'], ip_address)
flash(_(u"Wrong Username or Password"), category="error")
else:
- ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
- if 'forgot' in form and form['forgot'] == 'forgot':
+ ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
+ if form.get('forgot', "") == 'forgot':
if user is not None and user.name != "Guest":
ret, __ = reset_password(user.id)
if ret == 1:
flash(_(u"New Password was send to your email address"), category="info")
- log.info('Password reset for user "%s" IP-address: %s', form['username'], ip_Address)
+ log.info('Password reset for user "%s" IP-address: %s', form['username'], ip_address)
else:
log.error(u"An unknown error occurred. Please try again later")
flash(_(u"An unknown error occurred. Please try again later."), category="error")
else:
flash(_(u"Please enter valid username to reset password"), category="error")
- log.warning('Username missing for password reset IP-address: %s', ip_Address)
+ log.warning('Username missing for password reset IP-address: %s', ip_address)
else:
if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
login_user(user, remember=bool(form.get('remember_me')))
@@ -1639,7 +1325,7 @@ def login():
config.config_is_initial = False
return redirect_back(url_for("web.index"))
else:
- log.warning('Login failed for user "%s" IP-address: %s', form['username'], ip_Address)
+ log.warning('Login failed for user "{}" IP-address: {}'.format(form['username'], ip_address))
flash(_(u"Wrong Username or Password"), category="error")
next_url = request.args.get('next', default=url_for("web.index"), type=str)
@@ -1657,7 +1343,7 @@ def login():
@login_required
def logout():
if current_user is not None and current_user.is_authenticated:
- ub.delete_user_session(current_user.id, flask_session.get('_id',""))
+ ub.delete_user_session(current_user.id, flask_session.get('_id', ""))
logout_user()
if feature_support['oauth'] and (config.config_login_type == 2 or config.config_login_type == 3):
logout_oauth_user()
@@ -1671,21 +1357,19 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
current_user.random_books = 0
if current_user.role_passwd() or current_user.role_admin():
if to_save.get("password"):
- current_user.password = generate_password_hash(to_save["password"])
+ current_user.password = generate_password_hash(to_save.get("password"))
try:
if to_save.get("kindle_mail", current_user.kindle_mail) != current_user.kindle_mail:
- current_user.kindle_mail = valid_email(to_save["kindle_mail"])
+ current_user.kindle_mail = valid_email(to_save.get("kindle_mail"))
if to_save.get("email", current_user.email) != current_user.email:
- current_user.email = check_email(to_save["email"])
+ current_user.email = check_email(to_save.get("email"))
if current_user.role_admin():
if to_save.get("name", current_user.name) != current_user.name:
- # Query User name, if not existing, change
- current_user.name = check_username(to_save["name"])
+ # Query username, if not existing, change
+ current_user.name = check_username(to_save.get("name"))
current_user.random_books = 1 if to_save.get("show_random") == "on" else 0
- if to_save.get("default_language"):
- current_user.default_language = to_save["default_language"]
- if to_save.get("locale"):
- current_user.locale = to_save["locale"]
+ current_user.default_language = to_save.get("default_language", "all")
+ current_user.locale = to_save.get("locale", "en")
old_state = current_user.kobo_only_shelves_sync
# 1 -> 0: nothing has to be done
# 0 -> 1: all synced books have to be added to archived books, + currently synced shelfs which
@@ -1733,7 +1417,7 @@ def change_profile(kobo_support, local_oauth_check, oauth_status, translations,
@login_required
def profile():
languages = calibre_db.speaking_language()
- translations = babel.list_translations() + [LC('en')]
+ translations = get_available_locale()
kobo_support = feature_support['kobo'] and config.config_kobo_sync
if feature_support['oauth'] and config.config_login_type == 2:
oauth_status = get_oauth_status()
@@ -1764,12 +1448,15 @@ def profile():
@viewer_required
def read_book(book_id, book_format):
book = calibre_db.get_filtered_book(book_id)
+ book.ordered_authors = calibre_db.order_authors([book], False)
+
if not book:
- flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
+ flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
+ category="error")
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")
return redirect(url_for("web.index"))
- # check if book has bookmark
+ # check if book has a bookmark
bookmark = None
if current_user.is_authenticated:
bookmark = ub.session.query(ub.Bookmark).filter(and_(ub.Bookmark.user_id == int(current_user.id),
@@ -1806,7 +1493,8 @@ def read_book(book_id, book_format):
return render_title_template('readcbr.html', comicfile=all_name, title=title,
extension=fileExt)
log.debug(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")
+ flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"),
+ category="error")
return redirect(url_for("web.index"))
@@ -1820,33 +1508,33 @@ def show_book(book_id):
entry = entries[0]
entry.read_status = read_book == ub.ReadBook.STATUS_FINISHED
entry.is_archived = archived_book
- for index in range(0, len(entry.languages)):
- entry.languages[index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[
- index].lang_code)
- cc = get_cc_columns(filter_config_custom_read=True)
- book_in_shelfs = []
- shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all()
- for sh in shelfs:
- book_in_shelfs.append(sh.shelf)
+ for lang_index in range(0, len(entry.languages)):
+ entry.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[
+ lang_index].lang_code)
+ cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
+ book_in_shelves = []
+ shelves = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all()
+ for sh in shelves:
+ book_in_shelves.append(sh.shelf)
entry.tags = sort(entry.tags, key=lambda tag: tag.name)
- entry.authors = calibre_db.order_authors([entry])
+ entry.ordered_authors = calibre_db.order_authors([entry])
entry.kindle_list = check_send_to_kindle(entry)
entry.reader_list = check_read_formats(entry)
- entry.audioentries = []
+ entry.audio_entries = []
for media_format in entry.data:
if media_format.format.lower() in constants.EXTENSIONS_AUDIO:
- entry.audioentries.append(media_format.format.lower())
+ entry.audio_entries.append(media_format.format.lower())
return render_title_template('detail.html',
entry=entry,
cc=cc,
- is_xhr=request.headers.get('X-Requested-With')=='XMLHttpRequest',
+ is_xhr=request.headers.get('X-Requested-With') == 'XMLHttpRequest',
title=entry.title,
- books_shelfs=book_in_shelfs,
+ books_shelfs=book_in_shelves,
page="book")
else:
log.debug(u"Oops! Selected book title is unavailable. File does not exist or is not accessible")
diff --git a/exclude.txt b/exclude.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/messages.pot b/messages.pot
index 5588247f..4477a138 100644
--- a/messages.pot
+++ b/messages.pot
@@ -1,588 +1,595 @@
# Translations template for PROJECT.
-# Copyright (C) 2021 ORGANIZATION
+# Copyright (C) 2022 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
-# FIRST AUTHOR , 2021.
+# FIRST AUTHOR , 2022.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2021-12-14 17:51+0100\n"
+"POT-Creation-Date: 2022-04-18 20:01+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.9.0\n"
+"Generated-By: Babel 2.9.1\n"
-#: cps/about.py:34 cps/about.py:49 cps/about.py:65 cps/converter.py:31
-msgid "not installed"
-msgstr ""
-
-#: cps/about.py:47 cps/about.py:63
-msgid "installed"
-msgstr ""
-
-#: cps/about.py:138
+#: cps/about.py:86
msgid "Statistics"
msgstr ""
-#: cps/admin.py:144
+#: cps/admin.py:141
msgid "Server restarted, please reload page"
msgstr ""
-#: cps/admin.py:146
+#: cps/admin.py:143
msgid "Performing shutdown of server, please close window"
msgstr ""
-#: cps/admin.py:154
+#: cps/admin.py:151
msgid "Reconnect successful"
msgstr ""
-#: cps/admin.py:157
+#: cps/admin.py:154
msgid "Unknown command"
msgstr ""
-#: cps/admin.py:167 cps/editbooks.py:703 cps/editbooks.py:717
-#: cps/editbooks.py:862 cps/editbooks.py:864 cps/editbooks.py:891
-#: cps/editbooks.py:907 cps/updater.py:584 cps/uploader.py:93
-#: cps/uploader.py:103
+#: cps/admin.py:176 cps/editbooks.py:713 cps/editbooks.py:892
+#: cps/editbooks.py:894 cps/editbooks.py:930 cps/editbooks.py:947
+#: cps/updater.py:608 cps/uploader.py:93 cps/uploader.py:103
msgid "Unknown"
msgstr ""
-#: cps/admin.py:188
+#: cps/admin.py:197
msgid "Admin page"
msgstr ""
-#: cps/admin.py:207
+#: cps/admin.py:217
msgid "Basic Configuration"
msgstr ""
-#: cps/admin.py:244
+#: cps/admin.py:255
msgid "UI Configuration"
msgstr ""
-#: cps/admin.py:277 cps/templates/admin.html:50
+#: cps/admin.py:289 cps/templates/admin.html:51
msgid "Edit Users"
msgstr ""
-#: cps/admin.py:318 cps/opds.py:109 cps/opds.py:198 cps/opds.py:275
-#: cps/opds.py:327 cps/templates/grid.html:13 cps/templates/languages.html:9
+#: cps/admin.py:333 cps/opds.py:529 cps/templates/grid.html:13
#: cps/templates/list.html:13
msgid "All"
msgstr ""
-#: cps/admin.py:343 cps/admin.py:1595
+#: cps/admin.py:360 cps/admin.py:1648
msgid "User not found"
msgstr ""
-#: cps/admin.py:357
+#: cps/admin.py:374
msgid "{} users deleted successfully"
msgstr ""
-#: cps/admin.py:379 cps/templates/config_view_edit.html:133
+#: cps/admin.py:397 cps/templates/config_view_edit.html:133
#: cps/templates/user_edit.html:45 cps/templates/user_table.html:81
msgid "Show All"
msgstr ""
-#: cps/admin.py:400 cps/admin.py:406
+#: cps/admin.py:418 cps/admin.py:424
msgid "Malformed request"
msgstr ""
-#: cps/admin.py:418 cps/admin.py:1473
+#: cps/admin.py:436 cps/admin.py:1526
msgid "Guest Name can't be changed"
msgstr ""
-#: cps/admin.py:430
+#: cps/admin.py:448
msgid "Guest can't have this role"
msgstr ""
-#: cps/admin.py:442 cps/admin.py:1431
+#: cps/admin.py:460 cps/admin.py:1484
msgid "No admin user remaining, can't remove admin role"
msgstr ""
-#: cps/admin.py:446 cps/admin.py:460
+#: cps/admin.py:464 cps/admin.py:478
msgid "Value has to be true or false"
msgstr ""
-#: cps/admin.py:448
+#: cps/admin.py:466
msgid "Invalid role"
msgstr ""
-#: cps/admin.py:452
+#: cps/admin.py:470
msgid "Guest can't have this view"
msgstr ""
-#: cps/admin.py:462
+#: cps/admin.py:480
msgid "Invalid view"
msgstr ""
-#: cps/admin.py:465
+#: cps/admin.py:483
msgid "Guest's Locale is determined automatically and can't be set"
msgstr ""
-#: cps/admin.py:469
+#: cps/admin.py:487
msgid "No Valid Locale Given"
msgstr ""
-#: cps/admin.py:480
+#: cps/admin.py:498
msgid "No Valid Book Language Given"
msgstr ""
-#: cps/admin.py:482
+#: cps/admin.py:500 cps/editbooks.py:1267
msgid "Parameter not found"
msgstr ""
-#: cps/admin.py:533
+#: cps/admin.py:553
msgid "Invalid Read Column"
msgstr ""
-#: cps/admin.py:539
+#: cps/admin.py:559
msgid "Invalid Restricted Column"
msgstr ""
-#: cps/admin.py:560 cps/admin.py:1312
+#: cps/admin.py:579 cps/admin.py:1355
msgid "Calibre-Web configuration updated"
msgstr ""
-#: cps/admin.py:572
+#: cps/admin.py:591
msgid "Do you really want to delete the Kobo Token?"
msgstr ""
-#: cps/admin.py:574
+#: cps/admin.py:593
msgid "Do you really want to delete this domain?"
msgstr ""
-#: cps/admin.py:576
+#: cps/admin.py:595
msgid "Do you really want to delete this user?"
msgstr ""
-#: cps/admin.py:578 cps/templates/shelf.html:91
+#: cps/admin.py:597
msgid "Are you sure you want to delete this shelf?"
msgstr ""
-#: cps/admin.py:580
+#: cps/admin.py:599
msgid "Are you sure you want to change locales of selected user(s)?"
msgstr ""
-#: cps/admin.py:582
+#: cps/admin.py:601
msgid "Are you sure you want to change visible book languages for selected user(s)?"
msgstr ""
-#: cps/admin.py:584
+#: cps/admin.py:603
msgid "Are you sure you want to change the selected role for the selected user(s)?"
msgstr ""
-#: cps/admin.py:586
+#: cps/admin.py:605
msgid "Are you sure you want to change the selected restrictions for the selected user(s)?"
msgstr ""
-#: cps/admin.py:588
+#: cps/admin.py:607
msgid "Are you sure you want to change the selected visibility restrictions for the selected user(s)?"
msgstr ""
-#: cps/admin.py:590
+#: cps/admin.py:610
msgid "Are you sure you want to change shelf sync behavior for the selected user(s)?"
msgstr ""
-#: cps/admin.py:592
+#: cps/admin.py:612
msgid "Are you sure you want to change Calibre library location?"
msgstr ""
-#: cps/admin.py:594
+#: cps/admin.py:614
msgid "Are you sure you want delete Calibre-Web's sync database to force a full sync with your Kobo Reader?"
msgstr ""
-#: cps/admin.py:743
+#: cps/admin.py:764
msgid "Tag not found"
msgstr ""
-#: cps/admin.py:755
+#: cps/admin.py:776
msgid "Invalid Action"
msgstr ""
-#: cps/admin.py:860 cps/admin.py:866 cps/admin.py:876 cps/admin.py:886
+#: cps/admin.py:893 cps/admin.py:899 cps/admin.py:909 cps/admin.py:919
#: cps/templates/modal_dialogs.html:29 cps/templates/user_table.html:41
#: cps/templates/user_table.html:58
msgid "Deny"
msgstr ""
-#: cps/admin.py:862 cps/admin.py:868 cps/admin.py:878 cps/admin.py:888
+#: cps/admin.py:895 cps/admin.py:901 cps/admin.py:911 cps/admin.py:921
#: cps/templates/modal_dialogs.html:28 cps/templates/user_table.html:44
#: cps/templates/user_table.html:61
msgid "Allow"
msgstr ""
-#: cps/admin.py:902
+#: cps/admin.py:936
msgid "{} sync entries deleted"
msgstr ""
-#: cps/admin.py:1025
+#: cps/admin.py:1059
msgid "client_secrets.json Is Not Configured For Web Application"
msgstr ""
-#: cps/admin.py:1070
+#: cps/admin.py:1104
msgid "Logfile Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:1076
+#: cps/admin.py:1110
msgid "Access Logfile Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:1106
+#: cps/admin.py:1140
msgid "Please Enter a LDAP Provider, Port, DN and User Object Identifier"
msgstr ""
-#: cps/admin.py:1112
+#: cps/admin.py:1146
msgid "Please Enter a LDAP Service Account and Password"
msgstr ""
-#: cps/admin.py:1115
+#: cps/admin.py:1149
msgid "Please Enter a LDAP Service Account"
msgstr ""
-#: cps/admin.py:1120
+#: cps/admin.py:1154
#, python-format
msgid "LDAP Group Object Filter Needs to Have One \"%s\" Format Identifier"
msgstr ""
-#: cps/admin.py:1122
+#: cps/admin.py:1156
msgid "LDAP Group Object Filter Has Unmatched Parenthesis"
msgstr ""
-#: cps/admin.py:1126
+#: cps/admin.py:1160
#, python-format
msgid "LDAP User Object Filter needs to Have One \"%s\" Format Identifier"
msgstr ""
-#: cps/admin.py:1128
+#: cps/admin.py:1162
msgid "LDAP User Object Filter Has Unmatched Parenthesis"
msgstr ""
-#: cps/admin.py:1135
+#: cps/admin.py:1169
#, python-format
msgid "LDAP Member User Filter needs to Have One \"%s\" Format Identifier"
msgstr ""
-#: cps/admin.py:1137
+#: cps/admin.py:1171
msgid "LDAP Member User Filter Has Unmatched Parenthesis"
msgstr ""
-#: cps/admin.py:1144
+#: cps/admin.py:1178
msgid "LDAP CACertificate, Certificate or Key Location is not Valid, Please Enter Correct Path"
msgstr ""
-#: cps/admin.py:1186 cps/admin.py:1297 cps/admin.py:1394 cps/admin.py:1501
-#: cps/admin.py:1570 cps/shelf.py:100 cps/shelf.py:160 cps/shelf.py:203
-#: cps/shelf.py:274 cps/shelf.py:335 cps/shelf.py:370 cps/shelf.py:445
-msgid "Settings DB is not Writeable"
-msgstr ""
-
-#: cps/admin.py:1197
-msgid "DB Location is not Valid, Please Enter Correct Path"
-msgstr ""
-
-#: cps/admin.py:1212
-msgid "DB is not Writeable"
-msgstr ""
-
-#: cps/admin.py:1224
-msgid "Keyfile Location is not Valid, Please Enter Correct Path"
-msgstr ""
-
-#: cps/admin.py:1228
-msgid "Certfile Location is not Valid, Please Enter Correct Path"
-msgstr ""
-
-#: cps/admin.py:1335
-msgid "Database Settings updated"
-msgstr ""
-
-#: cps/admin.py:1343
-msgid "Database Configuration"
-msgstr ""
-
-#: cps/admin.py:1359 cps/web.py:1478
-msgid "Please fill out all fields!"
-msgstr ""
-
-#: cps/admin.py:1367
-msgid "E-mail is not from valid domain"
-msgstr ""
-
-#: cps/admin.py:1373 cps/admin.py:1523
-msgid "Add new user"
-msgstr ""
-
-#: cps/admin.py:1384
-#, python-format
-msgid "User '%(user)s' created"
-msgstr ""
-
-#: cps/admin.py:1390
-msgid "Found an existing account for this e-mail address or name."
-msgstr ""
-
-#: cps/admin.py:1410
-#, python-format
-msgid "User '%(nick)s' deleted"
-msgstr ""
-
-#: cps/admin.py:1412 cps/admin.py:1413
-msgid "Can't delete Guest User"
-msgstr ""
-
-#: cps/admin.py:1416
-msgid "No admin user remaining, can't delete user"
-msgstr ""
-
-#: cps/admin.py:1489 cps/admin.py:1614
-#, python-format
-msgid "Edit User %(nick)s"
-msgstr ""
-
-#: cps/admin.py:1493
-#, python-format
-msgid "User '%(nick)s' updated"
-msgstr ""
-
-#: cps/admin.py:1497 cps/admin.py:1629 cps/web.py:1503 cps/web.py:1563
-msgid "An unknown error occurred. Please try again later."
-msgstr ""
-
-#: cps/admin.py:1532 cps/templates/admin.html:98
-msgid "Edit E-mail Server Settings"
-msgstr ""
-
-#: cps/admin.py:1551
-msgid "Gmail Account Verification Successful"
-msgstr ""
-
-#: cps/admin.py:1577
-#, python-format
-msgid "Test e-mail queued for sending to %(email)s, please check Tasks for result"
-msgstr ""
-
-#: cps/admin.py:1580
-#, python-format
-msgid "There was an error sending the Test e-mail: %(res)s"
-msgstr ""
-
-#: cps/admin.py:1582
-msgid "Please configure your e-mail address first..."
-msgstr ""
-
-#: cps/admin.py:1584
-msgid "E-mail server settings updated"
-msgstr ""
-
-#: cps/admin.py:1626
-#, python-format
-msgid "Password for user %(user)s reset"
-msgstr ""
-
-#: cps/admin.py:1632 cps/web.py:1443
-msgid "Please configure the SMTP mail settings first..."
-msgstr ""
-
-#: cps/admin.py:1643
-msgid "Logfile viewer"
-msgstr ""
-
-#: cps/admin.py:1709
-msgid "Requesting update package"
-msgstr ""
-
-#: cps/admin.py:1710
-msgid "Downloading update package"
-msgstr ""
-
-#: cps/admin.py:1711
-msgid "Unzipping update package"
-msgstr ""
-
-#: cps/admin.py:1712
-msgid "Replacing files"
-msgstr ""
-
-#: cps/admin.py:1713
-msgid "Database connections are closed"
-msgstr ""
-
-#: cps/admin.py:1714
-msgid "Stopping server"
-msgstr ""
-
-#: cps/admin.py:1715
-msgid "Update finished, please press okay and reload page"
-msgstr ""
-
-#: cps/admin.py:1716 cps/admin.py:1717 cps/admin.py:1718 cps/admin.py:1719
-#: cps/admin.py:1720 cps/admin.py:1721
-msgid "Update failed:"
-msgstr ""
-
-#: cps/admin.py:1716 cps/updater.py:385 cps/updater.py:595 cps/updater.py:597
-msgid "HTTP Error"
-msgstr ""
-
-#: cps/admin.py:1717 cps/updater.py:387 cps/updater.py:599
-msgid "Connection error"
-msgstr ""
-
-#: cps/admin.py:1718 cps/updater.py:389 cps/updater.py:601
-msgid "Timeout while establishing connection"
-msgstr ""
-
-#: cps/admin.py:1719 cps/updater.py:391 cps/updater.py:603
-msgid "General error"
-msgstr ""
-
-#: cps/admin.py:1720
-msgid "Update file could not be saved in temp dir"
-msgstr ""
-
-#: cps/admin.py:1721
-msgid "Files could not be replaced during update"
-msgstr ""
-
-#: cps/admin.py:1745
-msgid "Failed to extract at least One LDAP User"
-msgstr ""
-
-#: cps/admin.py:1790
-msgid "Failed to Create at Least One LDAP User"
-msgstr ""
-
-#: cps/admin.py:1803
-#, python-format
-msgid "Error: %(ldaperror)s"
-msgstr ""
-
-#: cps/admin.py:1807
-msgid "Error: No user returned in response of LDAP server"
-msgstr ""
-
-#: cps/admin.py:1840
-msgid "At Least One LDAP User Not Found in Database"
-msgstr ""
-
-#: cps/admin.py:1842
-msgid "{} User Successfully Imported"
-msgstr ""
-
-#: cps/converter.py:30
-msgid "not configured"
-msgstr ""
-
-#: cps/converter.py:32
-msgid "Execution permissions missing"
-msgstr ""
-
-#: cps/db.py:648 cps/web.py:667 cps/web.py:1154
-#, python-format
-msgid "Custom Column No.%(column)d is not existing in calibre database"
-msgstr ""
-
-#: cps/editbooks.py:305 cps/editbooks.py:307
-msgid "Book Format Successfully Deleted"
-msgstr ""
-
-#: cps/editbooks.py:314 cps/editbooks.py:316
-msgid "Book Successfully Deleted"
-msgstr ""
-
-#: cps/editbooks.py:372 cps/editbooks.py:759 cps/web.py:523 cps/web.py:1702
-#: cps/web.py:1743 cps/web.py:1810
-msgid "Oops! Selected book title is unavailable. File does not exist or is not accessible"
-msgstr ""
-
-#: cps/editbooks.py:406
-msgid "edit metadata"
-msgstr ""
-
-#: cps/editbooks.py:454
-#, python-format
-msgid "%(seriesindex)s is not a valid number, skipping"
-msgstr ""
-
-#: cps/editbooks.py:490 cps/editbooks.py:954
-#, python-format
-msgid "'%(langname)s' is not a valid language"
-msgstr ""
-
-#: cps/editbooks.py:630 cps/editbooks.py:981
-#, python-format
-msgid "File extension '%(ext)s' is not allowed to be uploaded to this server"
-msgstr ""
-
-#: cps/editbooks.py:634 cps/editbooks.py:985
-msgid "File to be uploaded must have an extension"
-msgstr ""
-
-#: cps/editbooks.py:646
-#, python-format
-msgid "Failed to create path %(path)s (Permission denied)."
-msgstr ""
-
-#: cps/editbooks.py:651
-#, python-format
-msgid "Failed to store file %(file)s."
-msgstr ""
-
-#: cps/editbooks.py:669 cps/editbooks.py:1072 cps/web.py:1663
+#: cps/admin.py:1223 cps/admin.py:1339 cps/admin.py:1437 cps/admin.py:1554
+#: cps/admin.py:1623 cps/editbooks.py:678 cps/editbooks.py:882
+#: cps/editbooks.py:1130 cps/shelf.py:100 cps/shelf.py:160 cps/shelf.py:203
+#: cps/shelf.py:278 cps/shelf.py:343 cps/shelf.py:380 cps/shelf.py:456
+#: cps/web.py:1742
#, python-format
msgid "Database error: %(error)s."
msgstr ""
-#: cps/editbooks.py:674
+#: cps/admin.py:1235
+msgid "DB Location is not Valid, Please Enter Correct Path"
+msgstr ""
+
+#: cps/admin.py:1253
+msgid "DB is not Writeable"
+msgstr ""
+
+#: cps/admin.py:1266
+msgid "Keyfile Location is not Valid, Please Enter Correct Path"
+msgstr ""
+
+#: cps/admin.py:1270
+msgid "Certfile Location is not Valid, Please Enter Correct Path"
+msgstr ""
+
+#: cps/admin.py:1378
+msgid "Database Settings updated"
+msgstr ""
+
+#: cps/admin.py:1386
+msgid "Database Configuration"
+msgstr ""
+
+#: cps/admin.py:1402 cps/web.py:1557
+msgid "Please fill out all fields!"
+msgstr ""
+
+#: cps/admin.py:1410
+msgid "E-mail is not from valid domain"
+msgstr ""
+
+#: cps/admin.py:1416 cps/admin.py:1576
+msgid "Add new user"
+msgstr ""
+
+#: cps/admin.py:1427
+#, python-format
+msgid "User '%(user)s' created"
+msgstr ""
+
+#: cps/admin.py:1433
+msgid "Found an existing account for this e-mail address or name."
+msgstr ""
+
+#: cps/admin.py:1463
+#, python-format
+msgid "User '%(nick)s' deleted"
+msgstr ""
+
+#: cps/admin.py:1465 cps/admin.py:1466
+msgid "Can't delete Guest User"
+msgstr ""
+
+#: cps/admin.py:1469
+msgid "No admin user remaining, can't delete user"
+msgstr ""
+
+#: cps/admin.py:1542 cps/admin.py:1667
+#, python-format
+msgid "Edit User %(nick)s"
+msgstr ""
+
+#: cps/admin.py:1546
+#, python-format
+msgid "User '%(nick)s' updated"
+msgstr ""
+
+#: cps/admin.py:1550 cps/admin.py:1682 cps/web.py:1582 cps/web.py:1642
+msgid "An unknown error occurred. Please try again later."
+msgstr ""
+
+#: cps/admin.py:1585 cps/templates/admin.html:100
+msgid "Edit E-mail Server Settings"
+msgstr ""
+
+#: cps/admin.py:1604
+msgid "Gmail Account Verification Successful"
+msgstr ""
+
+#: cps/admin.py:1630
+#, python-format
+msgid "Test e-mail queued for sending to %(email)s, please check Tasks for result"
+msgstr ""
+
+#: cps/admin.py:1633
+#, python-format
+msgid "There was an error sending the Test e-mail: %(res)s"
+msgstr ""
+
+#: cps/admin.py:1635
+msgid "Please configure your e-mail address first..."
+msgstr ""
+
+#: cps/admin.py:1637
+msgid "E-mail server settings updated"
+msgstr ""
+
+#: cps/admin.py:1679
+#, python-format
+msgid "Password for user %(user)s reset"
+msgstr ""
+
+#: cps/admin.py:1685 cps/web.py:1522
+msgid "Please configure the SMTP mail settings first..."
+msgstr ""
+
+#: cps/admin.py:1696
+msgid "Logfile viewer"
+msgstr ""
+
+#: cps/admin.py:1762
+msgid "Requesting update package"
+msgstr ""
+
+#: cps/admin.py:1763
+msgid "Downloading update package"
+msgstr ""
+
+#: cps/admin.py:1764
+msgid "Unzipping update package"
+msgstr ""
+
+#: cps/admin.py:1765
+msgid "Replacing files"
+msgstr ""
+
+#: cps/admin.py:1766
+msgid "Database connections are closed"
+msgstr ""
+
+#: cps/admin.py:1767
+msgid "Stopping server"
+msgstr ""
+
+#: cps/admin.py:1768
+msgid "Update finished, please press okay and reload page"
+msgstr ""
+
+#: cps/admin.py:1769 cps/admin.py:1770 cps/admin.py:1771 cps/admin.py:1772
+#: cps/admin.py:1773 cps/admin.py:1774
+msgid "Update failed:"
+msgstr ""
+
+#: cps/admin.py:1769 cps/updater.py:384 cps/updater.py:619 cps/updater.py:621
+msgid "HTTP Error"
+msgstr ""
+
+#: cps/admin.py:1770 cps/updater.py:386 cps/updater.py:623
+msgid "Connection error"
+msgstr ""
+
+#: cps/admin.py:1771 cps/updater.py:388 cps/updater.py:625
+msgid "Timeout while establishing connection"
+msgstr ""
+
+#: cps/admin.py:1772 cps/updater.py:390 cps/updater.py:627
+msgid "General error"
+msgstr ""
+
+#: cps/admin.py:1773
+msgid "Update file could not be saved in temp dir"
+msgstr ""
+
+#: cps/admin.py:1774
+msgid "Files could not be replaced during update"
+msgstr ""
+
+#: cps/admin.py:1798
+msgid "Failed to extract at least One LDAP User"
+msgstr ""
+
+#: cps/admin.py:1843
+msgid "Failed to Create at Least One LDAP User"
+msgstr ""
+
+#: cps/admin.py:1856
+#, python-format
+msgid "Error: %(ldaperror)s"
+msgstr ""
+
+#: cps/admin.py:1860
+msgid "Error: No user returned in response of LDAP server"
+msgstr ""
+
+#: cps/admin.py:1893
+msgid "At Least One LDAP User Not Found in Database"
+msgstr ""
+
+#: cps/admin.py:1895
+msgid "{} User Successfully Imported"
+msgstr ""
+
+#: cps/converter.py:30
+msgid "not installed"
+msgstr ""
+
+#: cps/converter.py:31
+msgid "Execution permissions missing"
+msgstr ""
+
+#: cps/db.py:674 cps/web.py:710 cps/web.py:1222
+#, python-format
+msgid "Custom Column No.%(column)d is not existing in calibre database"
+msgstr ""
+
+#: cps/db.py:917 cps/templates/config_edit.html:204
+#: cps/templates/config_view_edit.html:62 cps/templates/email_edit.html:41
+#: cps/web.py:551 cps/web.py:585 cps/web.py:646 cps/web.py:671 cps/web.py:1003
+#: cps/web.py:1032 cps/web.py:1066 cps/web.py:1093 cps/web.py:1132
+msgid "None"
+msgstr ""
+
+#: cps/editbooks.py:295 cps/editbooks.py:297
+msgid "Book Format Successfully Deleted"
+msgstr ""
+
+#: cps/editbooks.py:304 cps/editbooks.py:306
+msgid "Book Successfully Deleted"
+msgstr ""
+
+#: cps/editbooks.py:358
+msgid "You are missing permissions to delete books"
+msgstr ""
+
+#: cps/editbooks.py:373 cps/editbooks.py:765 cps/web.py:518 cps/web.py:1783
+#: cps/web.py:1825 cps/web.py:1870
+msgid "Oops! Selected book title is unavailable. File does not exist or is not accessible"
+msgstr ""
+
+#: cps/editbooks.py:408
+msgid "edit metadata"
+msgstr ""
+
+#: cps/editbooks.py:457
+#, python-format
+msgid "%(seriesindex)s is not a valid number, skipping"
+msgstr ""
+
+#: cps/editbooks.py:493 cps/editbooks.py:1001
+#, python-format
+msgid "'%(langname)s' is not a valid language"
+msgstr ""
+
+#: cps/editbooks.py:634
+msgid "User has no rights to upload additional file formats"
+msgstr ""
+
+#: cps/editbooks.py:639 cps/editbooks.py:1029
+#, python-format
+msgid "File extension '%(ext)s' is not allowed to be uploaded to this server"
+msgstr ""
+
+#: cps/editbooks.py:643 cps/editbooks.py:1033
+msgid "File to be uploaded must have an extension"
+msgstr ""
+
+#: cps/editbooks.py:655
+#, python-format
+msgid "Failed to create path %(path)s (Permission denied)."
+msgstr ""
+
+#: cps/editbooks.py:660
+#, python-format
+msgid "Failed to store file %(file)s."
+msgstr ""
+
+#: cps/editbooks.py:683
#, python-format
msgid "File format %(ext)s added to %(book)s"
msgstr ""
-#: cps/editbooks.py:810
+#: cps/editbooks.py:697 cps/editbooks.py:809
+msgid "User has no rights to upload cover"
+msgstr ""
+
+#: cps/editbooks.py:828
msgid "Identifiers are not Case Sensitive, Overwriting Old Identifier"
msgstr ""
-#: cps/editbooks.py:844
+#: cps/editbooks.py:869
msgid "Metadata successfully updated"
msgstr ""
-#: cps/editbooks.py:857
-msgid "Error editing book, please check logfile for details"
+#: cps/editbooks.py:887
+msgid "Error editing book: {}"
msgstr ""
-#: cps/editbooks.py:895
+#: cps/editbooks.py:951
msgid "Uploaded book probably exists in the library, consider to change before upload new: "
msgstr ""
-#: cps/editbooks.py:993
+#: cps/editbooks.py:1041
#, python-format
msgid "File %(filename)s could not saved to temp dir"
msgstr ""
-#: cps/editbooks.py:1012
+#: cps/editbooks.py:1061
#, python-format
msgid "Failed to Move Cover File %(file)s: %(error)s"
msgstr ""
-#: cps/editbooks.py:1059
+#: cps/editbooks.py:1117
#, python-format
msgid "File %(file)s uploaded"
msgstr ""
-#: cps/editbooks.py:1084
+#: cps/editbooks.py:1143
msgid "Source or destination format for conversion missing"
msgstr ""
-#: cps/editbooks.py:1092
+#: cps/editbooks.py:1151
#, python-format
msgid "Book successfully queued for converting to %(book_format)s"
msgstr ""
-#: cps/editbooks.py:1096
+#: cps/editbooks.py:1155
#, python-format
msgid "There was an error converting this book: %(res)s"
msgstr ""
@@ -595,174 +602,190 @@ msgstr ""
msgid "Callback domain is not verified, please follow steps to verify domain in google developer console"
msgstr ""
-#: cps/helper.py:80
+#: cps/helper.py:81
#, python-format
msgid "%(format)s format not found for book id: %(book)d"
msgstr ""
-#: cps/helper.py:86 cps/tasks/convert.py:73
+#: cps/helper.py:87 cps/tasks/convert.py:75
#, python-format
msgid "%(format)s not found on Google Drive: %(fn)s"
msgstr ""
-#: cps/helper.py:91
+#: cps/helper.py:92
#, python-format
msgid "%(format)s not found: %(fn)s"
msgstr ""
-#: cps/helper.py:96 cps/helper.py:220 cps/templates/detail.html:41
+#: cps/helper.py:97 cps/helper.py:221 cps/templates/detail.html:41
#: cps/templates/detail.html:45
msgid "Send to Kindle"
msgstr ""
-#: cps/helper.py:97 cps/helper.py:114 cps/helper.py:222
+#: cps/helper.py:98 cps/helper.py:115 cps/helper.py:223
msgid "This e-mail has been sent via Calibre-Web."
msgstr ""
-#: cps/helper.py:112
+#: cps/helper.py:113
msgid "Calibre-Web test e-mail"
msgstr ""
-#: cps/helper.py:113
+#: cps/helper.py:114
msgid "Test e-mail"
msgstr ""
-#: cps/helper.py:130
+#: cps/helper.py:131
msgid "Get Started with Calibre-Web"
msgstr ""
-#: cps/helper.py:135
+#: cps/helper.py:136
#, python-format
msgid "Registration e-mail for user: %(name)s"
msgstr ""
-#: cps/helper.py:146 cps/helper.py:152
+#: cps/helper.py:147 cps/helper.py:153
#, python-format
msgid "Convert %(orig)s to %(format)s and send to Kindle"
msgstr ""
-#: cps/helper.py:171 cps/helper.py:175 cps/helper.py:179
+#: cps/helper.py:172 cps/helper.py:176 cps/helper.py:180
#, python-format
msgid "Send %(format)s to Kindle"
msgstr ""
-#: cps/helper.py:219 cps/tasks/convert.py:90
+#: cps/helper.py:220 cps/tasks/convert.py:92
#, python-format
msgid "%(book)s send to Kindle"
msgstr ""
-#: cps/helper.py:224
+#: cps/helper.py:225
msgid "The requested file could not be read. Maybe wrong permissions?"
msgstr ""
-#: cps/helper.py:316
+#: cps/helper.py:353
+msgid "Read status could not set: {}"
+msgstr ""
+
+#: cps/helper.py:376
#, python-format
msgid "Deleting bookfolder for book %(id)s failed, path has subfolders: %(path)s"
msgstr ""
-#: cps/helper.py:322
+#: cps/helper.py:382
#, python-format
msgid "Deleting book %(id)s failed: %(message)s"
msgstr ""
-#: cps/helper.py:333
+#: cps/helper.py:393
#, python-format
msgid "Deleting book %(id)s from database only, book path in database not valid: %(path)s"
msgstr ""
-#: cps/helper.py:388
+#: cps/helper.py:458
#, python-format
-msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
msgstr ""
-#: cps/helper.py:403
-#, python-format
-msgid "Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s"
-msgstr ""
-
-#: cps/helper.py:428 cps/helper.py:438 cps/helper.py:446
+#: cps/helper.py:529 cps/helper.py:538
#, python-format
msgid "File %(file)s not found on Google Drive"
msgstr ""
-#: cps/helper.py:467
+#: cps/helper.py:572
+#, python-format
+msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s"
+msgstr ""
+
+#: cps/helper.py:592
+msgid "Error in rename file in path: {}"
+msgstr ""
+
+#: cps/helper.py:610
#, python-format
msgid "Book path %(path)s not found on Google Drive"
msgstr ""
-#: cps/helper.py:507 cps/web.py:1658
+#: cps/helper.py:651 cps/web.py:1737
msgid "Found an existing account for this e-mail address"
msgstr ""
-#: cps/helper.py:515
+#: cps/helper.py:659
msgid "This username is already taken"
msgstr ""
-#: cps/helper.py:525
+#: cps/helper.py:669
msgid "Invalid e-mail address format"
msgstr ""
-#: cps/helper.py:598
+#: cps/helper.py:754
+msgid "Python modul 'advocate' is not installed but is needed for cover downloads"
+msgstr ""
+
+#: cps/helper.py:767
msgid "Error Downloading Cover"
msgstr ""
-#: cps/helper.py:601
+#: cps/helper.py:770
msgid "Cover Format Error"
msgstr ""
-#: cps/helper.py:611
+#: cps/helper.py:773
+msgid "You are not allowed to access localhost or the local network for cover uploads"
+msgstr ""
+
+#: cps/helper.py:783
msgid "Failed to create path for cover"
msgstr ""
-#: cps/helper.py:627
+#: cps/helper.py:799
msgid "Cover-file is not a valid image file, or could not be stored"
msgstr ""
-#: cps/helper.py:638
+#: cps/helper.py:810
msgid "Only jpg/jpeg/png/webp/bmp files are supported as coverfile"
msgstr ""
-#: cps/helper.py:651
+#: cps/helper.py:822
msgid "Invalid cover file content"
msgstr ""
-#: cps/helper.py:655
+#: cps/helper.py:826
msgid "Only jpg/jpeg files are supported as coverfile"
msgstr ""
-#: cps/helper.py:707
+#: cps/helper.py:878
msgid "Unrar binary file not found"
msgstr ""
-#: cps/helper.py:718
+#: cps/helper.py:889
msgid "Error excecuting UnRar"
msgstr ""
-#: cps/helper.py:766
+#: cps/helper.py:937
msgid "Waiting"
msgstr ""
-#: cps/helper.py:768
+#: cps/helper.py:939
msgid "Failed"
msgstr ""
-#: cps/helper.py:770
+#: cps/helper.py:941
msgid "Started"
msgstr ""
-#: cps/helper.py:772
+#: cps/helper.py:943
msgid "Finished"
msgstr ""
-#: cps/helper.py:774
+#: cps/helper.py:945
msgid "Unknown Status"
msgstr ""
-#: cps/kobo_auth.py:131
-msgid "PLease access calibre-web from non localhost to get valid api_endpoint for kobo device"
+#: cps/kobo_auth.py:128
+msgid "Please access Calibre-Web from non localhost to get valid api_endpoint for kobo device"
msgstr ""
-#: cps/kobo_auth.py:134 cps/kobo_auth.py:162
+#: cps/kobo_auth.py:154
msgid "Kobo Setup"
msgstr ""
@@ -771,7 +794,7 @@ msgstr ""
msgid "Register with %(provider)s"
msgstr ""
-#: cps/oauth_bb.py:138 cps/remotelogin.py:133 cps/web.py:1534
+#: cps/oauth_bb.py:138 cps/remotelogin.py:130 cps/web.py:1613
#, python-format
msgid "you are now logged in as: '%(nickname)s'"
msgstr ""
@@ -832,163 +855,163 @@ msgstr ""
msgid "Google Oauth error: {}"
msgstr ""
-#: cps/opds.py:384
+#: cps/opds.py:298
msgid "{} Stars"
msgstr ""
-#: cps/remotelogin.py:65 cps/templates/layout.html:84
-#: cps/templates/login.html:4 cps/templates/login.html:21 cps/web.py:1583
+#: cps/remotelogin.py:62 cps/templates/layout.html:84
+#: cps/templates/login.html:4 cps/templates/login.html:21 cps/web.py:1662
msgid "Login"
msgstr ""
-#: cps/remotelogin.py:77 cps/remotelogin.py:111
+#: cps/remotelogin.py:74 cps/remotelogin.py:108
msgid "Token not found"
msgstr ""
-#: cps/remotelogin.py:86 cps/remotelogin.py:119
+#: cps/remotelogin.py:83 cps/remotelogin.py:116
msgid "Token has expired"
msgstr ""
-#: cps/remotelogin.py:95
+#: cps/remotelogin.py:92
msgid "Success! Please return to your device"
msgstr ""
-#: cps/render_template.py:39 cps/web.py:416
+#: cps/render_template.py:41 cps/web.py:407
msgid "Books"
msgstr ""
-#: cps/render_template.py:41
+#: cps/render_template.py:43
msgid "Show recent books"
msgstr ""
-#: cps/render_template.py:42 cps/templates/index.xml:25
+#: cps/render_template.py:44 cps/templates/index.xml:25
msgid "Hot Books"
msgstr ""
-#: cps/render_template.py:44
+#: cps/render_template.py:46
msgid "Show Hot Books"
msgstr ""
-#: cps/render_template.py:46 cps/render_template.py:51
+#: cps/render_template.py:48 cps/render_template.py:53
msgid "Downloaded Books"
msgstr ""
-#: cps/render_template.py:48 cps/render_template.py:53
+#: cps/render_template.py:50 cps/render_template.py:55
#: cps/templates/user_table.html:167
msgid "Show Downloaded Books"
msgstr ""
-#: cps/render_template.py:56 cps/templates/index.xml:32 cps/web.py:430
+#: cps/render_template.py:58 cps/templates/index.xml:32 cps/web.py:422
msgid "Top Rated Books"
msgstr ""
-#: cps/render_template.py:58 cps/templates/user_table.html:161
+#: cps/render_template.py:60 cps/templates/user_table.html:161
msgid "Show Top Rated Books"
msgstr ""
-#: cps/render_template.py:59 cps/templates/index.xml:54
-#: cps/templates/index.xml:58 cps/web.py:676
+#: cps/render_template.py:61 cps/templates/index.xml:54
+#: cps/templates/index.xml:58 cps/web.py:729
msgid "Read Books"
msgstr ""
-#: cps/render_template.py:61
+#: cps/render_template.py:63
msgid "Show read and unread"
msgstr ""
-#: cps/render_template.py:63 cps/templates/index.xml:61
-#: cps/templates/index.xml:65 cps/web.py:679
+#: cps/render_template.py:65 cps/templates/index.xml:61
+#: cps/templates/index.xml:65 cps/web.py:732
msgid "Unread Books"
msgstr ""
-#: cps/render_template.py:65
+#: cps/render_template.py:67
msgid "Show unread"
msgstr ""
-#: cps/render_template.py:66
+#: cps/render_template.py:68
msgid "Discover"
msgstr ""
-#: cps/render_template.py:68 cps/templates/index.xml:50
+#: cps/render_template.py:70 cps/templates/index.xml:50
#: cps/templates/user_table.html:162
msgid "Show Random Books"
msgstr ""
-#: cps/render_template.py:69 cps/templates/book_table.html:67
-#: cps/templates/index.xml:83 cps/web.py:1041
+#: cps/render_template.py:71 cps/templates/book_table.html:67
+#: cps/templates/index.xml:83 cps/web.py:1135
msgid "Categories"
msgstr ""
-#: cps/render_template.py:71 cps/templates/user_table.html:158
+#: cps/render_template.py:73 cps/templates/user_table.html:158
msgid "Show category selection"
msgstr ""
-#: cps/render_template.py:72 cps/templates/book_edit.html:90
+#: cps/render_template.py:74 cps/templates/book_edit.html:90
#: cps/templates/book_table.html:68 cps/templates/index.xml:90
-#: cps/templates/search_form.html:69 cps/web.py:948 cps/web.py:959
+#: cps/templates/search_form.html:69 cps/web.py:1034 cps/web.py:1041
msgid "Series"
msgstr ""
-#: cps/render_template.py:74 cps/templates/user_table.html:157
+#: cps/render_template.py:76 cps/templates/user_table.html:157
msgid "Show series selection"
msgstr ""
-#: cps/render_template.py:75 cps/templates/book_table.html:66
+#: cps/render_template.py:77 cps/templates/book_table.html:66
#: cps/templates/index.xml:69
msgid "Authors"
msgstr ""
-#: cps/render_template.py:77 cps/templates/user_table.html:160
+#: cps/render_template.py:79 cps/templates/user_table.html:160
msgid "Show author selection"
msgstr ""
-#: cps/render_template.py:79 cps/templates/book_table.html:72
-#: cps/templates/index.xml:76 cps/web.py:925
+#: cps/render_template.py:81 cps/templates/book_table.html:72
+#: cps/templates/index.xml:76 cps/web.py:1006
msgid "Publishers"
msgstr ""
-#: cps/render_template.py:81 cps/templates/user_table.html:163
+#: cps/render_template.py:83 cps/templates/user_table.html:163
msgid "Show publisher selection"
msgstr ""
-#: cps/render_template.py:82 cps/templates/book_table.html:70
+#: cps/render_template.py:84 cps/templates/book_table.html:70
#: cps/templates/index.xml:97 cps/templates/search_form.html:107
-#: cps/web.py:1018
+#: cps/web.py:1108
msgid "Languages"
msgstr ""
-#: cps/render_template.py:85 cps/templates/user_table.html:155
+#: cps/render_template.py:87 cps/templates/user_table.html:155
msgid "Show language selection"
msgstr ""
-#: cps/render_template.py:86 cps/templates/index.xml:104
+#: cps/render_template.py:88 cps/templates/index.xml:104
msgid "Ratings"
msgstr ""
-#: cps/render_template.py:88 cps/templates/user_table.html:164
+#: cps/render_template.py:90 cps/templates/user_table.html:164
msgid "Show ratings selection"
msgstr ""
-#: cps/render_template.py:89 cps/templates/index.xml:112
+#: cps/render_template.py:91 cps/templates/index.xml:112
msgid "File formats"
msgstr ""
-#: cps/render_template.py:91 cps/templates/user_table.html:165
+#: cps/render_template.py:93 cps/templates/user_table.html:165
msgid "Show file formats selection"
msgstr ""
-#: cps/render_template.py:93 cps/web.py:703
+#: cps/render_template.py:95 cps/web.py:755
msgid "Archived Books"
msgstr ""
-#: cps/render_template.py:95 cps/templates/user_table.html:166
+#: cps/render_template.py:97 cps/templates/user_table.html:166
msgid "Show archived books"
msgstr ""
-#: cps/render_template.py:97 cps/web.py:780
+#: cps/render_template.py:100 cps/web.py:837
msgid "Books List"
msgstr ""
-#: cps/render_template.py:99 cps/templates/user_table.html:168
+#: cps/render_template.py:102 cps/templates/user_table.html:168
msgid "Show Books List"
msgstr ""
@@ -1042,256 +1065,264 @@ msgstr ""
msgid "Create a Shelf"
msgstr ""
-#: cps/shelf.py:237
+#: cps/shelf.py:236
msgid "Sorry you are not allowed to edit this shelf"
msgstr ""
-#: cps/shelf.py:239
+#: cps/shelf.py:238
msgid "Edit a shelf"
msgstr ""
-#: cps/shelf.py:249
+#: cps/shelf.py:248
msgid "Sorry you are not allowed to create a public shelf"
msgstr ""
-#: cps/shelf.py:261
+#: cps/shelf.py:265
#, python-format
msgid "Shelf %(title)s created"
msgstr ""
-#: cps/shelf.py:264
+#: cps/shelf.py:268
#, python-format
msgid "Shelf %(title)s changed"
msgstr ""
-#: cps/shelf.py:278
+#: cps/shelf.py:282
msgid "There was an error"
msgstr ""
-#: cps/shelf.py:300
+#: cps/shelf.py:304
#, python-format
msgid "A public shelf with the name '%(title)s' already exists."
msgstr ""
-#: cps/shelf.py:311
+#: cps/shelf.py:315
#, python-format
msgid "A private shelf with the name '%(title)s' already exists."
msgstr ""
-#: cps/shelf.py:380
+#: cps/shelf.py:337
+msgid "Error deleting Shelf"
+msgstr ""
+
+#: cps/shelf.py:339
+msgid "Shelf successfully deleted"
+msgstr ""
+
+#: cps/shelf.py:389
#, python-format
msgid "Change order of Shelf: '%(name)s'"
msgstr ""
-#: cps/shelf.py:450
+#: cps/shelf.py:461
#, python-format
msgid "Shelf: '%(name)s'"
msgstr ""
-#: cps/shelf.py:454
+#: cps/shelf.py:465
msgid "Error opening shelf. Shelf does not exist or is not accessible"
msgstr ""
-#: cps/updater.py:403 cps/updater.py:414 cps/updater.py:514 cps/updater.py:529
+#: cps/updater.py:426 cps/updater.py:437 cps/updater.py:538 cps/updater.py:553
msgid "Unexpected data while reading update information"
msgstr ""
-#: cps/updater.py:410 cps/updater.py:521
+#: cps/updater.py:433 cps/updater.py:545
msgid "No update available. You already have the latest version installed"
msgstr ""
-#: cps/updater.py:428
+#: cps/updater.py:451
msgid "A new update is available. Click on the button below to update to the latest version."
msgstr ""
-#: cps/updater.py:446
+#: cps/updater.py:469
msgid "Could not fetch update information"
msgstr ""
-#: cps/updater.py:456
+#: cps/updater.py:479
msgid "Click on the button below to update to the latest stable version."
msgstr ""
-#: cps/updater.py:465 cps/updater.py:479 cps/updater.py:490
+#: cps/updater.py:488 cps/updater.py:502 cps/updater.py:513
#, python-format
msgid "A new update is available. Click on the button below to update to version: %(version)s"
msgstr ""
-#: cps/updater.py:507
+#: cps/updater.py:531
msgid "No release information available"
msgstr ""
-#: cps/templates/index.html:5 cps/web.py:440
+#: cps/templates/index.html:5 cps/web.py:434
msgid "Discover (Random Books)"
msgstr ""
-#: cps/web.py:471
+#: cps/web.py:470
msgid "Hot Books (Most Downloaded)"
msgstr ""
-#: cps/web.py:507
+#: cps/web.py:501
#, python-format
msgid "Downloaded books by %(user)s"
msgstr ""
-#: cps/web.py:539
+#: cps/web.py:534
#, python-format
msgid "Author: %(name)s"
msgstr ""
-#: cps/web.py:554
+#: cps/web.py:570
#, python-format
msgid "Publisher: %(name)s"
msgstr ""
-#: cps/web.py:569
+#: cps/web.py:598
#, python-format
msgid "Series: %(serie)s"
msgstr ""
-#: cps/web.py:582
+#: cps/web.py:610
#, python-format
msgid "Rating: %(rating)s stars"
msgstr ""
-#: cps/web.py:597
+#: cps/web.py:626
#, python-format
msgid "File format: %(format)s"
msgstr ""
-#: cps/web.py:615
+#: cps/web.py:663
#, python-format
msgid "Category: %(name)s"
msgstr ""
-#: cps/web.py:631
+#: cps/web.py:690
#, python-format
msgid "Language: %(name)s"
msgstr ""
-#: cps/templates/layout.html:56 cps/web.py:737 cps/web.py:1370
+#: cps/templates/layout.html:56 cps/web.py:789 cps/web.py:1444
msgid "Advanced Search"
msgstr ""
#: cps/templates/book_edit.html:235 cps/templates/feed.xml:33
#: cps/templates/index.xml:11 cps/templates/layout.html:45
#: cps/templates/layout.html:48 cps/templates/search_form.html:226
-#: cps/web.py:750 cps/web.py:1076
+#: cps/web.py:807 cps/web.py:1164
msgid "Search"
msgstr ""
-#: cps/templates/admin.html:16 cps/web.py:903
+#: cps/templates/admin.html:16 cps/web.py:979
msgid "Downloads"
msgstr ""
-#: cps/web.py:980
+#: cps/web.py:1068
msgid "Ratings list"
msgstr ""
-#: cps/web.py:1001
+#: cps/web.py:1095
msgid "File formats list"
msgstr ""
-#: cps/templates/layout.html:73 cps/templates/tasks.html:7 cps/web.py:1055
+#: cps/templates/layout.html:73 cps/templates/tasks.html:7 cps/web.py:1149
msgid "Tasks"
msgstr ""
-#: cps/web.py:1214
+#: cps/web.py:1286
msgid "Published after "
msgstr ""
-#: cps/web.py:1221
+#: cps/web.py:1293
msgid "Published before "
msgstr ""
-#: cps/web.py:1243
+#: cps/web.py:1315
#, python-format
msgid "Rating <= %(rating)s"
msgstr ""
-#: cps/web.py:1245
+#: cps/web.py:1317
#, python-format
msgid "Rating >= %(rating)s"
msgstr ""
-#: cps/web.py:1247
+#: cps/web.py:1319
#, python-format
msgid "Read Status = %(status)s"
msgstr ""
-#: cps/web.py:1352
+#: cps/web.py:1425
msgid "Error on search for custom columns, please restart Calibre-Web"
msgstr ""
-#: cps/web.py:1448
+#: cps/web.py:1527
#, python-format
msgid "Book successfully queued for sending to %(kindlemail)s"
msgstr ""
-#: cps/web.py:1452
+#: cps/web.py:1531
#, python-format
msgid "Oops! There was an error sending this book: %(res)s"
msgstr ""
-#: cps/web.py:1454
+#: cps/web.py:1533
msgid "Please update your profile with a valid Send to Kindle E-mail Address."
msgstr ""
-#: cps/web.py:1471
+#: cps/web.py:1550
msgid "E-Mail server is not configured, please contact your administrator!"
msgstr ""
-#: cps/templates/layout.html:85 cps/templates/register.html:17 cps/web.py:1472
-#: cps/web.py:1479 cps/web.py:1485 cps/web.py:1504 cps/web.py:1508
-#: cps/web.py:1514
+#: cps/templates/layout.html:85 cps/templates/register.html:17 cps/web.py:1551
+#: cps/web.py:1558 cps/web.py:1564 cps/web.py:1583 cps/web.py:1587
+#: cps/web.py:1593
msgid "Register"
msgstr ""
-#: cps/web.py:1506
+#: cps/web.py:1585
msgid "Your e-mail is not allowed to register"
msgstr ""
-#: cps/web.py:1509
+#: cps/web.py:1588
msgid "Confirmation e-mail was send to your e-mail account."
msgstr ""
-#: cps/web.py:1523
+#: cps/web.py:1602
msgid "Cannot activate LDAP authentication"
msgstr ""
-#: cps/web.py:1542
+#: cps/web.py:1621
#, python-format
msgid "Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known"
msgstr ""
-#: cps/web.py:1548
+#: cps/web.py:1627
#, python-format
msgid "Could not login: %(message)s"
msgstr ""
-#: cps/web.py:1552 cps/web.py:1577
+#: cps/web.py:1631 cps/web.py:1656
msgid "Wrong Username or Password"
msgstr ""
-#: cps/web.py:1559
+#: cps/web.py:1638
msgid "New Password was send to your email address"
msgstr ""
-#: cps/web.py:1565
+#: cps/web.py:1644
msgid "Please enter valid username to reset password"
msgstr ""
-#: cps/web.py:1572
+#: cps/web.py:1651
#, python-format
msgid "You are now logged in as: '%(nickname)s'"
msgstr ""
-#: cps/web.py:1638 cps/web.py:1687
+#: cps/web.py:1717 cps/web.py:1766
#, python-format
msgid "%(name)s's profile"
msgstr ""
-#: cps/web.py:1654
+#: cps/web.py:1733
msgid "Profile updated"
msgstr ""
@@ -1299,36 +1330,36 @@ msgstr ""
msgid "Found no valid gmail.json file with OAuth information"
msgstr ""
-#: cps/tasks/convert.py:137
+#: cps/tasks/convert.py:154
#, python-format
msgid "Calibre ebook-convert %(tool)s not found"
msgstr ""
-#: cps/tasks/convert.py:163
+#: cps/tasks/convert.py:187
#, python-format
msgid "%(format)s format not found on disk"
msgstr ""
-#: cps/tasks/convert.py:167
+#: cps/tasks/convert.py:191
msgid "Ebook converter failed with unknown error"
msgstr ""
-#: cps/tasks/convert.py:177
+#: cps/tasks/convert.py:201
#, python-format
msgid "Kepubify-converter failed: %(error)s"
msgstr ""
-#: cps/tasks/convert.py:199
+#: cps/tasks/convert.py:223
#, python-format
msgid "Converted file not found or more than one file in folder %(folder)s"
msgstr ""
-#: cps/tasks/convert.py:222
+#: cps/tasks/convert.py:246
#, python-format
msgid "Ebook-converter failed: %(error)s"
msgstr ""
-#: cps/tasks/convert.py:241
+#: cps/tasks/convert.py:269
#, python-format
msgid "Calibre failed with error: %(error)s"
msgstr ""
@@ -1368,7 +1399,7 @@ msgid "Upload"
msgstr ""
#: cps/templates/admin.html:22 cps/templates/detail.html:18
-#: cps/templates/detail.html:27 cps/templates/shelf.html:6
+#: cps/templates/detail.html:27 cps/templates/shelf.html:7
#: cps/templates/user_table.html:146
msgid "Download"
msgstr ""
@@ -1383,7 +1414,7 @@ msgid "Edit"
msgstr ""
#: cps/templates/admin.html:25 cps/templates/book_edit.html:16
-#: cps/templates/book_table.html:97 cps/templates/modal_dialogs.html:63
+#: cps/templates/book_table.html:100 cps/templates/modal_dialogs.html:63
#: cps/templates/modal_dialogs.html:116 cps/templates/user_edit.html:67
#: cps/templates/user_table.html:149
msgid "Delete"
@@ -1393,180 +1424,179 @@ msgstr ""
msgid "Public Shelf"
msgstr ""
-#: cps/templates/admin.html:51
+#: cps/templates/admin.html:53
msgid "Add New User"
msgstr ""
-#: cps/templates/admin.html:53
+#: cps/templates/admin.html:55
msgid "Import LDAP Users"
msgstr ""
-#: cps/templates/admin.html:60
+#: cps/templates/admin.html:62
msgid "E-mail Server Settings"
msgstr ""
-#: cps/templates/admin.html:65 cps/templates/email_edit.html:31
+#: cps/templates/admin.html:67 cps/templates/email_edit.html:31
msgid "SMTP Hostname"
msgstr ""
-#: cps/templates/admin.html:69 cps/templates/email_edit.html:35
+#: cps/templates/admin.html:71 cps/templates/email_edit.html:35
msgid "SMTP Port"
msgstr ""
-#: cps/templates/admin.html:73 cps/templates/email_edit.html:39
+#: cps/templates/admin.html:75 cps/templates/email_edit.html:39
msgid "Encryption"
msgstr ""
-#: cps/templates/admin.html:77 cps/templates/email_edit.html:47
+#: cps/templates/admin.html:79 cps/templates/email_edit.html:47
msgid "SMTP Login"
msgstr ""
-#: cps/templates/admin.html:81 cps/templates/admin.html:92
+#: cps/templates/admin.html:83 cps/templates/admin.html:94
#: cps/templates/email_edit.html:55
msgid "From E-mail"
msgstr ""
-#: cps/templates/admin.html:88
+#: cps/templates/admin.html:90
msgid "E-Mail Service"
msgstr ""
-#: cps/templates/admin.html:89
+#: cps/templates/admin.html:91
msgid "Gmail via Oauth2"
msgstr ""
-#: cps/templates/admin.html:104
+#: cps/templates/admin.html:106
msgid "Configuration"
msgstr ""
-#: cps/templates/admin.html:107
+#: cps/templates/admin.html:109
msgid "Calibre Database Directory"
msgstr ""
-#: cps/templates/admin.html:111 cps/templates/config_edit.html:68
+#: cps/templates/admin.html:113 cps/templates/config_edit.html:68
msgid "Log Level"
msgstr ""
-#: cps/templates/admin.html:115
+#: cps/templates/admin.html:117
msgid "Port"
msgstr ""
-#: cps/templates/admin.html:120
+#: cps/templates/admin.html:122
msgid "External Port"
msgstr ""
-#: cps/templates/admin.html:127 cps/templates/config_view_edit.html:28
+#: cps/templates/admin.html:129 cps/templates/config_view_edit.html:28
msgid "Books per Page"
msgstr ""
-#: cps/templates/admin.html:131
+#: cps/templates/admin.html:133
msgid "Uploads"
msgstr ""
-#: cps/templates/admin.html:135
+#: cps/templates/admin.html:137
msgid "Anonymous Browsing"
msgstr ""
-#: cps/templates/admin.html:139
+#: cps/templates/admin.html:141
msgid "Public Registration"
msgstr ""
-#: cps/templates/admin.html:143
+#: cps/templates/admin.html:145
msgid "Magic Link Remote Login"
msgstr ""
-#: cps/templates/admin.html:147
+#: cps/templates/admin.html:149
msgid "Reverse Proxy Login"
msgstr ""
-#: cps/templates/admin.html:152 cps/templates/config_edit.html:173
+#: cps/templates/admin.html:154 cps/templates/config_edit.html:173
msgid "Reverse Proxy Header Name"
msgstr ""
-#: cps/templates/admin.html:157
+#: cps/templates/admin.html:159
msgid "Edit Calibre Database Configuration"
msgstr ""
-#: cps/templates/admin.html:158
+#: cps/templates/admin.html:160
msgid "Edit Basic Configuration"
msgstr ""
-#: cps/templates/admin.html:159
+#: cps/templates/admin.html:161
msgid "Edit UI Configuration"
msgstr ""
-#: cps/templates/admin.html:164
+#: cps/templates/admin.html:166
msgid "Administration"
msgstr ""
-#: cps/templates/admin.html:165
+#: cps/templates/admin.html:167
msgid "Download Debug Package"
msgstr ""
-#: cps/templates/admin.html:166
+#: cps/templates/admin.html:168
msgid "View Logs"
msgstr ""
-#: cps/templates/admin.html:169
+#: cps/templates/admin.html:171
msgid "Reconnect Calibre Database"
msgstr ""
-#: cps/templates/admin.html:170
+#: cps/templates/admin.html:172
msgid "Restart"
msgstr ""
-#: cps/templates/admin.html:171
+#: cps/templates/admin.html:173
msgid "Shutdown"
msgstr ""
-#: cps/templates/admin.html:176
+#: cps/templates/admin.html:178
msgid "Update"
msgstr ""
-#: cps/templates/admin.html:180
+#: cps/templates/admin.html:182
msgid "Version"
msgstr ""
-#: cps/templates/admin.html:181
+#: cps/templates/admin.html:183
msgid "Details"
msgstr ""
-#: cps/templates/admin.html:187
+#: cps/templates/admin.html:189
msgid "Current version"
msgstr ""
-#: cps/templates/admin.html:195
+#: cps/templates/admin.html:196
msgid "Check for Update"
msgstr ""
-#: cps/templates/admin.html:196
+#: cps/templates/admin.html:197
msgid "Perform Update"
msgstr ""
-#: cps/templates/admin.html:209
+#: cps/templates/admin.html:210
msgid "Are you sure you want to restart?"
msgstr ""
-#: cps/templates/admin.html:214 cps/templates/admin.html:228
-#: cps/templates/admin.html:248 cps/templates/config_db.html:70
-#: cps/templates/shelf.html:96
+#: cps/templates/admin.html:215 cps/templates/admin.html:229
+#: cps/templates/admin.html:249 cps/templates/config_db.html:70
msgid "OK"
msgstr ""
-#: cps/templates/admin.html:215 cps/templates/admin.html:229
-#: cps/templates/book_edit.html:213 cps/templates/book_table.html:124
+#: cps/templates/admin.html:216 cps/templates/admin.html:230
+#: cps/templates/book_edit.html:213 cps/templates/book_table.html:127
#: cps/templates/config_db.html:54 cps/templates/config_edit.html:359
-#: cps/templates/config_view_edit.html:173 cps/templates/modal_dialogs.html:64
+#: cps/templates/config_view_edit.html:175 cps/templates/modal_dialogs.html:64
#: cps/templates/modal_dialogs.html:99 cps/templates/modal_dialogs.html:117
-#: cps/templates/modal_dialogs.html:135 cps/templates/shelf.html:97
-#: cps/templates/shelf_edit.html:27 cps/templates/user_edit.html:144
+#: cps/templates/modal_dialogs.html:135 cps/templates/shelf_edit.html:27
+#: cps/templates/user_edit.html:144
msgid "Cancel"
msgstr ""
-#: cps/templates/admin.html:227
+#: cps/templates/admin.html:228
msgid "Are you sure you want to shutdown?"
msgstr ""
-#: cps/templates/admin.html:239
+#: cps/templates/admin.html:240
msgid "Updating, please do not reload this page"
msgstr ""
@@ -1578,44 +1608,43 @@ msgstr ""
msgid "In Library"
msgstr ""
-#: cps/templates/author.html:26 cps/templates/index.html:72
-#: cps/templates/search.html:29 cps/templates/shelf.html:17
+#: cps/templates/author.html:26 cps/templates/index.html:73
+#: cps/templates/search.html:30 cps/templates/shelf.html:19
msgid "Sort according to book date, newest first"
msgstr ""
-#: cps/templates/author.html:27 cps/templates/index.html:73
-#: cps/templates/search.html:30 cps/templates/shelf.html:18
+#: cps/templates/author.html:27 cps/templates/index.html:74
+#: cps/templates/search.html:31 cps/templates/shelf.html:20
msgid "Sort according to book date, oldest first"
msgstr ""
-#: cps/templates/author.html:28 cps/templates/index.html:74
-#: cps/templates/search.html:31 cps/templates/shelf.html:19
+#: cps/templates/author.html:28 cps/templates/index.html:75
+#: cps/templates/search.html:32 cps/templates/shelf.html:21
msgid "Sort title in alphabetical order"
msgstr ""
-#: cps/templates/author.html:29 cps/templates/index.html:75
-#: cps/templates/search.html:32 cps/templates/shelf.html:20
+#: cps/templates/author.html:29 cps/templates/index.html:76
+#: cps/templates/search.html:33 cps/templates/shelf.html:22
msgid "Sort title in reverse alphabetical order"
msgstr ""
-#: cps/templates/author.html:30 cps/templates/index.html:78
-#: cps/templates/search.html:35 cps/templates/shelf.html:23
+#: cps/templates/author.html:30 cps/templates/index.html:79
+#: cps/templates/search.html:36 cps/templates/shelf.html:25
msgid "Sort according to publishing date, newest first"
msgstr ""
-#: cps/templates/author.html:31 cps/templates/index.html:79
-#: cps/templates/search.html:36 cps/templates/shelf.html:24
+#: cps/templates/author.html:31 cps/templates/index.html:80
+#: cps/templates/search.html:37 cps/templates/shelf.html:26
msgid "Sort according to publishing date, oldest first"
msgstr ""
-#: cps/templates/author.html:57 cps/templates/author.html:117
-#: cps/templates/discover.html:30 cps/templates/index.html:29
-#: cps/templates/index.html:111 cps/templates/search.html:65
-#: cps/templates/shelf.html:52
+#: cps/templates/author.html:56 cps/templates/author.html:115
+#: cps/templates/index.html:29 cps/templates/index.html:112
+#: cps/templates/search.html:66 cps/templates/shelf.html:54
msgid "reduce"
msgstr ""
-#: cps/templates/author.html:101
+#: cps/templates/author.html:99
msgid "More by"
msgstr ""
@@ -1740,7 +1769,7 @@ msgid "Fetch Metadata"
msgstr ""
#: cps/templates/book_edit.html:212 cps/templates/config_db.html:53
-#: cps/templates/config_edit.html:358 cps/templates/config_view_edit.html:172
+#: cps/templates/config_edit.html:358 cps/templates/config_view_edit.html:174
#: cps/templates/email_edit.html:65 cps/templates/shelf_edit.html:25
#: cps/templates/shelf_order.html:41 cps/templates/user_edit.html:142
msgid "Save"
@@ -1867,26 +1896,34 @@ msgstr ""
msgid "Comments"
msgstr ""
-#: cps/templates/book_table.html:77 cps/templates/book_table.html:79
-#: cps/templates/book_table.html:81 cps/templates/book_table.html:83
-#: cps/templates/book_table.html:87 cps/templates/book_table.html:89
-#: cps/templates/book_table.html:91 cps/templates/book_table.html:93
+#: cps/templates/book_table.html:75
+msgid "Archiv Status"
+msgstr ""
+
+#: cps/templates/book_table.html:77 cps/templates/search_form.html:42
+msgid "Read Status"
+msgstr ""
+
+#: cps/templates/book_table.html:80 cps/templates/book_table.html:82
+#: cps/templates/book_table.html:84 cps/templates/book_table.html:86
+#: cps/templates/book_table.html:90 cps/templates/book_table.html:92
+#: cps/templates/book_table.html:96
msgid "Enter "
msgstr ""
-#: cps/templates/book_table.html:110 cps/templates/modal_dialogs.html:46
+#: cps/templates/book_table.html:113 cps/templates/modal_dialogs.html:46
msgid "Are you really sure?"
msgstr ""
-#: cps/templates/book_table.html:114
+#: cps/templates/book_table.html:117
msgid "Books with Title will be merged from:"
msgstr ""
-#: cps/templates/book_table.html:118
+#: cps/templates/book_table.html:121
msgid "Into Book with Title:"
msgstr ""
-#: cps/templates/book_table.html:123
+#: cps/templates/book_table.html:126
msgid "Merge"
msgstr ""
@@ -2062,11 +2099,6 @@ msgstr ""
msgid "LDAP Encryption"
msgstr ""
-#: cps/templates/config_edit.html:204 cps/templates/config_view_edit.html:62
-#: cps/templates/email_edit.html:41
-msgid "None"
-msgstr ""
-
#: cps/templates/config_edit.html:205
msgid "TLS"
msgstr ""
@@ -2283,11 +2315,11 @@ msgstr ""
msgid "Show Random Books in Detail View"
msgstr ""
-#: cps/templates/config_view_edit.html:165 cps/templates/user_edit.html:87
+#: cps/templates/config_view_edit.html:166 cps/templates/user_edit.html:87
msgid "Add Allowed/Denied Tags"
msgstr ""
-#: cps/templates/config_view_edit.html:166
+#: cps/templates/config_view_edit.html:167
msgid "Add Allowed/Denied custom column values"
msgstr ""
@@ -2336,13 +2368,13 @@ msgstr ""
msgid "Description:"
msgstr ""
-#: cps/templates/detail.html:256 cps/templates/search.html:14
+#: cps/templates/detail.html:256 cps/templates/search.html:15
msgid "Add to shelf"
msgstr ""
#: cps/templates/detail.html:267 cps/templates/detail.html:284
#: cps/templates/feed.xml:79 cps/templates/layout.html:137
-#: cps/templates/search.html:20
+#: cps/templates/search.html:21
msgid "(Public)"
msgstr ""
@@ -2420,10 +2452,14 @@ msgstr ""
msgid "Next"
msgstr ""
-#: cps/templates/generate_kobo_auth_url.html:5
+#: cps/templates/generate_kobo_auth_url.html:6
msgid "Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):"
msgstr ""
+#: cps/templates/generate_kobo_auth_url.html:11
+msgid "Kobo Token:"
+msgstr ""
+
#: cps/templates/http_error.html:31
msgid "Calibre-Web Instance is unconfigured, please contact your administrator"
msgstr ""
@@ -2440,29 +2476,29 @@ msgstr ""
msgid "Logout User"
msgstr ""
-#: cps/templates/index.html:69
+#: cps/templates/index.html:70
msgid "Sort ascending according to download count"
msgstr ""
-#: cps/templates/index.html:70
+#: cps/templates/index.html:71
msgid "Sort descending according to download count"
msgstr ""
-#: cps/templates/index.html:76 cps/templates/search.html:33
-#: cps/templates/shelf.html:21
+#: cps/templates/index.html:77 cps/templates/search.html:34
+#: cps/templates/shelf.html:23
msgid "Sort authors in alphabetical order"
msgstr ""
-#: cps/templates/index.html:77 cps/templates/search.html:34
-#: cps/templates/shelf.html:22
+#: cps/templates/index.html:78 cps/templates/search.html:35
+#: cps/templates/shelf.html:24
msgid "Sort authors in reverse alphabetical order"
msgstr ""
-#: cps/templates/index.html:81
+#: cps/templates/index.html:82
msgid "Sort ascending according to series index"
msgstr ""
-#: cps/templates/index.html:82
+#: cps/templates/index.html:83
msgid "Sort descending according to series index"
msgstr ""
@@ -2559,7 +2595,7 @@ msgstr ""
msgid "Upload done, processing, please wait..."
msgstr ""
-#: cps/templates/layout.html:76 cps/templates/read.html:71
+#: cps/templates/layout.html:76 cps/templates/read.html:72
#: cps/templates/readcbr.html:84 cps/templates/readcbr.html:108
msgid "Settings"
msgstr ""
@@ -2708,7 +2744,7 @@ msgstr ""
msgid "epub Reader"
msgstr ""
-#: cps/templates/read.html:74
+#: cps/templates/read.html:75
msgid "Reflow text when sidebars are open."
msgstr ""
@@ -2892,10 +2928,6 @@ msgstr ""
msgid "Published Date To"
msgstr ""
-#: cps/templates/search_form.html:42
-msgid "Read Status"
-msgstr ""
-
#: cps/templates/search_form.html:59
msgid "Exclude Tags"
msgstr ""
@@ -2936,30 +2968,26 @@ msgstr ""
msgid "To:"
msgstr ""
-#: cps/templates/shelf.html:11
+#: cps/templates/shelf.html:12
msgid "Delete this Shelf"
msgstr ""
-#: cps/templates/shelf.html:12
+#: cps/templates/shelf.html:13
msgid "Edit Shelf Properties"
msgstr ""
-#: cps/templates/shelf.html:14
+#: cps/templates/shelf.html:16
msgid "Arrange books manually"
msgstr ""
-#: cps/templates/shelf.html:15
+#: cps/templates/shelf.html:17
msgid "Disable Change order"
msgstr ""
-#: cps/templates/shelf.html:15
+#: cps/templates/shelf.html:17
msgid "Enable Change order"
msgstr ""
-#: cps/templates/shelf.html:94
-msgid "Shelf will be deleted for all users"
-msgstr ""
-
#: cps/templates/shelf_edit.html:14
msgid "Share with Everyone"
msgstr ""
diff --git a/optional-requirements.txt b/optional-requirements.txt
index 03f58bb5..4360d221 100644
--- a/optional-requirements.txt
+++ b/optional-requirements.txt
@@ -1,4 +1,5 @@
# GDrive Integration
+google-api-python-client>=1.7.11,<2.50.0
gevent>20.6.0,<22.0.0
greenlet>=0.4.17,<1.2.0
httplib2>=0.9.2,<0.21.0
@@ -8,34 +9,36 @@ pyasn1-modules>=0.0.8,<0.3.0
pyasn1>=0.1.9,<0.5.0
PyDrive2>=1.3.1,<1.11.0
PyYAML>=3.12
-rsa>=3.4.2,<4.8.0
-six>=1.10.0,<1.17.0
-
-# Gdrive and Gmail integration
-google-api-python-client>=1.7.11,<2.32.0
+rsa>=3.4.2,<4.9.0
# Gmail
-google-auth-oauthlib>=0.4.3,<0.5.0
+google-auth-oauthlib>=0.4.3,<0.6.0
+google-api-python-client>=1.7.11,<2.50.0
# goodreads
goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.13.0
# ldap login
-python-ldap>=3.0.0,<3.4.0
+python-ldap>=3.0.0,<3.5.0
Flask-SimpleLDAP>=1.4.0,<1.5.0
-#oauth
+# oauth
Flask-Dance>=2.0.0,<5.2.0
-SQLAlchemy-Utils>=0.33.5,<0.38.0
+SQLAlchemy-Utils>=0.33.5,<0.39.0
-# extracting metadata
-rarfile>=2.7
-scholarly>=1.2.0, <1.5
+# metadata extraction
+rarfile>=3.2
+scholarly>=1.2.0,<1.7
+markdown2>=2.0.0,<2.5.0
+html2text>=2020.1.16,<2022.1.1
+python-dateutil>=2.1,<2.9.0
+beautifulsoup4>=4.0.1,<4.11.0
+cchardet>=2.0.0,<2.2.0
-# other
-natsort>=2.2.0,<8.1.0
+# Comics
+natsort>=2.2.0,<8.2.0
comicapi>=2.2.0,<2.3.0
-#Kobo integration
-jsonschema>=3.2.0,<4.3.0
+# Kobo integration
+jsonschema>=3.2.0,<4.5.0
diff --git a/requirements.txt b/requirements.txt
index 1db961fe..7a30ae06 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,16 +1,20 @@
+APScheduler>=3.6.3,<3.10.0
+werkzeug<2.1.0
Babel>=1.3,<3.0
Flask-Babel>=0.11.1,<2.1.0
-Flask-Login>=0.3.2,<0.5.1
+Flask-Login>=0.3.2,<0.6.1
Flask-Principal>=0.3.2,<0.5.1
backports_abc>=0.4
Flask>=1.0.2,<2.1.0
iso-639>=0.4.5,<0.5.0
-PyPDF3>=1.0.0,<1.0.6
+PyPDF3>=1.0.0,<1.0.7
pytz>=2016.10
-requests>=2.11.1,<2.25.0
+requests>=2.11.1,<2.28.0
SQLAlchemy>=1.3.0,<1.5.0
tornado>=4.1,<6.2
Wand>=0.4.4,<0.7.0
-unidecode>=0.04.19,<1.3.0
-lxml>=3.8.0,<4.7.0
+unidecode>=0.04.19,<1.4.0
+lxml>=3.8.0,<4.9.0
flask-wtf>=0.14.2,<1.1.0
+chardet>=3.0.0,<4.1.0
+advocate>=1.0.0,<1.1.0
diff --git a/setup.cfg b/setup.cfg
index fc74a1ed..aff22e9e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,93 +1,101 @@
[metadata]
name = calibreweb
url = https://github.com/janeczku/calibre-web
-project_urls =
- Bug Tracker = https://github.com/janeczku/calibre-web/issues
- Release Management = https://github.com/janeczku/calibre-web/releases
- Documentation = https://github.com/janeczku/calibre-web/wiki
- Source Code = https://github.com/janeczku/calibre-web
+project_urls =
+ Bug Tracker = https://github.com/janeczku/calibre-web/issues
+ Release Management = https://github.com/janeczku/calibre-web/releases
+ Documentation = https://github.com/janeczku/calibre-web/wiki
+ Source Code = https://github.com/janeczku/calibre-web
description = Web app for browsing, reading and downloading eBooks stored in a Calibre database.
long_description = file: README.md
-long_description_content_type= text/markdown
+long_description_content_type = text/markdown
author = @OzzieIsaacs
author_email = Ozzie.Fernandez.Isaacs@googlemail.com
maintainer = @OzzieIsaacs
license = GPLv3+
license_file = LICENSE
-classifiers =
- Development Status :: 5 - Production/Stable
- License :: OSI Approved :: GNU Affero General Public License v3
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3.5
- Programming Language :: Python :: 3.6
- Programming Language :: Python :: 3.7
- Programming Language :: Python :: 3.8
- Programming Language :: Python :: 3.9
- Programming Language :: Python :: 3.10
- Operating System :: OS Independent
-keywords =
- calibre
- calibre-web
- library
+classifiers =
+ Development Status :: 5 - Production/Stable
+ License :: OSI Approved :: GNU Affero General Public License v3
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.5
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Operating System :: OS Independent
+keywords =
+ calibre
+ calibre-web
+ library
python_requires = >=3.5
[options.entry_points]
-console_scripts =
- cps = calibreweb:main
+console_scripts =
+ cps = calibreweb:main
+
[options]
include_package_data = True
-install_requires =
- Babel>=1.3,<3.0
- Flask-Babel>=0.11.1,<2.1.0
- Flask-Login>=0.3.2,<0.5.1
- Flask-Principal>=0.3.2,<0.5.1
- backports_abc>=0.4
- Flask>=1.0.2,<2.1.0
- iso-639>=0.4.5,<0.5.0
- PyPDF3>=1.0.0,<1.0.6
- pytz>=2016.10
- requests>=2.11.1,<2.25.0
- SQLAlchemy>=1.3.0,<1.5.0
- tornado>=4.1,<6.2
- Wand>=0.4.4,<0.7.0
- unidecode>=0.04.19,<1.3.0
- lxml>=3.8.0,<4.7.0
- flask-wtf>=0.14.2,<1.1.0
+install_requires =
+ APScheduler>=3.6.3,<3.10.0
+ werkzeug<2.1.0
+ Babel>=1.3,<3.0
+ Flask-Babel>=0.11.1,<2.1.0
+ Flask-Login>=0.3.2,<0.6.1
+ Flask-Principal>=0.3.2,<0.5.1
+ backports_abc>=0.4
+ Flask>=1.0.2,<2.1.0
+ iso-639>=0.4.5,<0.5.0
+ PyPDF3>=1.0.0,<1.0.7
+ pytz>=2016.10
+ requests>=2.11.1,<2.28.0
+ SQLAlchemy>=1.3.0,<1.5.0
+ tornado>=4.1,<6.2
+ Wand>=0.4.4,<0.7.0
+ unidecode>=0.04.19,<1.4.0
+ lxml>=3.8.0,<4.9.0
+ flask-wtf>=0.14.2,<1.1.0
+ chardet>=3.0.0,<4.1.0
+ advocate>=1.0.0,<1.1.0
+
[options.extras_require]
-gdrive =
- google-api-python-client>=1.7.11,<2.32.0
- gevent>20.6.0,<22.0.0
- greenlet>=0.4.17,<1.2.0
- httplib2>=0.9.2,<0.21.0
- oauth2client>=4.0.0,<4.1.4
- uritemplate>=3.0.0,<4.2.0
- pyasn1-modules>=0.0.8,<0.3.0
- pyasn1>=0.1.9,<0.5.0
- PyDrive2>=1.3.1,<1.11.0
- PyYAML>=3.12
- rsa>=3.4.2,<4.8.0
- six>=1.10.0,<1.17.0
-gmail =
- google-auth-oauthlib>=0.4.3,<0.5.0
- google-api-python-client>=1.7.11,<2.32.0
-goodreads =
- goodreads>=0.3.2,<0.4.0
- python-Levenshtein>=0.12.0,<0.13.0
-ldap =
- python-ldap>=3.0.0,<3.4.0
- Flask-SimpleLDAP>=1.4.0,<1.5.0
-oauth =
- Flask-Dance>=2.0.0,<5.2.0
- SQLAlchemy-Utils>=0.33.5,<0.38.0
-metadata =
- rarfile>=2.7
- scholarly>=1.2.0,<1.5
-comics =
- natsort>=2.2.0,<8.1.0
- comicapi>= 2.2.0,<2.3.0
-kobo =
- jsonschema>=3.2.0,<4.3.0
-
-
+gdrive =
+ google-api-python-client>=1.7.11,<2.50.0
+ gevent>20.6.0,<22.0.0
+ greenlet>=0.4.17,<1.2.0
+ httplib2>=0.9.2,<0.21.0
+ oauth2client>=4.0.0,<4.1.4
+ uritemplate>=3.0.0,<4.2.0
+ pyasn1-modules>=0.0.8,<0.3.0
+ pyasn1>=0.1.9,<0.5.0
+ PyDrive2>=1.3.1,<1.11.0
+ PyYAML>=3.12
+ rsa>=3.4.2,<4.9.0
+gmail =
+ google-auth-oauthlib>=0.4.3,<0.6.0
+ google-api-python-client>=1.7.11,<2.50.0
+goodreads =
+ goodreads>=0.3.2,<0.4.0
+ python-Levenshtein>=0.12.0,<0.13.0
+ldap =
+ python-ldap>=3.0.0,<3.5.0
+ Flask-SimpleLDAP>=1.4.0,<1.5.0
+oauth =
+ Flask-Dance>=2.0.0,<5.2.0
+ SQLAlchemy-Utils>=0.33.5,<0.39.0
+metadata =
+ rarfile>=3.2
+ scholarly>=1.2.0,<1.7
+ markdown2>=2.0.0,<2.5.0
+ html2text>=2020.1.16,<2022.1.1
+ python-dateutil>=2.1,<2.9.0
+ beautifulsoup4>=4.0.1,<4.11.0
+ cchardet>=2.0.0,<2.2.0
+comics =
+ natsort>=2.2.0,<8.2.0
+ comicapi>=2.2.0,<2.3.0
+kobo =
+ jsonschema>=3.2.0,<4.5.0
diff --git a/test/Calibre-Web TestSummary.html b/test/Calibre-Web TestSummary.html
deleted file mode 100755
index 10e2e0ee..00000000
--- a/test/Calibre-Web TestSummary.html
+++ /dev/null
@@ -1,2947 +0,0 @@
-
-
-
- Calibre-Web Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Calibre-Web Tests
-
-
-
-
-
-
-
-
-
Start Time: 2020-08-30 15:47:09
-
-
-
-
-
-
-
Stop Time: 2020-08-30 17:06:27
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestAnonymous
- 13
- 13
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestAnonymous - test_check_locale_guest
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_about
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_change_visibility_category
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_change_visibility_format
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_change_visibility_hot
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_change_visibility_language
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_change_visibility_publisher
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_change_visibility_rated
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_change_visibility_rating
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_change_visibility_series
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_random_books_available
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_restricted_settings_visibility
-
- PASS
-
-
-
-
-
-
- TestAnonymous - test_guest_visibility_sidebar
-
- PASS
-
-
-
-
-
-
- TestCli
- 7
- 6
- 0
- 0
- 1
-
- Detail
-
-
-
-
-
-
-
- TestCli - test_already_started
-
- PASS
-
-
-
-
-
-
- TestCli - test_bind_to_single_interface
-
- PASS
-
-
-
-
-
-
- TestCli - test_cli_SSL_files
-
- PASS
-
-
-
-
-
-
- TestCli - test_cli_different_folder
-
- PASS
-
-
-
-
-
-
- TestCli - test_cli_different_settings_database
-
- PASS
-
-
-
-
-
-
- TestCli - test_cli_gdrive_location
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestCli - test_environ_port_setting
-
- PASS
-
-
-
-
-
-
- TestCoverEditBooks
- 1
- 0
- 1
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestCoverEditBooks - test_upload_jpg
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestDeleteDatabase
- 1
- 1
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestDeleteDatabase - test_delete_books_in_database
-
- PASS
-
-
-
-
-
-
- TestEbookConvert
- 11
- 11
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestEbookConvert - test_convert_deactivate
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_convert_email
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_convert_failed_and_email
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_convert_only
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_convert_parameter
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_convert_wrong_excecutable
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_email_failed
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_email_only
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_kindle_send_not_configured
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_ssl_smtp_setup_error
-
- PASS
-
-
-
-
-
-
- TestEbookConvert - test_starttls_smtp_setup_error
-
- PASS
-
-
-
-
-
-
- TestEditAdditionalBooks
- 5
- 5
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestEditAdditionalBooks - test_delete_book
-
- PASS
-
-
-
-
-
-
- TestEditAdditionalBooks - test_upload_metadata_cbt
-
- PASS
-
-
-
-
-
-
- TestEditAdditionalBooks - test_upload_metadate_cbr
-
- PASS
-
-
-
-
-
-
- TestEditAdditionalBooks - test_writeonly_database
-
- PASS
-
-
-
-
-
-
- TestEditAdditionalBooks - test_writeonly_path
-
- PASS
-
-
-
-
-
-
- TestEditBooks
- 33
- 30
- 1
- 0
- 2
-
- Detail
-
-
-
-
-
-
-
- TestEditBooks - test_download_book
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_author
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_category
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_comments
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_custom_bool
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_custom_categories
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_custom_float
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_custom_int
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_custom_rating
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_custom_single_select
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_custom_text
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_language
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_publisher
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_publishing_date
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestEditBooks - test_edit_rating
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_series
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_edit_title
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_rename_uppercase_lowercase
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestEditBooks - test_typeahead_author
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_typeahead_functions
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_typeahead_language
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_typeahead_publisher
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_typeahead_series
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_typeahead_tag
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_book_cbr
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_book_cbt
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_book_cbz
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_book_epub
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_book_fb2
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_book_lit
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_book_mobi
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_book_pdf
-
- PASS
-
-
-
-
-
-
- TestEditBooks - test_upload_cover_hdd
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestEditBooksGdrive
- 1
- 1
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestEditBooksGdrive - test_config_gdrive
-
- PASS
-
-
-
-
-
-
- TestSTARTTLS
- 3
- 3
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestSTARTTLS - test_STARTTLS
-
- PASS
-
-
-
-
-
-
- TestSTARTTLS - test_STARTTLS_SSL_setup_error
-
- PASS
-
-
-
-
-
-
- TestSTARTTLS - test_STARTTLS_resend_password
-
- PASS
-
-
-
-
-
-
- TestSSL
- 4
- 4
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestSSL - test_SSL_None_setup_error
-
- PASS
-
-
-
-
-
-
- TestSSL - test_SSL_STARTTLS_setup_error
-
- PASS
-
-
-
-
-
-
- TestSSL - test_SSL_logging_email
-
- PASS
-
-
-
-
-
-
- TestSSL - test_SSL_only
-
- PASS
-
-
-
-
-
-
- TestGoodreads
- 3
- 3
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestGoodreads - test_author_page
-
- PASS
-
-
-
-
-
-
- TestGoodreads - test_author_page_invalid
-
- PASS
-
-
-
-
-
-
- TestGoodreads - test_goodreads_about
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper
- 16
- 16
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestCalibreHelper - test_author_sort
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_author_sort_comma
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_author_sort_junior
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_author_sort_oneword
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_author_sort_roman
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_check_Limit_Length
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_check_char_replacement
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_check_chinese_Characters
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_check_deg_eur_replacement
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_check_doubleS
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_check_finish_Dot
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_check_high23
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_check_umlauts
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_random_password
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_split_authors
-
- PASS
-
-
-
-
-
-
- TestCalibreHelper - test_whitespaces
-
- PASS
-
-
-
-
-
-
- TestKoboSync
- 8
- 8
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestKoboSync - test_kobo_about
-
- PASS
-
-
-
-
-
-
- TestKoboSync - test_shelves_add_remove_books
-
- PASS
-
-
-
-
-
-
- TestKoboSync - test_sync_changed_book
-
- PASS
-
-
-
-
-
-
- TestKoboSync - test_sync_invalid
-
- PASS
-
-
-
-
-
-
- TestKoboSync - test_sync_reading_state
-
- PASS
-
-
-
-
-
-
- TestKoboSync - test_sync_shelf
-
- PASS
-
-
-
-
-
-
- TestKoboSync - test_sync_unchanged
-
- PASS
-
-
-
-
-
-
- TestKoboSync - test_sync_upload
-
- PASS
-
-
-
-
-
-
- TestLdapLogin
- 10
- 10
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestLdapLogin - test_LDAP_SSL
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_LDAP_STARTTLS
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_LDAP_fallback_Login
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_LDAP_import
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_LDAP_login
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_invalid_LDAP
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_ldap_about
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_ldap_authentication
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_ldap_kobo_sync
-
- PASS
-
-
-
-
-
-
- TestLdapLogin - test_ldap_opds_download_book
-
- PASS
-
-
-
-
-
-
- TestLogging
- 7
- 6
- 0
- 0
- 1
-
- Detail
-
-
-
-
-
-
-
- TestLogging - test_access_log_recover
-
- PASS
-
-
-
-
-
-
- TestLogging - test_debug_log
-
- PASS
-
-
-
-
-
-
- TestLogging - test_failed_login
-
- PASS
-
-
-
-
-
-
- TestLogging - test_failed_register
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestLogging - test_logfile_change
-
- PASS
-
-
-
-
-
-
- TestLogging - test_logfile_recover
-
- PASS
-
-
-
-
-
-
- TestLogging - test_logviewer
-
- PASS
-
-
-
-
-
-
- TestLogin
- 11
- 11
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestLogin - test_digest_login
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_capital_letters_user_unicode_password
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_delete_admin
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_empty_password
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_locale_select
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_protected
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_remember_me
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_rename_user
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_unicode_user_space_end_password
-
- PASS
-
-
-
-
-
-
- TestLogin - test_login_user_with_space_password_end_space
-
- PASS
-
-
-
-
-
-
- TestLogin - test_robots
-
- PASS
-
-
-
-
-
-
- TestOAuthLogin
- 2
- 2
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestOAuthLogin - test_oauth_about
-
- PASS
-
-
-
-
-
-
- TestOAuthLogin - test_visible_oauth
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed
- 20
- 20
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestOPDSFeed - test_opds
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_author
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_calibre_companion
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_cover
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_download_book
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_formats
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_guest_user
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_hot
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_language
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_non_admin
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_publisher
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_random
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_ratings
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_read_unread
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_search
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_series
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_shelf_access
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_tags
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_opds_top_rated
-
- PASS
-
-
-
-
-
-
- TestOPDSFeed - test_recently_added
-
- PASS
-
-
-
-
-
-
- TestRegister
- 7
- 7
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestRegister - test_forgot_password
-
- PASS
-
-
-
-
-
-
- TestRegister - test_limit_domain
-
- PASS
-
-
-
-
-
-
- TestRegister - test_register_no_server
-
- PASS
-
-
-
-
-
-
- TestRegister - test_registering_only_email
-
- PASS
-
-
-
-
-
-
- TestRegister - test_registering_user
-
- PASS
-
-
-
-
-
-
- TestRegister - test_registering_user_fail
-
- PASS
-
-
-
-
-
-
- TestRegister - test_user_change_password
-
- PASS
-
-
-
-
-
-
- TestShelf
- 10
- 9
- 0
- 0
- 1
-
- Detail
-
-
-
-
-
-
-
- TestShelf - test_add_shelf_from_search
-
- PASS
-
-
-
-
-
-
- TestShelf - test_arrange_shelf
-
- PASS
-
-
-
-
-
-
- TestShelf - test_delete_book_of_shelf
-
- PASS
-
-
-
-
-
-
- TestShelf - test_private_shelf
-
- PASS
-
-
-
-
-
-
- TestShelf - test_public_private_shelf
-
- PASS
-
-
-
-
-
-
- TestShelf - test_public_shelf
-
- PASS
-
-
-
-
-
-
- TestShelf - test_rename_shelf
-
- PASS
-
-
-
-
-
-
- TestShelf - test_shelf_action_non_shelf_edit_role
-
- PASS
-
-
-
-
-
-
- TestShelf - test_shelf_database_change
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestShelf - test_shelf_long_name
-
- PASS
-
-
-
-
-
-
- TestUpdater
- 8
- 7
- 0
- 0
- 1
-
- Detail
-
-
-
-
-
-
-
- TestUpdater - test_check_update_nightly_errors
-
- PASS
-
-
-
-
-
-
- TestUpdater - test_check_update_nightly_request_errors
-
- PASS
-
-
-
-
-
-
- TestUpdater - test_check_update_stable_errors
-
- PASS
-
-
-
-
-
-
- TestUpdater - test_check_update_stable_versions
-
- PASS
-
-
-
-
-
-
- TestUpdater - test_perform_update
-
- PASS
-
-
-
-
-
-
- TestUpdater - test_perform_update_stable_errors
-
- PASS
-
-
-
-
-
-
- TestUpdater - test_perform_update_timeout
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- TestUpdater - test_reconnect_database
-
- PASS
-
-
-
-
-
-
- TestUserTemplate
- 19
- 19
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestUserTemplate - test_allow_column_restriction
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_allow_tag_restriction
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_archived_format_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_author_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_best_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_category_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_deny_column_restriction
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_deny_tag_restriction
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_detail_random_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_format_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_hot_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_language_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_limit_book_languages
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_publisher_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_random_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_read_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_recent_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_series_user_template
-
- PASS
-
-
-
-
-
-
- TestUserTemplate - test_ui_language_settings
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys
- 30
- 30
- 0
- 0
- 0
-
- Detail
-
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_about
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_SMTP_Settings
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_add_user
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_password
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_archived
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_authors
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_category
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_file_formats
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_hot
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_language
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_publisher
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_random
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_rated
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_rating
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_read
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_admin_change_visibility_series
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_allow_columns
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_allow_tags
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_archive_books
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_authors_max_settings
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_checked_logged_in
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_hide_custom_column
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_link_column_to_read_status
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_random_books_available
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_restrict_columns
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_restrict_tags
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_search_functions
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_search_string
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_user_email_available
-
- PASS
-
-
-
-
-
-
- TestCalibreWebVisibilitys - test_user_visibility_sidebar
-
- PASS
-
-
-
-
-
- Total
- 230
- 222
- 2
- 0
- 6
-
-
-
-
-
-
-
-
-
-
-
- Program library
- Installed Version
- Test class
-
-
-
-
-
- Platform
- Linux 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64
- Basic
-
-
-
- Python
- 3.8.2
- Basic
-
-
-
- Babel
- 2.8.0
- Basic
-
-
-
- backports-abc
- 0.5
- Basic
-
-
-
- Flask
- 1.1.2
- Basic
-
-
-
- Flask-Babel
- 1.0.0
- Basic
-
-
-
- Flask-Login
- 0.5.0
- Basic
-
-
-
- Flask-Principal
- 0.4.0
- Basic
-
-
-
- iso-639
- 0.4.5
- Basic
-
-
-
- Jinja2
- 2.11.2
- Basic
-
-
-
- PyPDF2
- 1.26.0
- Basic
-
-
-
- pytz
- 2020.1
- Basic
-
-
-
- requests
- 2.23.0
- Basic
-
-
-
- singledispatch
- 3.4.0.3
- Basic
-
-
-
- six
- 1.15.0
- Basic
-
-
-
- SQLAlchemy
- 1.3.19
- Basic
-
-
-
- tornado
- 6.0.4
- Basic
-
-
-
- Unidecode
- 1.1.1
- Basic
-
-
-
- Wand
- 0.5.9
- Basic
-
-
-
- Werkzeug
- 1.0.1
- Basic
-
-
-
- Pillow
- 7.2.0
- TestCoverEditBooks
-
-
-
- comicapi
- 2.1.1
- TestEditAdditionalBooks
-
-
-
- lxml
- 4.5.2
- TestEditAdditionalBooks
-
-
-
- Pillow
- 7.2.0
- TestEditAdditionalBooks
-
-
-
- rarfile
- 4.0
- TestEditAdditionalBooks
-
-
-
- lxml
- 4.5.2
- TestEditBooks
-
-
-
- Pillow
- 7.2.0
- TestEditBooks
-
-
-
- google-api-python-client
- 1.11.0
- TestEditBooksGdrive
-
-
-
- httplib2
- 0.18.1
- TestEditBooksGdrive
-
-
-
- oauth2client
- 4.1.3
- TestEditBooksGdrive
-
-
-
- PyDrive
- 1.3.1
- TestEditBooksGdrive
-
-
-
- PyYAML
- 5.3.1
- TestEditBooksGdrive
-
-
-
- goodreads
- 0.3.2
- TestGoodreads
-
-
-
- jsonschema
- 3.2.0
- TestKoboSync
-
-
-
- Flask-SimpleLDAP
- 1.4.0
- TestLdapLogin
-
-
-
- jsonschema
- 3.2.0
- TestLdapLogin
-
-
-
- python-ldap
- 3.3.1
- TestLdapLogin
-
-
-
- Flask-Dance
- 3.0.0
- TestOAuthLogin
-
-
-
- SQLAlchemy-Utils
- 0.36.8
- TestOAuthLogin
-
-
-
-
-
-
-
-
-
-
-
-