Merge remote-tracking branch 'upstream/master'

pull/1653/head
Ghighi Eftimie 4 years ago
commit 5027304801

@ -73,7 +73,6 @@ ub.init_db(cli.settingspath)
# pylint: disable=no-member # pylint: disable=no-member
config = config_sql.load_configuration(ub.session) config = config_sql.load_configuration(ub.session)
searched_ids = {}
web_server = WebServer() web_server = WebServer()
babel = Babel() babel = Babel()
@ -83,6 +82,8 @@ log = logger.create()
from . import services from . import services
db.CalibreDB.setup_db(config, cli.settingspath)
calibre_db = db.CalibreDB() calibre_db = db.CalibreDB()
def create_app(): def create_app():
@ -101,8 +102,6 @@ def create_app():
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session)) app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
web_server.init_app(app, config) web_server.init_app(app, config)
calibre_db.setup_db(config, cli.settingspath)
calibre_db.start()
babel.init_app(app) babel.init_app(app)
_BABEL_TRANSLATIONS.update(str(item) for item in babel.list_translations()) _BABEL_TRANSLATIONS.update(str(item) for item in babel.list_translations())

@ -287,7 +287,7 @@ class _ConfigSQL(object):
db_file = os.path.join(self.config_calibre_dir, 'metadata.db') db_file = os.path.join(self.config_calibre_dir, 'metadata.db')
have_metadata_db = os.path.isfile(db_file) have_metadata_db = os.path.isfile(db_file)
self.db_configured = have_metadata_db self.db_configured = have_metadata_db
constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip() for x in self.config_upload_formats.split(',')] constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
logfile = logger.setup(self.config_logfile, self.config_log_level) logfile = logger.setup(self.config_logfile, self.config_log_level)
if logfile != self.config_logfile: if logfile != self.config_logfile:
log.warning("Log path %s not valid, falling back to default", self.config_logfile) log.warning("Log path %s not valid, falling back to default", self.config_logfile)

@ -81,10 +81,11 @@ SIDEBAR_PUBLISHER = 1 << 12
SIDEBAR_RATING = 1 << 13 SIDEBAR_RATING = 1 << 13
SIDEBAR_FORMAT = 1 << 14 SIDEBAR_FORMAT = 1 << 14
SIDEBAR_ARCHIVED = 1 << 15 SIDEBAR_ARCHIVED = 1 << 15
# SIDEBAR_LIST = 1 << 16 SIDEBAR_DOWNLOAD = 1 << 16
SIDEBAR_LIST = 1 << 17
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_ARCHIVED << 1) - 1 ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1
UPDATE_STABLE = 0 << 0 UPDATE_STABLE = 0 << 0
AUTO_UPDATE_STABLE = 1 << 0 AUTO_UPDATE_STABLE = 1 << 0

@ -24,14 +24,13 @@ import re
import ast import ast
import json import json
from datetime import datetime from datetime import datetime
import threading
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
from sqlalchemy.orm import relationship, sessionmaker, scoped_session from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.exc import OperationalError from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import and_, true, false, text, func, or_ from sqlalchemy.sql.expression import and_, true, false, text, func, or_
@ -43,13 +42,14 @@ from flask_babel import gettext as _
from . import logger, ub, isoLanguages from . import logger, ub, isoLanguages
from .pagination import Pagination from .pagination import Pagination
from weakref import WeakSet
try: try:
import unidecode import unidecode
use_unidecode = True use_unidecode = True
except ImportError: except ImportError:
use_unidecode = False use_unidecode = False
cc_exceptions = ['datetime', 'comments', 'composite', 'series'] cc_exceptions = ['datetime', 'comments', 'composite', 'series']
cc_classes = {} cc_classes = {}
@ -171,6 +171,9 @@ class Comments(Base):
self.text = text self.text = text
self.book = book self.book = book
def get(self):
return self.text
def __repr__(self): def __repr__(self):
return u"<Comments({0})>".format(self.text) return u"<Comments({0})>".format(self.text)
@ -184,6 +187,9 @@ class Tags(Base):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Tags('{0})>".format(self.name) return u"<Tags('{0})>".format(self.name)
@ -201,6 +207,9 @@ class Authors(Base):
self.sort = sort self.sort = sort
self.link = link self.link = link
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link) return u"<Authors('{0},{1}{2}')>".format(self.name, self.sort, self.link)
@ -216,6 +225,9 @@ class Series(Base):
self.name = name self.name = name
self.sort = sort self.sort = sort
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Series('{0},{1}')>".format(self.name, self.sort) return u"<Series('{0},{1}')>".format(self.name, self.sort)
@ -229,6 +241,9 @@ class Ratings(Base):
def __init__(self, rating): def __init__(self, rating):
self.rating = rating self.rating = rating
def get(self):
return self.rating
def __repr__(self): def __repr__(self):
return u"<Ratings('{0}')>".format(self.rating) return u"<Ratings('{0}')>".format(self.rating)
@ -242,6 +257,12 @@ class Languages(Base):
def __init__(self, lang_code): def __init__(self, lang_code):
self.lang_code = lang_code self.lang_code = lang_code
def get(self):
if self.language_name:
return self.language_name
else:
return self.lang_code
def __repr__(self): def __repr__(self):
return u"<Languages('{0}')>".format(self.lang_code) return u"<Languages('{0}')>".format(self.lang_code)
@ -257,6 +278,9 @@ class Publishers(Base):
self.name = name self.name = name
self.sort = sort self.sort = sort
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Publishers('{0},{1}')>".format(self.name, self.sort) return u"<Publishers('{0},{1}')>".format(self.name, self.sort)
@ -277,6 +301,10 @@ class Data(Base):
self.uncompressed_size = uncompressed_size self.uncompressed_size = uncompressed_size
self.name = name self.name = name
# ToDo: Check
def get(self):
return self.name
def __repr__(self): def __repr__(self):
return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name) return u"<Data('{0},{1}{2}{3}')>".format(self.book, self.format, self.uncompressed_size, self.name)
@ -284,14 +312,14 @@ class Data(Base):
class Books(Base): class Books(Base):
__tablename__ = 'books' __tablename__ = 'books'
DEFAULT_PUBDATE = "0101-01-01 00:00:00+00:00" DEFAULT_PUBDATE = datetime(101, 1, 1, 0, 0, 0, 0) # ("0101-01-01 00:00:00+00:00")
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(collation='NOCASE'), nullable=False, default='Unknown') title = Column(String(collation='NOCASE'), nullable=False, default='Unknown')
sort = Column(String(collation='NOCASE')) sort = Column(String(collation='NOCASE'))
author_sort = Column(String(collation='NOCASE')) author_sort = Column(String(collation='NOCASE'))
timestamp = Column(TIMESTAMP, default=datetime.utcnow) timestamp = Column(TIMESTAMP, default=datetime.utcnow)
pubdate = Column(String) # , default=datetime.utcnow) pubdate = Column(TIMESTAMP, default=DEFAULT_PUBDATE)
series_index = Column(String, nullable=False, default="1.0") series_index = Column(String, nullable=False, default="1.0")
last_modified = Column(TIMESTAMP, default=datetime.utcnow) last_modified = Column(TIMESTAMP, default=datetime.utcnow)
path = Column(String, default="", nullable=False) path = Column(String, default="", nullable=False)
@ -321,7 +349,8 @@ class Books(Base):
self.series_index = series_index self.series_index = series_index
self.last_modified = last_modified self.last_modified = last_modified
self.path = path self.path = path
self.has_cover = has_cover self.has_cover = (has_cover != None)
def __repr__(self): def __repr__(self):
return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort, return u"<Books('{0},{1}{2}{3}{4}{5}{6}{7}{8}')>".format(self.title, self.sort, self.author_sort,
@ -332,6 +361,7 @@ class Books(Base):
def atom_timestamp(self): def atom_timestamp(self):
return (self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or '') return (self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or '')
class Custom_Columns(Base): class Custom_Columns(Base):
__tablename__ = 'custom_columns' __tablename__ = 'custom_columns'
@ -352,46 +382,67 @@ class Custom_Columns(Base):
return display_dict return display_dict
class CalibreDB(threading.Thread): class AlchemyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj.__class__, DeclarativeMeta):
# an SQLAlchemy class
fields = {}
for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
if field == 'books':
continue
data = obj.__getattribute__(field)
try:
if isinstance(data, str):
data = data.replace("'", "\'")
elif isinstance(data, InstrumentedList):
el = list()
for ele in data:
if ele.get:
el.append(ele.get())
else:
el.append(json.dumps(ele, cls=AlchemyEncoder))
data = ",".join(el)
if data == '[]':
data = ""
else:
json.dumps(data)
fields[field] = data
except:
fields[field] = ""
# a json-encodable dict
return fields
return json.JSONEncoder.default(self, obj)
class CalibreDB():
_init = False
engine = None
config = None
session_factory = None
# This is a WeakSet so that references here don't keep other CalibreDB
# instances alive once they reach the end of their respective scopes
instances = WeakSet()
def __init__(self): def __init__(self):
threading.Thread.__init__(self) """ Initialize a new CalibreDB session
self.engine = None """
self.session = None self.session = None
self.queue = None if self._init:
self.log = None self.initSession()
self.config = None
def add_queue(self,queue):
self.queue = queue
self.log = logger.create()
def run(self):
while True:
i = self.queue.get()
if i == 'dummy':
self.queue.task_done()
break
if i['task'] == 'add_format':
cur_book = self.session.query(Books).filter(Books.id == i['id']).first()
cur_book.data.append(i['format'])
try:
# db.session.merge(cur_book)
self.session.commit()
except OperationalError as e:
self.session.rollback()
self.log.error("Database error: %s", e)
# self._handleError(_(u"Database error: %(error)s.", error=e))
# return
self.queue.task_done()
self.instances.add(self)
def stop(self):
self.queue.put('dummy')
def setup_db(self, config, app_db_path): def initSession(self):
self.config = config self.session = self.session_factory()
self.dispose() self.update_title_sort(self.config)
@classmethod
def setup_db(cls, config, app_db_path):
cls.config = config
cls.dispose()
if not config.config_calibre_dir: if not config.config_calibre_dir:
config.invalidate() config.invalidate()
@ -403,22 +454,21 @@ class CalibreDB(threading.Thread):
return False return False
try: try:
self.engine = create_engine('sqlite://', cls.engine = create_engine('sqlite://',
echo=False, echo=False,
isolation_level="SERIALIZABLE", isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False}, connect_args={'check_same_thread': False},
poolclass=StaticPool) poolclass=StaticPool)
self.engine.execute("attach database '{}' as calibre;".format(dbpath)) cls.engine.execute("attach database '{}' as calibre;".format(dbpath))
self.engine.execute("attach database '{}' as app_settings;".format(app_db_path)) cls.engine.execute("attach database '{}' as app_settings;".format(app_db_path))
conn = self.engine.connect() conn = cls.engine.connect()
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302 # conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
except Exception as e: except Exception as e:
config.invalidate(e) config.invalidate(e)
return False return False
config.db_configured = True config.db_configured = True
self.update_title_sort(config, conn.connection)
if not cc_classes: if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns") cc = conn.execute("SELECT id, datatype FROM custom_columns")
@ -488,10 +538,13 @@ class CalibreDB(threading.Thread):
secondary=books_custom_column_links[cc_id[0]], secondary=books_custom_column_links[cc_id[0]],
backref='books')) backref='books'))
Session = scoped_session(sessionmaker(autocommit=False, cls.session_factory = scoped_session(sessionmaker(autocommit=False,
autoflush=False, autoflush=True,
bind=self.engine)) bind=cls.engine))
self.session = Session() for inst in cls.instances:
inst.initSession()
cls._init = True
return True return True
def get_book(self, book_id): def get_book(self, book_id):
@ -545,10 +598,12 @@ class CalibreDB(threading.Thread):
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter) pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
# Fill indexpage with all requested data from database # Fill indexpage with all requested data from database
def fill_indexpage(self, page, database, db_filter, order, *join): def fill_indexpage(self, page, pagesize, database, db_filter, order, *join):
return self.fill_indexpage_with_archived_books(page, database, db_filter, order, False, *join) return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
def fill_indexpage_with_archived_books(self, page, database, db_filter, order, allow_show_archived, *join): def fill_indexpage_with_archived_books(self, page, pagesize, database, db_filter, order, allow_show_archived,
*join):
pagesize = pagesize or self.config.config_books_per_page
if current_user.show_detail_random(): if current_user.show_detail_random():
randm = self.session.query(Books) \ randm = self.session.query(Books) \
.filter(self.common_filters(allow_show_archived)) \ .filter(self.common_filters(allow_show_archived)) \
@ -556,14 +611,14 @@ class CalibreDB(threading.Thread):
.limit(self.config.config_random_books) .limit(self.config.config_random_books)
else: else:
randm = false() randm = false()
off = int(int(self.config.config_books_per_page) * (page - 1)) off = int(int(pagesize) * (page - 1))
query = self.session.query(database) \ query = self.session.query(database) \
.join(*join, isouter=True) \ .join(*join, isouter=True) \
.filter(db_filter) \ .filter(db_filter) \
.filter(self.common_filters(allow_show_archived)) .filter(self.common_filters(allow_show_archived))
pagination = Pagination(page, self.config.config_books_per_page, pagination = Pagination(page, pagesize,
len(query.all())) len(query.all()))
entries = query.order_by(*order).offset(off).limit(self.config.config_books_per_page).all() entries = query.order_by(*order).offset(off).limit(pagesize).all()
for book in entries: for book in entries:
book = self.order_authors(book) book = self.order_authors(book)
return entries, randm, pagination return entries, randm, pagination
@ -573,13 +628,16 @@ class CalibreDB(threading.Thread):
sort_authors = entry.author_sort.split('&') sort_authors = entry.author_sort.split('&')
authors_ordered = list() authors_ordered = list()
error = False error = False
ids = [a.id for a in entry.authors]
for auth in sort_authors: for auth in sort_authors:
results = self.session.query(Authors).filter(Authors.sort == auth.lstrip().strip()).all()
# ToDo: How to handle not found authorname # ToDo: How to handle not found authorname
result = self.session.query(Authors).filter(Authors.sort == auth.lstrip().strip()).first() if not len(results):
if not result:
error = True error = True
break break
authors_ordered.append(result) for r in results:
if r.id in ids:
authors_ordered.append(r)
if not error: if not error:
entry.authors = authors_ordered entry.authors = authors_ordered
return entry return entry
@ -603,20 +661,35 @@ class CalibreDB(threading.Thread):
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first() .filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
# read search results from calibre-database and return it (function is used for feed and simple search # read search results from calibre-database and return it (function is used for feed and simple search
def get_search_results(self, term): def get_search_results(self, term, offset=None, order=None, limit=None):
order = order or [Books.sort]
pagination = None
term.strip().lower() term.strip().lower()
self.session.connection().connection.connection.create_function("lower", 1, lcase) self.session.connection().connection.connection.create_function("lower", 1, lcase)
q = list() q = list()
authorterms = re.split("[, ]+", term) authorterms = re.split("[, ]+", term)
for authorterm in authorterms: for authorterm in authorterms:
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%"))) q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
return self.session.query(Books).filter(self.common_filters(True)).filter( result = self.session.query(Books).filter(self.common_filters(True)).filter(
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")), or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")), Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
Books.authors.any(and_(*q)), Books.authors.any(and_(*q)),
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")), Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
func.lower(Books.title).ilike("%" + term + "%") func.lower(Books.title).ilike("%" + term + "%")
)).order_by(Books.sort).all() )).order_by(*order).all()
result_count = len(result)
if offset != None and limit != 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
ub.store_ids(result)
return result[offset:limit_all], result_count, pagination,
# Creates for all stored languages a translated speaking name in the array for the UI # Creates for all stored languages a translated speaking name in the array for the UI
def speaking_language(self, languages=None): def speaking_language(self, languages=None):
@ -650,17 +723,23 @@ class CalibreDB(threading.Thread):
conn = conn or self.session.connection().connection.connection conn = conn or self.session.connection().connection.connection
conn.create_function("title_sort", 1, _title_sort) conn.create_function("title_sort", 1, _title_sort)
def dispose(self): @classmethod
def dispose(cls):
# global session # global session
old_session = self.session for inst in cls.instances:
self.session = None old_session = inst.session
inst.session = None
if old_session: if old_session:
try: old_session.close() try:
except: pass old_session.close()
except:
pass
if old_session.bind: if old_session.bind:
try: old_session.bind.dispose() try:
except Exception: pass old_session.bind.dispose()
except Exception:
pass
for attr in list(Books.__dict__.keys()): for attr in list(Books.__dict__.keys()):
if attr.startswith("custom_column_"): if attr.startswith("custom_column_"):
@ -677,10 +756,11 @@ class CalibreDB(threading.Thread):
Base.metadata.remove(table) Base.metadata.remove(table)
def reconnect_db(self, config, app_db_path): def reconnect_db(self, config, app_db_path):
self.session.close() self.dispose()
self.engine.dispose() self.engine.dispose()
self.setup_db(config, app_db_path) self.setup_db(config, app_db_path)
def lcase(s): def lcase(s):
try: try:
return unidecode.unidecode(s.lower()) return unidecode.unidecode(s.lower())

@ -27,14 +27,17 @@ import json
from shutil import copyfile from shutil import copyfile
from uuid import uuid4 from uuid import uuid4
from babel import Locale as LC
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper from . import constants, logger, isoLanguages, gdriveutils, uploader, helper
from . import config, get_locale, ub, worker, db from . import config, get_locale, ub, db
from . import calibre_db from . import calibre_db
from .services.worker import WorkerThread
from .tasks.upload import TaskUpload
from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required
@ -172,20 +175,41 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
changed = True changed = True
return changed, error return changed, error
@editbook.route("/ajax/delete/<int:book_id>")
@login_required
def delete_book_from_details(book_id):
return Response(delete_book(book_id,"", True), mimetype='application/json')
@editbook.route("/delete/<int:book_id>/", defaults={'book_format': ""}) @editbook.route("/delete/<int:book_id>", defaults={'book_format': ""})
@editbook.route("/delete/<int:book_id>/<string:book_format>/") @editbook.route("/delete/<int:book_id>/<string:book_format>")
@login_required @login_required
def delete_book(book_id, book_format): def delete_book_ajax(book_id, book_format):
return delete_book(book_id,book_format, False)
def delete_book(book_id, book_format, jsonResponse):
warning = {}
if current_user.role_delete_books(): if current_user.role_delete_books():
book = calibre_db.get_book(book_id) book = calibre_db.get_book(book_id)
if book: if book:
try: try:
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
if not result: if not result:
if jsonResponse:
return json.dumps({"location": url_for("editbook.edit_book"),
"type": "alert",
"format": "",
"error": error}),
else:
flash(error, category="error") flash(error, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id)) return redirect(url_for('editbook.edit_book', book_id=book_id))
if error: if error:
if jsonResponse:
warning = {"location": url_for("editbook.edit_book"),
"type": "warning",
"format": "",
"error": error}
else:
flash(error, category="warning") flash(error, category="warning")
if not book_format: if not book_format:
# delete book from Shelfs, Downloads, Read list # delete book from Shelfs, Downloads, Read list
@ -236,14 +260,26 @@ def delete_book(book_id, book_format):
filter(db.Data.format == book_format).delete() filter(db.Data.format == book_format).delete()
calibre_db.session.commit() calibre_db.session.commit()
except Exception as e: except Exception as e:
log.debug(e) log.exception(e)
calibre_db.session.rollback() calibre_db.session.rollback()
else: else:
# book not found # book not found
log.error('Book with id "%s" could not be deleted: not found', book_id) log.error('Book with id "%s" could not be deleted: not found', book_id)
if book_format: if book_format:
if jsonResponse:
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
"type": "success",
"format": book_format,
"message": _('Book Format Successfully Deleted')}])
else:
flash(_('Book Format Successfully Deleted'), category="success") flash(_('Book Format Successfully Deleted'), category="success")
return redirect(url_for('editbook.edit_book', book_id=book_id)) return redirect(url_for('editbook.edit_book', book_id=book_id))
else:
if jsonResponse:
return json.dumps([warning, {"location": url_for('web.index'),
"type": "success",
"format": book_format,
"message": _('Book Successfully Deleted')}])
else: else:
flash(_('Book Successfully Deleted'), category="success") flash(_('Book Successfully Deleted'), category="success")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
@ -518,8 +554,8 @@ def upload_single_file(request, book, book_id):
# Queue uploader info # Queue uploader info
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
worker.add_upload(current_user.nickname, WorkerThread.add(current_user.nickname, TaskUpload(
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>") "<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>"))
return uploader.process( return uploader.process(
saved_filename, *os.path.splitext(requested_file.filename), saved_filename, *os.path.splitext(requested_file.filename),
@ -569,6 +605,7 @@ def edit_book(book_id):
merge_metadata(to_save, meta) merge_metadata(to_save, meta)
# Update book # Update book
edited_books_id = None edited_books_id = None
#handle book title #handle book title
if book.title != to_save["book_title"].rstrip().strip(): if book.title != to_save["book_title"].rstrip().strip():
if to_save["book_title"] == '': if to_save["book_title"] == '':
@ -779,42 +816,17 @@ def upload():
if not db_author: if not db_author:
db_author = stored_author db_author = stored_author
sort_author = stored_author.sort sort_author = stored_author.sort
sort_authors_list.append(sort_author) # helper.get_sorted_author(sort_author)) sort_authors_list.append(sort_author)
sort_authors = ' & '.join(sort_authors_list) sort_authors = ' & '.join(sort_authors_list)
title_dir = helper.get_valid_filename(title) title_dir = helper.get_valid_filename(title)
author_dir = helper.get_valid_filename(db_author.name) author_dir = helper.get_valid_filename(db_author.name)
filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir)
saved_filename = os.path.join(filepath, title_dir + meta.extension.lower())
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
if not os.path.exists(filepath):
try:
os.makedirs(filepath)
except OSError:
log.error("Failed to create path %s (Permission denied)", filepath)
flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
try:
copyfile(meta.file_path, saved_filename)
os.unlink(meta.file_path)
except OSError as e:
log.error("Failed to move file %s: %s", saved_filename, e)
flash(_(u"Failed to Move File %(file)s: %(error)s", file=saved_filename, error=e), category="error")
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
if meta.cover is None:
has_cover = 0
copyfile(os.path.join(constants.STATIC_DIR, 'generic_cover.jpg'),
os.path.join(filepath, "cover.jpg"))
else:
has_cover = 1
# combine path and normalize path from windows systems # combine path and normalize path from windows systems
path = os.path.join(author_dir, title_dir).replace('\\', '/') path = os.path.join(author_dir, title_dir).replace('\\', '/')
# Calibre adds books with utc as timezone # Calibre adds books with utc as timezone
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1), db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
'1', datetime.utcnow(), path, has_cover, db_author, [], "") '1', datetime.utcnow(), path, meta.cover, db_author, [], "")
modif_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session, modif_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
'author') 'author')
@ -832,7 +844,7 @@ def upload():
modif_date |= edit_book_series(meta.series, db_book) modif_date |= edit_book_series(meta.series, db_book)
# Add file to book # Add file to book
file_size = os.path.getsize(saved_filename) file_size = os.path.getsize(meta.file_path)
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir) db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir)
db_book.data.append(db_data) db_book.data.append(db_data)
calibre_db.session.add(db_book) calibre_db.session.add(db_book)
@ -840,19 +852,27 @@ def upload():
# flush content, get db_book.id available # flush content, get db_book.id available
calibre_db.session.flush() calibre_db.session.flush()
# Comments needs book id therfore only possiblw after flush # Comments needs book id therfore only possible after flush
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book) modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
book_id = db_book.id book_id = db_book.id
title = db_book.title title = db_book.title
error = helper.update_dir_stucture(book_id, config.config_calibre_dir, input_authors[0]) error = helper.update_dir_structure_file(book_id,
config.config_calibre_dir,
input_authors[0],
meta.file_path,
title_dir + meta.extension)
# move cover to final directory, including book id # move cover to final directory, including book id
if has_cover: if meta.cover:
coverfile = meta.cover
else:
coverfile = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg')
new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg") new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg")
try: try:
copyfile(meta.cover, new_coverpath) copyfile(coverfile, new_coverpath)
if meta.cover:
os.unlink(meta.cover) os.unlink(meta.cover)
except OSError as e: except OSError as e:
log.error("Failed to move cover file %s: %s", new_coverpath, e) log.error("Failed to move cover file %s: %s", new_coverpath, e)
@ -862,17 +882,14 @@ def upload():
# save data to database, reread data # save data to database, reread data
calibre_db.session.commit() calibre_db.session.commit()
#calibre_db.setup_db(config, ub.app_DB_path)
# Reread book. It's important not to filter the result, as it could have language which hide it from
# current users view (tags are not stored/extracted from metadata and could also be limited)
#book = calibre_db.get_book(book_id)
if config.config_use_google_drive: if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal() gdriveutils.updateGdriveCalibreFromLocal()
if error: if error:
flash(error, category="error") flash(error, category="error")
uploadText=_(u"File %(file)s uploaded", file=title) uploadText=_(u"File %(file)s uploaded", file=title)
worker.add_upload(current_user.nickname, WorkerThread.add(current_user.nickname, TaskUpload(
"<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>") "<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>"))
if len(request.files.getlist("btn-upload")) < 2: if len(request.files.getlist("btn-upload")) < 2:
if current_user.role_edit() or current_user.role_admin(): if current_user.role_edit() or current_user.role_admin():
@ -910,3 +927,112 @@ def convert_bookformat(book_id):
else: else:
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id)) return redirect(url_for('editbook.edit_book', book_id=book_id))
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
@login_required_if_no_ano
def edit_list_book(param):
vals = request.form.to_dict()
# calibre_db.update_title_sort(config)
#calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
book = calibre_db.get_book(vals['pk'])
if param =='series_index':
edit_book_series_index(vals['value'], book)
elif param =='tags':
edit_book_tags(vals['value'], book)
elif param =='series':
edit_book_series(vals['value'], book)
elif param =='publishers':
vals['publisher'] = vals['value']
edit_book_publisher(vals, book)
elif param =='languages':
edit_book_languages(vals['value'], book)
elif param =='author_sort':
book.author_sort = vals['value']
elif param =='title':
book.title = vals['value']
helper.update_dir_stucture(book.id, config.config_calibre_dir)
elif param =='sort':
book.sort = vals['value']
# ToDo: edit books
elif param =='authors':
input_authors = vals['value'].split('&')
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
sort_authors_list = list()
for inp in input_authors:
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
if not stored_author:
stored_author = helper.get_sorted_author(inp)
else:
stored_author = stored_author.sort
sort_authors_list.append(helper.get_sorted_author(stored_author))
sort_authors = ' & '.join(sort_authors_list)
if book.author_sort != sort_authors:
book.author_sort = sort_authors
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
book.last_modified = datetime.utcnow()
calibre_db.session.commit()
return ""
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
@login_required
def get_sorted_entry(field, bookid):
if field == 'title' or field == 'authors':
book = calibre_db.get_filtered_book(bookid)
if book:
if field == 'title':
return json.dumps({'sort': book.sort})
elif field == 'authors':
return json.dumps({'author_sort': book.author_sort})
return ""
@editbook.route("/ajax/simulatemerge", methods=['POST'])
@login_required
def simulate_merge_list_book():
vals = request.get_json().get('Merge_books')
if vals:
to_book = calibre_db.get_book(vals[0]).title
vals.pop(0)
if to_book:
for book_id in vals:
from_book = []
from_book.append(calibre_db.get_book(book_id).title)
return json.dumps({'to': to_book, 'from': from_book})
return ""
@editbook.route("/ajax/mergebooks", methods=['POST'])
@login_required
def merge_list_book():
vals = request.get_json().get('Merge_books')
to_file = list()
if vals:
# load all formats from target book
to_book = calibre_db.get_book(vals[0])
vals.pop(0)
if to_book:
for file in to_book.data:
to_file.append(file.format)
to_name = helper.get_valid_filename(to_book.title) + ' - ' + \
helper.get_valid_filename(to_book.authors[0].name)
for book_id in vals:
from_book = calibre_db.get_book(book_id)
if from_book:
for element in from_book.data:
if element.format not in to_file:
# create new data entry with: book_id, book_format, uncompressed_size, name
filepath_new = os.path.normpath(os.path.join(config.config_calibre_dir,
to_book.path,
to_name + "." + element.format.lower()))
filepath_old = os.path.normpath(os.path.join(config.config_calibre_dir,
from_book.path,
element.name + "." + element.format.lower()))
copyfile(filepath_old, filepath_new)
to_book.data.append(db.Data(to_book.id,
element.format,
element.uncompressed_size,
to_name))
delete_book(from_book.id,"", True) # json_resp =
return json.dumps({'success': True})
return ""

@ -26,6 +26,7 @@ from .helper import split_authors
from .constants import BookMeta from .constants import BookMeta
def extractCover(zipFile, coverFile, coverpath, tmp_file_name): def extractCover(zipFile, coverFile, coverpath, tmp_file_name):
if coverFile is None: if coverFile is None:
return None return None

@ -32,13 +32,14 @@ from tempfile import gettempdir
import requests import requests
from babel.dates import format_datetime from babel.dates import format_datetime
from babel.units import format_unit from babel.units import format_unit
from flask import send_from_directory, make_response, redirect, abort from flask import send_from_directory, make_response, redirect, abort, url_for
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import current_user from flask_login import current_user
from sqlalchemy.sql.expression import true, false, and_, text, func from sqlalchemy.sql.expression import true, false, and_, text, func
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from . import calibre_db from . import calibre_db
from .tasks.convert import TaskConvert
try: try:
from urllib.parse import quote from urllib.parse import quote
@ -58,12 +59,12 @@ try:
except ImportError: except ImportError:
use_PIL = False use_PIL = False
from . import logger, config, get_locale, db, ub, worker from . import logger, config, get_locale, db, ub
from . import gdriveutils as gd from . import gdriveutils as gd
from .constants import STATIC_DIR as _STATIC_DIR from .constants import STATIC_DIR as _STATIC_DIR
from .subproc_wrapper import process_wait from .subproc_wrapper import process_wait
from .worker import STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS from .services.worker import WorkerThread, STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS
from .worker import TASK_EMAIL, TASK_CONVERT, TASK_UPLOAD, TASK_CONVERT_ANY from .tasks.mail import TaskEmail
log = logger.create() log = logger.create()
@ -73,46 +74,42 @@ log = logger.create()
def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None): def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, user_id, kindle_mail=None):
book = calibre_db.get_book(book_id) book = calibre_db.get_book(book_id)
data = calibre_db.get_book_format(book.id, old_book_format) data = calibre_db.get_book_format(book.id, old_book_format)
file_path = os.path.join(calibrepath, book.path, data.name)
if not data: if not data:
error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id) error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id)
log.error("convert_book_format: %s", error_message) log.error("convert_book_format: %s", error_message)
return error_message return error_message
if config.config_use_google_drive: if config.config_use_google_drive:
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()) if not gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()):
if df:
datafile = os.path.join(calibrepath, book.path, data.name + u"." + old_book_format.lower())
if not os.path.exists(os.path.join(calibrepath, book.path)):
os.makedirs(os.path.join(calibrepath, book.path))
df.GetContentFile(datafile)
else:
error_message = _(u"%(format)s not found on Google Drive: %(fn)s", error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
format=old_book_format, fn=data.name + "." + old_book_format.lower()) format=old_book_format, fn=data.name + "." + old_book_format.lower())
return error_message return error_message
file_path = os.path.join(calibrepath, book.path, data.name) else:
if os.path.exists(file_path + "." + old_book_format.lower()): if not os.path.exists(file_path + "." + old_book_format.lower()):
error_message = _(u"%(format)s not found: %(fn)s",
format=old_book_format, fn=data.name + "." + old_book_format.lower())
return error_message
# read settings and append converter task to queue # read settings and append converter task to queue
if kindle_mail: if kindle_mail:
settings = config.get_mail_settings() settings = config.get_mail_settings()
settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.') settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
# text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title)
else: else:
settings = dict() settings = dict()
txt = (u"%s -> %s: %s" % (old_book_format, new_book_format, book.title)) txt = (u"%s -> %s: %s" % (
old_book_format,
new_book_format,
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + book.title + "</a>"))
settings['old_book_format'] = old_book_format settings['old_book_format'] = old_book_format
settings['new_book_format'] = new_book_format settings['new_book_format'] = new_book_format
worker.add_convert(file_path, book.id, user_id, txt, settings, kindle_mail) WorkerThread.add(user_id, TaskConvert(file_path, book.id, txt, settings, kindle_mail, user_id))
return None return None
else:
error_message = _(u"%(format)s not found: %(fn)s",
format=old_book_format, fn=data.name + "." + old_book_format.lower())
return error_message
def send_test_mail(kindle_mail, user_name): def send_test_mail(kindle_mail, user_name):
worker.add_email(_(u'Calibre-Web test e-mail'), None, None, WorkerThread.add(user_name, TaskEmail(_(u'Calibre-Web test e-mail'), None, None,
config.get_mail_settings(), kindle_mail, user_name, config.get_mail_settings(), kindle_mail, _(u"Test e-mail"),
_(u"Test e-mail"), _(u'This e-mail has been sent via Calibre-Web.')) _(u'This e-mail has been sent via Calibre-Web.')))
return return
@ -127,9 +124,16 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
text += "Don't forget to change your password after first login.\r\n" text += "Don't forget to change your password after first login.\r\n"
text += "Sincerely\r\n\r\n" text += "Sincerely\r\n\r\n"
text += "Your Calibre-Web team" text += "Your Calibre-Web team"
worker.add_email(_(u'Get Started with Calibre-Web'), None, None, WorkerThread.add(None, TaskEmail(
config.get_mail_settings(), e_mail, None, subject=_(u'Get Started with Calibre-Web'),
_(u"Registration e-mail for user: %(name)s", name=user_name), text) filepath=None,
attachment=None,
settings=config.get_mail_settings(),
recipient=e_mail,
taskMessage=_(u"Registration e-mail for user: %(name)s", name=user_name),
text=text
))
return return
@ -221,9 +225,9 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
for entry in iter(book.data): for entry in iter(book.data):
if entry.format.upper() == book_format.upper(): if entry.format.upper() == book_format.upper():
converted_file_name = entry.name + '.' + book_format.lower() converted_file_name = entry.name + '.' + book_format.lower()
worker.add_email(_(u"Send to Kindle"), book.path, converted_file_name, WorkerThread.add(user_id, TaskEmail(_(u"Send to Kindle"), book.path, converted_file_name,
config.get_mail_settings(), kindle_mail, user_id, config.get_mail_settings(), kindle_mail,
_(u"E-mail: %(book)s", book=book.title), _(u'This e-mail has been sent via Calibre-Web.')) _(u"E-mail: %(book)s", book=book.title), _(u'This e-mail has been sent via Calibre-Web.')))
return return
return _(u"The requested file could not be read. Maybe wrong permissions?") return _(u"The requested file could not be read. Maybe wrong permissions?")
@ -343,66 +347,69 @@ def delete_book_file(book, calibrepath, book_format=None):
path=book.path) path=book.path)
def update_dir_structure_file(book_id, calibrepath, first_author): # Moves files in file storage during author/title rename, or from temp dir to file storage
def update_dir_structure_file(book_id, calibrepath, first_author, orignal_filepath, db_filename):
# get book database entry from id, if original path overwrite source with original_filepath
localbook = calibre_db.get_book(book_id) localbook = calibre_db.get_book(book_id)
if orignal_filepath:
path = orignal_filepath
else:
path = os.path.join(calibrepath, localbook.path) path = os.path.join(calibrepath, localbook.path)
# Create (current) authordir and titledir from database
authordir = localbook.path.split('/')[0] authordir = localbook.path.split('/')[0]
titledir = localbook.path.split('/')[1]
# Create new_authordir from parameter or from database
# Create new titledir from database and add id
if first_author: if first_author:
new_authordir = get_valid_filename(first_author) new_authordir = get_valid_filename(first_author)
else: else:
new_authordir = get_valid_filename(localbook.authors[0].name) new_authordir = get_valid_filename(localbook.authors[0].name)
titledir = localbook.path.split('/')[1]
new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")" new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")"
if titledir != new_titledir: if titledir != new_titledir or authordir != new_authordir or orignal_filepath:
new_title_path = os.path.join(os.path.dirname(path), new_titledir) new_path = os.path.join(calibrepath, new_authordir, new_titledir)
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
try: try:
if not os.path.exists(new_title_path): if orignal_filepath:
os.renames(os.path.normcase(path), os.path.normcase(new_title_path)) os.renames(os.path.normcase(path),
else: os.path.normcase(os.path.join(new_path, db_filename)))
log.info("Copying title: %s into existing: %s", path, new_title_path) log.debug("Moving title: %s to %s/%s", path, new_path, new_name)
# Check new path is not valid path
elif not os.path.exists(new_path):
# move original path to new path
os.renames(os.path.normcase(path), os.path.normcase(new_path))
log.debug("Moving title: %s to %s", path, new_path)
else: # path is valid copy only files to new location (merge)
log.info("Moving title: %s into existing: %s", path, new_path)
# Take all files and subfolder from old path (strange command)
for dir_name, __, file_list in os.walk(path): for dir_name, __, file_list in os.walk(path):
for file in file_list: for file in file_list:
os.renames(os.path.normcase(os.path.join(dir_name, file)), os.renames(os.path.normcase(os.path.join(dir_name, file)),
os.path.normcase(os.path.join(new_title_path + dir_name[len(path):], file))) os.path.normcase(os.path.join(new_path + dir_name[len(path):], file)))
path = new_title_path # change location in database to new author/title path
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir localbook.path = os.path.join(new_authordir, new_titledir)
except OSError as ex: except OSError as ex:
log.error("Rename title from: %s to %s: %s", path, new_title_path, ex) log.error("Rename title from: %s to %s: %s", path, new_path, ex)
log.debug(ex, exc_info=True) log.debug(ex, exc_info=True)
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_title_path, error=str(ex)) src=path, dest=new_path, error=str(ex))
if authordir != new_authordir:
new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path))
try:
os.renames(os.path.normcase(path), os.path.normcase(new_author_path))
localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
except OSError as ex:
log.error("Rename author from: %s to %s: %s", path, new_author_path, ex)
log.debug(ex, exc_info=True)
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_author_path, error=str(ex))
# Rename all files from old names to new names # Rename all files from old names to new names
if authordir != new_authordir or titledir != new_titledir:
new_name = ""
try: try:
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
for file_format in localbook.data: for file_format in localbook.data:
os.renames(os.path.normcase( os.renames(os.path.normcase(
os.path.join(path_name, file_format.name + '.' + file_format.format.lower())), os.path.join(new_path, file_format.name + '.' + file_format.format.lower())),
os.path.normcase(os.path.join(path_name, new_name + '.' + file_format.format.lower()))) os.path.normcase(os.path.join(new_path, new_name + '.' + file_format.format.lower())))
file_format.name = new_name file_format.name = new_name
except OSError as ex: except OSError as ex:
log.error("Rename file in path %s to %s: %s", path, new_name, ex) log.error("Rename file in path %s to %s: %s", new_path, new_name, ex)
log.debug(ex, exc_info=True) log.debug(ex, exc_info=True)
return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s", return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_name, error=str(ex)) src=new_path, dest=new_name, error=str(ex))
return False return False
def update_dir_structure_gdrive(book_id, first_author): def update_dir_structure_gdrive(book_id, first_author):
error = False error = False
book = calibre_db.get_book(book_id) book = calibre_db.get_book(book_id)
@ -505,11 +512,11 @@ def uniq(inpt):
# ################################# External interface ################################# # ################################# External interface #################################
def update_dir_stucture(book_id, calibrepath, first_author=None): def update_dir_stucture(book_id, calibrepath, first_author=None, orignal_filepath=None, db_filename=None):
if config.config_use_google_drive: if config.config_use_google_drive:
return update_dir_structure_gdrive(book_id, first_author) return update_dir_structure_gdrive(book_id, first_author)
else: else:
return update_dir_structure_file(book_id, calibrepath, first_author) return update_dir_structure_file(book_id, calibrepath, first_author, orignal_filepath, db_filename)
def delete_book(book, calibrepath, book_format): def delete_book(book, calibrepath, book_format):
@ -722,47 +729,30 @@ def format_runtime(runtime):
# helper function to apply localize status information in tasklist entries # helper function to apply localize status information in tasklist entries
def render_task_status(tasklist): def render_task_status(tasklist):
renderedtasklist = list() renderedtasklist = list()
for task in tasklist: for num, user, added, task in tasklist:
if task['user'] == current_user.nickname or current_user.role_admin(): if user == current_user.nickname or current_user.role_admin():
if task['formStarttime']: ret = {}
task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=get_locale()) if task.start_time:
# task2['formStarttime'] = "" ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())
else: ret['runtime'] = format_runtime(task.runtime)
if 'starttime' not in task:
task['starttime'] = ""
if 'formRuntime' not in task:
task['runtime'] = ""
else:
task['runtime'] = format_runtime(task['formRuntime'])
# localize the task status # localize the task status
if isinstance(task['stat'], int): if isinstance(task.stat, int):
if task['stat'] == STAT_WAITING: if task.stat == STAT_WAITING:
task['status'] = _(u'Waiting') ret['status'] = _(u'Waiting')
elif task['stat'] == STAT_FAIL: elif task.stat == STAT_FAIL:
task['status'] = _(u'Failed') ret['status'] = _(u'Failed')
elif task['stat'] == STAT_STARTED: elif task.stat == STAT_STARTED:
task['status'] = _(u'Started') ret['status'] = _(u'Started')
elif task['stat'] == STAT_FINISH_SUCCESS: elif task.stat == STAT_FINISH_SUCCESS:
task['status'] = _(u'Finished') ret['status'] = _(u'Finished')
else: else:
task['status'] = _(u'Unknown Status') ret['status'] = _(u'Unknown Status')
# localize the task type ret['taskMessage'] = "{}: {}".format(_(task.name), task.message)
if isinstance(task['taskType'], int): ret['progress'] = "{} %".format(int(task.progress * 100))
if task['taskType'] == TASK_EMAIL: ret['user'] = user
task['taskMessage'] = _(u'E-mail: ') + task['taskMess'] renderedtasklist.append(ret)
elif task['taskType'] == TASK_CONVERT:
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
elif task['taskType'] == TASK_UPLOAD:
task['taskMessage'] = _(u'Upload: ') + task['taskMess']
elif task['taskType'] == TASK_CONVERT_ANY:
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
else:
task['taskMessage'] = _(u'Unknown Task: ') + task['taskMess']
renderedtasklist.append(task)
return renderedtasklist return renderedtasklist

@ -44,6 +44,8 @@ log = logger.create()
def url_for_other_page(page): def url_for_other_page(page):
args = request.view_args.copy() args = request.view_args.copy()
args['page'] = page args['page'] = page
for get, val in request.args.items():
args[get] = val
return url_for(request.endpoint, **args) return url_for(request.endpoint, **args)
@ -76,22 +78,18 @@ def mimetype_filter(val):
@jinjia.app_template_filter('formatdate') @jinjia.app_template_filter('formatdate')
def formatdate_filter(val): def formatdate_filter(val):
try: try:
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) return format_date(val, format='medium', locale=get_locale())
formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
return format_date(formatdate, format='medium', locale=get_locale())
except AttributeError as e: except AttributeError as e:
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
current_user.locale, current_user.locale,
current_user.nickname current_user.nickname
) )
return formatdate return val
@jinjia.app_template_filter('formatdateinput') @jinjia.app_template_filter('formatdateinput')
def format_date_input(val): def format_date_input(val):
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) input_date = val.isoformat().split('T', 1)[0] # Hack to support dates <1900
date_obj = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
input_date = date_obj.isoformat().split('T', 1)[0] # Hack to support dates <1900
return '' if input_date == "0101-01-01" else input_date return '' if input_date == "0101-01-01" else input_date

@ -100,7 +100,7 @@ def feed_normal_search():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_new(): def feed_new():
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, True, [db.Books.timestamp.desc()]) db.Books, True, [db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination) return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -118,7 +118,7 @@ def feed_discover():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_best_rated(): def feed_best_rated():
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books.ratings.any(db.Ratings.rating > 9), db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination) return render_xml_template('feed.xml', entries=entries, pagination=pagination)
@ -164,7 +164,7 @@ def feed_authorindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_author(book_id): def feed_author(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.authors.any(db.Authors.id == book_id), db.Books.authors.any(db.Authors.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -190,7 +190,7 @@ def feed_publisherindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_publisher(book_id): def feed_publisher(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.publishers.any(db.Publishers.id == book_id), db.Books.publishers.any(db.Publishers.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -218,7 +218,7 @@ def feed_categoryindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_category(book_id): def feed_category(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.tags.any(db.Tags.id == book_id), db.Books.tags.any(db.Tags.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -245,7 +245,7 @@ def feed_seriesindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_series(book_id): def feed_series(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.series.any(db.Series.id == book_id), db.Books.series.any(db.Series.id == book_id),
[db.Books.series_index]) [db.Books.series_index])
@ -276,7 +276,7 @@ def feed_ratingindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_ratings(book_id): def feed_ratings(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.ratings.any(db.Ratings.id == book_id), db.Books.ratings.any(db.Ratings.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -304,7 +304,7 @@ def feed_formatindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_format(book_id): def feed_format(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.data.any(db.Data.format == book_id.upper()), db.Books.data.any(db.Data.format == book_id.upper()),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -338,7 +338,7 @@ def feed_languagesindex():
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_languages(book_id): def feed_languages(book_id):
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books,
db.Books.languages.any(db.Languages.id == book_id), db.Books.languages.any(db.Languages.id == book_id),
[db.Books.timestamp.desc()]) [db.Books.timestamp.desc()])
@ -408,7 +408,7 @@ def get_metadata_calibre_companion(uuid, library):
def feed_search(term): def feed_search(term):
if term: if term:
entries = calibre_db.get_search_results(term) entries, __ = calibre_db.get_search_results(term)
entriescount = len(entries) if len(entries) > 0 else 1 entriescount = len(entries) if len(entries) > 0 else 1
pagination = Pagination(1, entriescount, entriescount) pagination = Pagination(1, entriescount, entriescount)
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)

@ -212,9 +212,6 @@ class WebServer(object):
def stop(self, restart=False): def stop(self, restart=False):
from . import updater_thread from . import updater_thread
updater_thread.stop() updater_thread.stop()
from . import calibre_db
calibre_db.stop()
log.info("webserver stop (restart=%s)", restart) log.info("webserver stop (restart=%s)", restart)
self.restart = restart self.restart = restart

@ -0,0 +1,220 @@
from __future__ import division, print_function, unicode_literals
import threading
import abc
import uuid
import time
try:
import queue
except ImportError:
import Queue as queue
from datetime import datetime
from collections import namedtuple
from cps import logger
log = logger.create()
# task 'status' consts
STAT_WAITING = 0
STAT_FAIL = 1
STAT_STARTED = 2
STAT_FINISH_SUCCESS = 3
# Only retain this many tasks in dequeued list
TASK_CLEANUP_TRIGGER = 20
QueuedTask = namedtuple('QueuedTask', 'num, user, added, task')
def _get_main_thread():
for t in threading.enumerate():
if t.__class__.__name__ == '_MainThread':
return t
raise Exception("main thread not found?!")
class ImprovedQueue(queue.Queue):
def to_list(self):
"""
Returns a copy of all items in the queue without removing them.
"""
with self.mutex:
return list(self.queue)
#Class for all worker tasks in the background
class WorkerThread(threading.Thread):
_instance = None
@classmethod
def getInstance(cls):
if cls._instance is None:
cls._instance = WorkerThread()
return cls._instance
def __init__(self):
threading.Thread.__init__(self)
self.dequeued = list()
self.doLock = threading.Lock()
self.queue = ImprovedQueue()
self.num = 0
self.start()
@classmethod
def add(cls, user, task):
ins = cls.getInstance()
ins.num += 1
ins.queue.put(QueuedTask(
num=ins.num,
user=user,
added=datetime.now(),
task=task,
))
@property
def tasks(self):
with self.doLock:
tasks = self.queue.to_list() + self.dequeued
return sorted(tasks, key=lambda x: x.num)
def cleanup_tasks(self):
with self.doLock:
dead = []
alive = []
for x in self.dequeued:
(dead if x.task.dead else alive).append(x)
# if the ones that we need to keep are within the trigger, do nothing else
delta = len(self.dequeued) - len(dead)
if delta > TASK_CLEANUP_TRIGGER:
ret = alive
else:
# otherwise, lop off the oldest dead tasks until we hit the target trigger
ret = sorted(dead, key=lambda x: x.task.end_time)[-TASK_CLEANUP_TRIGGER:] + alive
self.dequeued = sorted(ret, key=lambda x: x.num)
# Main thread loop starting the different tasks
def run(self):
main_thread = _get_main_thread()
while main_thread.is_alive():
try:
# this blocks until something is available. This can cause issues when the main thread dies - this
# thread will remain alive. We implement a timeout to unblock every second which allows us to check if
# the main thread is still alive.
# We don't use a daemon here because we don't want the tasks to just be abruptly halted, leading to
# possible file / database corruption
item = self.queue.get(timeout=1)
except queue.Empty as ex:
time.sleep(1)
continue
with self.doLock:
# add to list so that in-progress tasks show up
self.dequeued.append(item)
# once we hit our trigger, start cleaning up dead tasks
if len(self.dequeued) > TASK_CLEANUP_TRIGGER:
self.cleanup_tasks()
# sometimes tasks (like Upload) don't actually have work to do and are created as already finished
if item.task.stat is STAT_WAITING:
# CalibreTask.start() should wrap all exceptions in it's own error handling
item.task.start(self)
self.queue.task_done()
class CalibreTask:
__metaclass__ = abc.ABCMeta
def __init__(self, message):
self._progress = 0
self.stat = STAT_WAITING
self.error = None
self.start_time = None
self.end_time = None
self.message = message
self.id = uuid.uuid4()
@abc.abstractmethod
def run(self, worker_thread):
"""Provides the caller some human-readable name for this class"""
raise NotImplementedError
@abc.abstractmethod
def name(self):
"""Provides the caller some human-readable name for this class"""
raise NotImplementedError
def start(self, *args):
self.start_time = datetime.now()
self.stat = STAT_STARTED
# catch any unhandled exceptions in a task and automatically fail it
try:
self.run(*args)
except Exception as e:
self._handleError(str(e))
log.exception(e)
self.end_time = datetime.now()
@property
def stat(self):
return self._stat
@stat.setter
def stat(self, x):
self._stat = x
@property
def progress(self):
return self._progress
@progress.setter
def progress(self, x):
if not 0 <= x <= 1:
raise ValueError("Task progress should within [0, 1] range")
self._progress = x
@property
def error(self):
return self._error
@error.setter
def error(self, x):
self._error = x
@property
def runtime(self):
return (self.end_time or datetime.now()) - self.start_time
@property
def dead(self):
"""Determines whether or not this task can be garbage collected
We have a separate dictating this because there may be certain tasks that want to override this
"""
# By default, we're good to clean a task if it's "Done"
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
@progress.setter
def progress(self, x):
# todo: throw error if outside of [0,1]
self._progress = x
def _handleError(self, error_message):
log.exception(error_message)
self.stat = STAT_FAIL
self.progress = 1
self.error = error_message
def _handleSuccess(self):
self.stat = STAT_FINISH_SUCCESS
self.progress = 1

@ -29,7 +29,7 @@ from flask_login import login_required, current_user
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from sqlalchemy.exc import OperationalError, InvalidRequestError from sqlalchemy.exc import OperationalError, InvalidRequestError
from . import logger, ub, searched_ids, calibre_db from . import logger, ub, calibre_db
from .web import login_required_if_no_ano, render_title_template from .web import login_required_if_no_ano, render_title_template
@ -124,18 +124,18 @@ def search_to_shelf(shelf_id):
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error") flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if current_user.id in searched_ids and searched_ids[current_user.id]: if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
books_for_shelf = list() books_for_shelf = list()
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all() books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
if books_in_shelf: if books_in_shelf:
book_ids = list() book_ids = list()
for book_id in books_in_shelf: for book_id in books_in_shelf:
book_ids.append(book_id.book_id) book_ids.append(book_id.book_id)
for searchid in searched_ids[current_user.id]: for searchid in ub.searched_ids[current_user.id]:
if searchid not in book_ids: if searchid not in book_ids:
books_for_shelf.append(searchid) books_for_shelf.append(searchid)
else: else:
books_for_shelf = searched_ids[current_user.id] books_for_shelf = ub.searched_ids[current_user.id]
if not books_for_shelf: if not books_for_shelf:
log.error("Books are already part of %s", shelf) log.error("Books are already part of %s", shelf)

@ -585,7 +585,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > .dow
border-left: 2px solid rgba(0, 0, 0, .15) border-left: 2px solid rgba(0, 0, 0, .15)
} }
div[aria-label="Edit/Delete book"] > .btn-warning { div[aria-label="Edit/Delete book"] > .btn {
width: 50px; width: 50px;
height: 60px; height: 60px;
margin: 0; margin: 0;
@ -600,7 +600,7 @@ div[aria-label="Edit/Delete book"] > .btn-warning {
color: transparent color: transparent
} }
div[aria-label="Edit/Delete book"] > .btn-warning > span { div[aria-label="Edit/Delete book"] > .btn > span {
visibility: visible; visibility: visible;
position: relative; position: relative;
display: inline-block; display: inline-block;
@ -616,7 +616,7 @@ div[aria-label="Edit/Delete book"] > .btn-warning > span {
margin: auto margin: auto
} }
div[aria-label="Edit/Delete book"] > .btn-warning > span:before { div[aria-label="Edit/Delete book"] > .btn > span:before {
content: "\EA5d"; content: "\EA5d";
font-family: plex-icons; font-family: plex-icons;
font-size: 20px; font-size: 20px;
@ -625,7 +625,7 @@ div[aria-label="Edit/Delete book"] > .btn-warning > span:before {
height: 60px height: 60px
} }
div[aria-label="Edit/Delete book"] > .btn-warning > span:hover { div[aria-label="Edit/Delete book"] > .btn > span:hover {
color: #fff color: #fff
} }
@ -1939,7 +1939,9 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > form > .btn.bt
z-index: 99999 z-index: 99999
} }
.pagination:after, body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.next, body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous { body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-next > a,
body > div.container-fluid > div > div.col-sm-10 > div.pagination .page-previous > a
{
top: 0; top: 0;
font-family: plex-icons-new; font-family: plex-icons-new;
font-weight: 100; font-weight: 100;
@ -1947,7 +1949,8 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > form > .btn.bt
line-height: 60px; line-height: 60px;
height: 60px; height: 60px;
font-style: normal; font-style: normal;
-moz-osx-font-smoothing: grayscale -moz-osx-font-smoothing: grayscale;
overflow: hidden;
} }
.pagination > a { .pagination > a {
@ -1967,68 +1970,46 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > form > .btn.bt
color: #fff !important color: #fff !important
} }
body > div.container-fluid > div > div.col-sm-10 > div.pagination > a, body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous + a, body > div.container-fluid > div > div.col-sm-10 > div.pagination > a[href*=page] { body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-item:not(.page-next):not(.page-previous)
{
display: none display: none
} }
body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.next, body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous { body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-next > a,
body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-previous > a {
color: transparent; color: transparent;
background-color:transparent;
margin-left: 0; margin-left: 0;
width: 65px; width: 65px;
padding: 0; padding: 0;
font-size: 15px; font-size: 15px;
position: absolute; display: block !important;
display: block !important border: none;
}
body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.next {
right: 0
}
body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous {
right: 65px
} }
body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.next:before { body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-next > a:before,
content: "\EA32"; body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-previous > a:before {
visibility: visible; visibility: visible;
color: hsla(0, 0%, 100%, .35); color: hsla(0, 0%, 100%, .35);
height: 60px; height: 60px;
line-height: 60px; line-height: 60px;
border-left: 2px solid transparent; border-left: 2px solid transparent;
font-size: 20px; font-size: 20px;
padding: 20px 0 20px 20px; padding: 20px 25px;
margin-right: -27px margin-right: -27px;
} }
body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous:before { body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-next > a:before {
content: "\EA33"; content: "\EA32";
visibility: visible;
color: hsla(0, 0%, 100%, .65);
height: 60px;
line-height: 60px;
font-size: 20px;
padding: 20px 25px
}
body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.next:hover:before, body > div.container-fluid > div > div.col-sm-10 > div.pagination > a.previous:hover:before {
color: #fff
} }
.pagination > strong { body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-previous > a:before {
display: none content: "\EA33";
} }
.pagination:after { body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-next > a:hover:before,
content: "\EA32"; body > div.container-fluid > div > div.col-sm-10 > div.pagination > .page-previous > a:hover:before {
position: relative; color: #fff
right: 0;
display: inline-block;
color: hsla(0, 0%, 100%, .55);
font-size: 20px;
padding: 0 23px;
margin-left: 20px;
z-index: -1
} }
.pagination > .ellipsis, .pagination > a:nth-last-of-type(2) { .pagination > .ellipsis, .pagination > a:nth-last-of-type(2) {

@ -5,7 +5,7 @@ body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
.cover .badge{ .cover .badge{
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; left: 0;
background-color: #cc7b19; background-color: #cc7b19;
border-radius: 0; border-radius: 0;
padding: 0 8px; padding: 0 8px;

@ -51,7 +51,22 @@ body h2 {
color:#444; color:#444;
} }
a { color: #45b29d; } a, .danger,.book-remove, .editable-empty, .editable-empty:hover { color: #45b29d; }
.book-remove:hover { color: #23527c; }
.btn-default a { color: #444; }
.btn-default a:hover {
color: #45b29d;
text-decoration: None;
}
.btn-default:hover {
color: #45b29d;
}
.editable-click, a.editable-click, a.editable-click:hover { border-bottom: None; }
.navigation .nav-head { .navigation .nav-head {
text-transform: uppercase; text-transform: uppercase;
@ -63,6 +78,7 @@ a { color: #45b29d; }
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
padding-top: 20px; padding-top: 20px;
} }
.navigation li a { .navigation li a {
color: #444; color: #444;
text-decoration: none; text-decoration: none;

@ -411,6 +411,19 @@ bitjs.archive = bitjs.archive || {};
return "unrar.js"; return "unrar.js";
}; };
/**
* Unrarrer5
* @extends {bitjs.archive.Unarchiver}
* @constructor
*/
bitjs.archive.Unrarrer5 = function(arrayBuffer, optPathToBitJS) {
bitjs.base(this, arrayBuffer, optPathToBitJS);
};
bitjs.inherits(bitjs.archive.Unrarrer5, bitjs.archive.Unarchiver);
bitjs.archive.Unrarrer5.prototype.getScriptFileName = function() {
return "unrar5.js";
};
/** /**
* Untarrer * Untarrer
* @extends {bitjs.archive.Unarchiver} * @extends {bitjs.archive.Unarchiver}

@ -14,10 +14,10 @@
/* global VM_FIXEDGLOBALSIZE, VM_GLOBALMEMSIZE, MAXWINMASK, VM_GLOBALMEMADDR, MAXWINSIZE */ /* global VM_FIXEDGLOBALSIZE, VM_GLOBALMEMSIZE, MAXWINMASK, VM_GLOBALMEMADDR, MAXWINSIZE */
// This file expects to be invoked as a Worker (see onmessage below). // This file expects to be invoked as a Worker (see onmessage below).
importScripts("../io/bitstream.js"); /*importScripts("../io/bitstream.js");
importScripts("../io/bytebuffer.js"); importScripts("../io/bytebuffer.js");
importScripts("archive.js"); importScripts("archive.js");
importScripts("rarvm.js"); importScripts("rarvm.js");*/
// Progress variables. // Progress variables.
var currentFilename = ""; var currentFilename = "";
@ -29,19 +29,21 @@ var totalFilesInArchive = 0;
// Helper functions. // Helper functions.
var info = function(str) { var info = function(str) {
postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); console.log(str);
// postMessage(new bitjs.archive.UnarchiveInfoEvent(str));
}; };
var err = function(str) { var err = function(str) {
postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); console.log(str);
// postMessage(new bitjs.archive.UnarchiveErrorEvent(str));
}; };
var postProgress = function() { var postProgress = function() {
postMessage(new bitjs.archive.UnarchiveProgressEvent( /*postMessage(new bitjs.archive.UnarchiveProgressEvent(
currentFilename, currentFilename,
currentFileNumber, currentFileNumber,
currentBytesUnarchivedInFile, currentBytesUnarchivedInFile,
currentBytesUnarchived, currentBytesUnarchived,
totalUncompressedBytesInArchive, totalUncompressedBytesInArchive,
totalFilesInArchive)); totalFilesInArchive));*/
}; };
// shows a byte value as its hex representation // shows a byte value as its hex representation
@ -1298,7 +1300,7 @@ var unrar = function(arrayBuffer) {
totalUncompressedBytesInArchive = 0; totalUncompressedBytesInArchive = 0;
totalFilesInArchive = 0; totalFilesInArchive = 0;
postMessage(new bitjs.archive.UnarchiveStartEvent()); //postMessage(new bitjs.archive.UnarchiveStartEvent());
var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */); var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */);
var header = new RarVolumeHeader(bstream); var header = new RarVolumeHeader(bstream);
@ -1348,7 +1350,7 @@ var unrar = function(arrayBuffer) {
localfile.unrar(); localfile.unrar();
if (localfile.isValid) { if (localfile.isValid) {
postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); // postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile));
postProgress(); postProgress();
} }
} }
@ -1358,7 +1360,7 @@ var unrar = function(arrayBuffer) {
} else { } else {
err("Invalid RAR file"); err("Invalid RAR file");
} }
postMessage(new bitjs.archive.UnarchiveFinishEvent()); // postMessage(new bitjs.archive.UnarchiveFinishEvent());
}; };
// event.data.file has the ArrayBuffer. // event.data.file has the ArrayBuffer.

File diff suppressed because it is too large Load Diff

@ -24,6 +24,14 @@ var $list = $("#list").isotope({
}); });
$("#desc").click(function() { $("#desc").click(function() {
var page = $(this).data("id");
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/view",
data: "{\"" + page + "\": {\"dir\": \"desc\"}}",
});
$list.isotope({ $list.isotope({
sortBy: "name", sortBy: "name",
sortAscending: true sortAscending: true
@ -32,6 +40,14 @@ $("#desc").click(function() {
}); });
$("#asc").click(function() { $("#asc").click(function() {
var page = $(this).data("id");
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/view",
data: "{\"" + page + "\": {\"dir\": \"asc\"}}",
});
$list.isotope({ $list.isotope({
sortBy: "name", sortBy: "name",
sortAscending: false sortAscending: false

@ -19,6 +19,17 @@ var direction = 0; // Descending order
var sort = 0; // Show sorted entries var sort = 0; // Show sorted entries
$("#sort_name").click(function() { $("#sort_name").click(function() {
var class_name = $("h1").attr('Class') + "_sort_name";
var obj = {};
obj[class_name] = sort;
/*$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/view",
data: JSON.stringify({obj}),
});*/
var count = 0; var count = 0;
var index = 0; var index = 0;
var store; var store;
@ -40,9 +51,7 @@ $("#sort_name").click(function() {
count++; count++;
} }
}); });
/*listItems.sort(function(a,b){
return $(a).children()[1].innerText.localeCompare($(b).children()[1].innerText)
});*/
// Find count of middle element // Find count of middle element
if (count > 20) { if (count > 20) {
var middle = parseInt(count / 2, 10) + (count % 2); var middle = parseInt(count / 2, 10) + (count % 2);
@ -66,6 +75,14 @@ $("#desc").click(function() {
if (direction === 0) { if (direction === 0) {
return; return;
} }
var page = $(this).data("id");
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/view",
data: "{\"" + page + "\": {\"dir\": \"desc\"}}",
});
var index = 0; var index = 0;
var list = $("#list"); var list = $("#list");
var second = $("#second"); var second = $("#second");
@ -102,9 +119,18 @@ $("#desc").click(function() {
$("#asc").click(function() { $("#asc").click(function() {
if (direction === 1) { if (direction === 1) {
return; return;
} }
var page = $(this).data("id");
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/view",
data: "{\"" + page + "\": {\"dir\": \"asc\"}}",
});
var index = 0; var index = 0;
var list = $("#list"); var list = $("#list");
var second = $("#second"); var second = $("#second");
@ -131,7 +157,6 @@ $("#asc").click(function() {
}); });
// middle = parseInt(elementLength / 2) + (elementLength % 2); // middle = parseInt(elementLength / 2) + (elementLength % 2);
list.append(reversed.slice(0, index)); list.append(reversed.slice(0, index));
second.append(reversed.slice(index, elementLength)); second.append(reversed.slice(index, elementLength));
} else { } else {

@ -162,10 +162,15 @@ function initProgressClick() {
function loadFromArrayBuffer(ab) { function loadFromArrayBuffer(ab) {
var start = (new Date).getTime(); var start = (new Date).getTime();
var h = new Uint8Array(ab, 0, 10); var h = new Uint8Array(ab, 0, 10);
unrar5(ab);
var pathToBitJS = "../../static/js/archive/"; var pathToBitJS = "../../static/js/archive/";
var lastCompletion = 0; var lastCompletion = 0;
if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar! /*if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar!
if (h[7] === 0x01) {
unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS);
} else {
unarchiver = new bitjs.archive.Unrarrer5(ab, pathToBitJS);
}
} else if (h[0] === 80 && h[1] === 75) { //PK (Zip) } else if (h[0] === 80 && h[1] === 75) { //PK (Zip)
unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS); unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS);
} else if (h[0] === 255 && h[1] === 216) { // JPEG } else if (h[0] === 255 && h[1] === 216) { // JPEG
@ -229,7 +234,7 @@ function loadFromArrayBuffer(ab) {
unarchiver.start(); unarchiver.start();
} else { } else {
alert("Some error"); alert("Some error");
} }*/
} }
function scrollTocToActive() { function scrollTocToActive() {

@ -58,6 +58,60 @@ $(document).on("change", "select[data-controlall]", function() {
} }
}); });
$("#delete_confirm").click(function() {
//get data-id attribute of the clicked element
var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length - 1].src;
var path = src.substring(0, src.lastIndexOf("/"));
var deleteId = $(this).data("delete-id");
var bookFormat = $(this).data("delete-format");
if (bookFormat) {
window.location.href = path + "/../../delete/" + deleteId + "/" + bookFormat;
} else {
if ($(this).data("delete-format")) {
path = path + "/../../ajax/delete/" + deleteId;
$.ajax({
method:"get",
url: path,
timeout: 900,
success:function(data) {
data.forEach(function(item) {
if (!jQuery.isEmptyObject(item)) {
if (item.format != "") {
$("button[data-delete-format='"+item.format+"']").addClass('hidden');
}
$( ".navbar" ).after( '<div class="row-fluid text-center" style="margin-top: -20px;">' +
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
'</div>');
}
});
}
});
} else {
window.location.href = path + "/../../delete/" + deleteId;
}
}
});
//triggered when modal is about to be shown
$("#deleteModal").on("show.bs.modal", function(e) {
//get data-id attribute of the clicked element and store in button
var bookId = $(e.relatedTarget).data("delete-id");
var bookfomat = $(e.relatedTarget).data("delete-format");
if (bookfomat) {
$("#book_format").removeClass('hidden');
$("#book_complete").addClass('hidden');
} else {
$("#book_complete").removeClass('hidden');
$("#book_format").addClass('hidden');
}
$(e.currentTarget).find("#delete_confirm").data("delete-id", bookId);
$(e.currentTarget).find("#delete_confirm").data("delete-format", bookfomat);
});
$(function() { $(function() {
var updateTimerID; var updateTimerID;
@ -324,16 +378,19 @@ $(function() {
}); });
$(".update-view").click(function(e) { $(".update-view").click(function(e) {
var target = $(this).data("target");
var view = $(this).data("view"); var view = $(this).data("view");
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
var data = {}; $.ajax({
data[target] = view; method:"post",
console.debug("Updating view data: ", data); contentType: "application/json; charset=utf-8",
$.post( "/ajax/view", data).done(function( ) { dataType: "json",
url: window.location.pathname + "/../../ajax/view",
data: "{\"series\": {\"series_view\": \""+ view +"\"}}",
success: function success() {
location.reload(); location.reload();
}
}); });
}); });
}); });

@ -1,5 +1,5 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) /* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 OzzieIsaacs * Copyright (C) 2020 OzzieIsaacs
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -15,10 +15,158 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* exported TableActions, RestrictionActions*/ /* exported TableActions, RestrictionActions, EbookActions, responseHandler */
var selections = [];
$(function() { $(function() {
$("#books-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table",
function (e, rowsAfter, rowsBefore) {
var rows = rowsAfter;
if (e.type === "uncheck-all") {
rows = rowsBefore;
}
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id;
});
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids);
if (selections.length >= 2) {
$("#merge_books").removeClass("disabled");
$("#merge_books").attr("aria-disabled", false);
} else {
$("#merge_books").addClass("disabled");
$("#merge_books").attr("aria-disabled", true);
}
if (selections.length < 1) {
$("#delete_selection").addClass("disabled");
$("#delete_selection").attr("aria-disabled", true);
}
else{
$("#delete_selection").removeClass("disabled");
$("#delete_selection").attr("aria-disabled", false);
}
});
$("#delete_selection").click(function() {
$("#books-table").bootstrapTable('uncheckAll');
});
$("#merge_confirm").click(function() {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/mergebooks",
data: JSON.stringify({"Merge_books":selections}),
success: function success() {
$('#books-table').bootstrapTable('refresh');
$("#books-table").bootstrapTable('uncheckAll');
}
});
});
$("#merge_books").click(function() {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/simulatemerge",
data: JSON.stringify({"Merge_books":selections}),
success: function success(book_titles) {
$.each(book_titles.from, function(i, item) {
$("<span>- " + item + "</span>").appendTo("#merge_from");
});
$('#merge_to').text("- " + book_titles.to);
}
});
});
var column = [];
$("#books-table > thead > tr > th").each(function() {
var element = {};
if ($(this).attr("data-edit")) {
element = {
editable: {
mode: "inline",
emptytext: "<span class='glyphicon glyphicon-plus'></span>",
}
};
}
var validateText = $(this).attr("data-edit-validate");
if (validateText) {
element.editable.validate = function (value) {
if ($.trim(value) === "") return validateText;
};
}
column.push(element);
});
$("#books-table").bootstrapTable({
sidePagination: "server",
pagination: true,
paginationLoop: false,
paginationDetailHAlign: " hidden",
paginationHAlign: "left",
idField: "id",
uniqueId: "id",
search: true,
showColumns: true,
searchAlign: "left",
showSearchButton : false,
searchOnEnterKey: true,
checkboxHeader: false,
maintainMetaData: true,
responseHandler: responseHandler,
columns: column,
formatNoMatches: function () {
return "";
},
onEditableSave: function (field, row, oldvalue, $el) {
if (field === 'title' || field === 'authors') {
$.ajax({
method:"get",
dataType: "json",
url: window.location.pathname + "/../../ajax/sort_value/" + field + '/' + row.id,
success: function success(data) {
var key = Object.keys(data)[0]
$("#books-table").bootstrapTable('updateCellByUniqueId', {
id: row.id,
field: key,
value: data[key]
});
console.log(data);
}
});
}
},
onColumnSwitch: function (field, checked) {
var visible = $("#books-table").bootstrapTable('getVisibleColumns');
var hidden = $("#books-table").bootstrapTable('getHiddenColumns');
var visibility =[]
var st = ""
visible.forEach(function(item) {
st += "\""+ item.field + "\":\"" +"true"+ "\","
});
hidden.forEach(function(item) {
st += "\""+ item.field + "\":\"" +"false"+ "\","
});
st = st.slice(0, -1);
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/table_settings",
data: "{" + st + "}",
});
},
});
$("#domain_allow_submit").click(function(event) { $("#domain_allow_submit").click(function(event) {
event.preventDefault(); event.preventDefault();
$("#domain_add_allow").ajaxForm(); $("#domain_add_allow").ajaxForm();
@ -33,6 +181,7 @@ $(function() {
} }
}); });
}); });
$("#domain-allow-table").bootstrapTable({ $("#domain-allow-table").bootstrapTable({
formatNoMatches: function () { formatNoMatches: function () {
return ""; return "";
@ -205,6 +354,7 @@ function TableActions (value, row) {
].join(""); ].join("");
} }
/* Function for deleting domain restrictions */ /* Function for deleting domain restrictions */
function RestrictionActions (value, row) { function RestrictionActions (value, row) {
return [ return [
@ -213,3 +363,20 @@ function RestrictionActions (value, row) {
"</div>" "</div>"
].join(""); ].join("");
} }
/* Function for deleting books */
function EbookActions (value, row) {
return [
"<div class=\"book-remove\" data-toggle=\"modal\" data-target=\"#deleteModal\" data-ajax=\"1\" data-delete-id=\"" + row.id + "\" title=\"Remove\">",
"<i class=\"glyphicon glyphicon-trash\"></i>",
"</div>"
].join("");
}
/* Function for keeping checked rows */
function responseHandler(res) {
$.each(res.rows, function (i, row) {
row.state = $.inArray(row.id, selections) !== -1;
});
return res;
}

@ -0,0 +1,217 @@
from __future__ import division, print_function, unicode_literals
import sys
import os
import re
from glob import glob
from shutil import copyfile
from sqlalchemy.exc import SQLAlchemyError
from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS
from cps import calibre_db, db
from cps import logger, config
from cps.subproc_wrapper import process_open
from flask_babel import gettext as _
from cps.tasks.mail import TaskEmail
from cps import gdriveutils
log = logger.create()
class TaskConvert(CalibreTask):
def __init__(self, file_path, bookid, taskMessage, settings, kindle_mail, user=None):
super(TaskConvert, self).__init__(taskMessage)
self.file_path = file_path
self.bookid = bookid
self.settings = settings
self.kindle_mail = kindle_mail
self.user = user
self.results = dict()
def run(self, worker_thread):
self.worker_thread = worker_thread
if config.config_use_google_drive:
cur_book = calibre_db.get_book(self.bookid)
data = calibre_db.get_book_format(self.bookid, self.settings['old_book_format'])
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
data.name + "." + self.settings['old_book_format'].lower())
if df:
datafile = os.path.join(config.config_calibre_dir,
cur_book.path,
data.name + u"." + self.settings['old_book_format'].lower())
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)):
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path))
df.GetContentFile(datafile)
else:
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
format=self.settings['old_book_format'],
fn=data.name + "." + self.settings['old_book_format'].lower())
return error_message
filename = self._convert_ebook_format()
if config.config_use_google_drive:
os.remove(self.file_path + u'.' + self.settings['old_book_format'].lower())
if filename:
if config.config_use_google_drive:
# Upload files to gdrive
gdriveutils.updateGdriveCalibreFromLocal()
self._handleSuccess()
if self.kindle_mail:
# if we're sending to kindle after converting, create a one-off task and run it immediately
# todo: figure out how to incorporate this into the progress
try:
worker_thread.add(self.user, TaskEmail(self.settings['subject'], self.results["path"],
filename, self.settings, self.kindle_mail,
self.settings['subject'], self.settings['body'], internal=True))
except Exception as e:
return self._handleError(str(e))
def _convert_ebook_format(self):
error_message = None
local_session = db.CalibreDB().session
file_path = self.file_path
book_id = self.bookid
format_old_ext = u'.' + self.settings['old_book_format'].lower()
format_new_ext = u'.' + self.settings['new_book_format'].lower()
# check to see if destination format already exists -
# if it does - mark the conversion task as complete and return a success
# this will allow send to kindle workflow to continue to work
if os.path.isfile(file_path + format_new_ext):
log.info("Book id %d already converted to %s", book_id, format_new_ext)
cur_book = calibre_db.get_book(book_id)
self.results['path'] = file_path
self.results['title'] = cur_book.title
self._handleSuccess()
return os.path.basename(file_path + format_new_ext)
else:
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
book_id,
format_new_ext)
if config.config_kepubifypath and format_old_ext == '.epub' and format_new_ext == '.kepub':
check, error_message = self._convert_kepubify(file_path,
format_old_ext,
format_new_ext)
else:
# check if calibre converter-executable is existing
if not os.path.exists(config.config_converterpath):
# ToDo Text is not translated
self._handleError(_(u"Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
return
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
if check == 0:
cur_book = calibre_db.get_book(book_id)
if os.path.isfile(file_path + format_new_ext):
# self.db_queue.join()
new_format = db.Data(name=cur_book.data[0].name,
book_format=self.settings['new_book_format'].upper(),
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
try:
local_session.merge(new_format)
local_session.commit()
except SQLAlchemyError as e:
local_session.rollback()
log.error("Database error: %s", e)
return
self.results['path'] = cur_book.path
self.results['title'] = cur_book.title
if not config.config_use_google_drive:
self._handleSuccess()
return os.path.basename(file_path + format_new_ext)
else:
error_message = _('%(format)s format not found on disk', format=format_new_ext.upper())
log.info("ebook converter failed with error while converting book")
if not error_message:
error_message = _('Ebook converter failed with unknown error')
self._handleError(error_message)
return
def _convert_kepubify(self, file_path, format_old_ext, format_new_ext):
quotes = [1, 3]
command = [config.config_kepubifypath, (file_path + format_old_ext), '-o', os.path.dirname(file_path)]
try:
p = process_open(command, quotes)
except OSError as e:
return 1, _(u"Kepubify-converter failed: %(error)s", error=e)
self.progress = 0.01
while True:
nextline = p.stdout.readlines()
nextline = [x.strip('\n') for x in nextline if x != '\n']
if sys.version_info < (3, 0):
nextline = [x.decode('utf-8') for x in nextline]
for line in nextline:
log.debug(line)
if p.poll() is not None:
break
# ToD Handle
# process returncode
check = p.returncode
# move file
if check == 0:
converted_file = glob(os.path.join(os.path.dirname(file_path), "*.kepub.epub"))
if len(converted_file) == 1:
copyfile(converted_file[0], (file_path + format_new_ext))
os.unlink(converted_file[0])
else:
return 1, _(u"Converted file not found or more than one file in folder %(folder)s",
folder=os.path.dirname(file_path))
return check, None
def _convert_calibre(self, file_path, format_old_ext, format_new_ext):
try:
# Linux py2.7 encode as list without quotes no empty element for parameters
# linux py3.x no encode and as list without quotes no empty element for parameters
# windows py2.7 encode as string with quotes empty element for parameters is okay
# windows py 3.x no encode and as string with quotes empty element for parameters is okay
# separate handling for windows and linux
quotes = [1, 2]
command = [config.config_converterpath, (file_path + format_old_ext),
(file_path + format_new_ext)]
quotes_index = 3
if config.config_calibre:
parameters = config.config_calibre.split(" ")
for param in parameters:
command.append(param)
quotes.append(quotes_index)
quotes_index += 1
p = process_open(command, quotes)
except OSError as e:
return 1, _(u"Ebook-converter failed: %(error)s", error=e)
while p.poll() is None:
nextline = p.stdout.readline()
if os.name == 'nt' and sys.version_info < (3, 0):
nextline = nextline.decode('windows-1252')
elif os.name == 'posix' and sys.version_info < (3, 0):
nextline = nextline.decode('utf-8')
log.debug(nextline.strip('\r\n'))
# parse progress string from calibre-converter
progress = re.search(r"(\d+)%\s.*", nextline)
if progress:
self.progress = int(progress.group(1)) / 100
if config.config_use_google_drive:
self.progress *= 0.9
# process returncode
check = p.returncode
calibre_traceback = p.stderr.readlines()
error_message = ""
for ele in calibre_traceback:
if sys.version_info < (3, 0):
ele = ele.decode('utf-8')
log.debug(ele.strip('\n'))
if not ele.startswith('Traceback') and not ele.startswith(' File'):
error_message = _("Calibre failed with error: %(error)s", error=ele.strip('\n'))
return check, error_message
@property
def name(self):
return "Convert"

@ -0,0 +1,241 @@
from __future__ import division, print_function, unicode_literals
import sys
import os
import smtplib
import threading
import socket
try:
from StringIO import StringIO
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
except ImportError:
from io import StringIO
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email import encoders
from email.utils import formatdate, make_msgid
from email.generator import Generator
from cps.services.worker import CalibreTask
from cps import logger, config
from cps import gdriveutils
log = logger.create()
CHUNKSIZE = 8192
# Class for sending email with ability to get current progress
class EmailBase:
transferSize = 0
progress = 0
def data(self, msg):
self.transferSize = len(msg)
(code, resp) = smtplib.SMTP.data(self, msg)
self.progress = 0
return (code, resp)
def send(self, strg):
"""Send `strg' to the server."""
log.debug('send: %r', strg[:300])
if hasattr(self, 'sock') and self.sock:
try:
if self.transferSize:
lock=threading.Lock()
lock.acquire()
self.transferSize = len(strg)
lock.release()
for i in range(0, self.transferSize, CHUNKSIZE):
if isinstance(strg, bytes):
self.sock.send((strg[i:i + CHUNKSIZE]))
else:
self.sock.send((strg[i:i + CHUNKSIZE]).encode('utf-8'))
lock.acquire()
self.progress = i
lock.release()
else:
self.sock.sendall(strg.encode('utf-8'))
except socket.error:
self.close()
raise smtplib.SMTPServerDisconnected('Server not connected')
else:
raise smtplib.SMTPServerDisconnected('please run connect() first')
@classmethod
def _print_debug(cls, *args):
log.debug(args)
def getTransferStatus(self):
if self.transferSize:
lock2 = threading.Lock()
lock2.acquire()
value = int((float(self.progress) / float(self.transferSize))*100)
lock2.release()
return value / 100
else:
return 1
# Class for sending email with ability to get current progress, derived from emailbase class
class Email(EmailBase, smtplib.SMTP):
def __init__(self, *args, **kwargs):
smtplib.SMTP.__init__(self, *args, **kwargs)
# Class for sending ssl encrypted email with ability to get current progress, , derived from emailbase class
class EmailSSL(EmailBase, smtplib.SMTP_SSL):
def __init__(self, *args, **kwargs):
smtplib.SMTP_SSL.__init__(self, *args, **kwargs)
class TaskEmail(CalibreTask):
def __init__(self, subject, filepath, attachment, settings, recipient, taskMessage, text, internal=False):
super(TaskEmail, self).__init__(taskMessage)
self.subject = subject
self.attachment = attachment
self.settings = settings
self.filepath = filepath
self.recipent = recipient
self.text = text
self.asyncSMTP = None
self.results = dict()
def run(self, worker_thread):
# create MIME message
msg = MIMEMultipart()
msg['Subject'] = self.subject
msg['Message-Id'] = make_msgid('calibre-web')
msg['Date'] = formatdate(localtime=True)
text = self.text
msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8'))
if self.attachment:
result = self._get_attachment(self.filepath, self.attachment)
if result:
msg.attach(result)
else:
self._handleError(u"Attachment not found")
return
msg['From'] = self.settings["mail_from"]
msg['To'] = self.recipent
use_ssl = int(self.settings.get('mail_use_ssl', 0))
try:
# convert MIME message to string
fp = StringIO()
gen = Generator(fp, mangle_from_=False)
gen.flatten(msg)
msg = fp.getvalue()
# send email
timeout = 600 # set timeout to 5mins
# redirect output to logfile on python2 pn python3 debugoutput is caught with overwritten
# _print_debug function
if sys.version_info < (3, 0):
org_smtpstderr = smtplib.stderr
smtplib.stderr = logger.StderrLogger('worker.smtp')
if use_ssl == 2:
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
timeout=timeout)
else:
self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout)
# link to logginglevel
if logger.is_debug_enabled():
self.asyncSMTP.set_debuglevel(1)
if use_ssl == 1:
self.asyncSMTP.starttls()
if self.settings["mail_password"]:
self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"]))
self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, msg)
self.asyncSMTP.quit()
self._handleSuccess()
if sys.version_info < (3, 0):
smtplib.stderr = org_smtpstderr
except (MemoryError) as e:
log.exception(e)
self._handleError(u'MemoryError sending email: ' + str(e))
# return None
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
if hasattr(e, "smtp_error"):
text = e.smtp_error.decode('utf-8').replace("\n", '. ')
elif hasattr(e, "message"):
text = e.message
elif hasattr(e, "args"):
text = '\n'.join(e.args)
else:
log.exception(e)
text = ''
self._handleError(u'Smtplib Error sending email: ' + text)
# return None
except (socket.error) as e:
self._handleError(u'Socket Error sending email: ' + e.strerror)
# return None
@property
def progress(self):
if self.asyncSMTP is not None:
return self.asyncSMTP.getTransferStatus()
else:
return self._progress
@progress.setter
def progress(self, x):
"""This gets explicitly set when handle(Success|Error) are called. In this case, remove the SMTP connection"""
if x == 1:
self.asyncSMTP = None
self._progress = x
@classmethod
def _get_attachment(cls, bookpath, filename):
"""Get file as MIMEBase message"""
calibrepath = config.config_calibre_dir
if config.config_use_google_drive:
df = gdriveutils.getFileFromEbooksFolder(bookpath, filename)
if df:
datafile = os.path.join(calibrepath, bookpath, filename)
if not os.path.exists(os.path.join(calibrepath, bookpath)):
os.makedirs(os.path.join(calibrepath, bookpath))
df.GetContentFile(datafile)
else:
return None
file_ = open(datafile, 'rb')
data = file_.read()
file_.close()
os.remove(datafile)
else:
try:
file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb')
data = file_.read()
file_.close()
except IOError as e:
log.exception(e)
log.error(u'The requested file could not be read. Maybe wrong permissions?')
return None
attachment = MIMEBase('application', 'octet-stream')
attachment.set_payload(data)
encoders.encode_base64(attachment)
attachment.add_header('Content-Disposition', 'attachment',
filename=filename)
return attachment
@property
def name(self):
return "Email"

@ -0,0 +1,19 @@
from __future__ import division, print_function, unicode_literals
from datetime import datetime
from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS
class TaskUpload(CalibreTask):
def __init__(self, taskMessage):
super(TaskUpload, self).__init__(taskMessage)
self.start_time = self.end_time = datetime.now()
self.stat = STAT_FINISH_SUCCESS
self.progress = 1
def run(self, worker_thread):
"""Upload task doesn't have anything to do, it's simply a way to add information to the task list"""
pass
@property
def name(self):
return "Upload"

@ -161,8 +161,8 @@
</table> </table>
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div> <div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
<div class="btn btn-default" id="check_for_update">{{_('Check for Update')}}</div> <div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div>
<div class="btn btn-default hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div> <div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div>
</div> </div>
</div> </div>
</div> </div>

@ -23,14 +23,14 @@
<h3>{{_("In Library")}}</h3> <h3>{{_("In Library")}}</h3>
{% endif %} {% endif %}
<div class="filterheader hidden-xs hidden-sm"> <div class="filterheader hidden-xs hidden-sm">
<a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a> <a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a> <a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<!--div class="btn-group character" role="group"> <!--div class="btn-group character" role="group">
<a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort='pubold')}}"><span class="glyphicon glyphicon-list"></span><b>?</b></a> <a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-list"></span><b>?</b></a>
<div id="all" class="btn btn-primary">{{_('All')}}</div> <div id="all" class="btn btn-primary">{{_('All')}}</div>
</div--> </div-->
</div> </div>
@ -53,7 +53,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -61,7 +61,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.data %} {% for format in entry.data %}

@ -7,13 +7,13 @@
</div> </div>
{% if g.user.role_delete_books() %} {% if g.user.role_delete_books() %}
<div class="text-center"> <div class="text-center">
<button type="button" class="btn btn-danger" id="delete" data-toggle="modal" data-target="#deleteModal">{{_("Delete Book")}}</button> <button type="button" class="btn btn-danger" id="delete" data-toggle="modal" data-delete-id="{{ book.id }}" data-target="#deleteModal">{{_("Delete Book")}}</button>
</div> </div>
{% if book.data|length > 1 %} {% if book.data|length > 1 %}
<div class="text-center more-stuff"><h4>{{_('Delete formats:')}}</h4> <div class="text-center more-stuff"><h4>{{_('Delete formats:')}}</h4>
{% for file in book.data %} {% for file in book.data %}
<div class="form-group"> <div class="form-group">
<a href="{{ url_for('editbook.delete_book', book_id=book.id, book_format=file.format) }}" class="btn btn-danger" type="button">{{_('Delete')}} - {{file.format}}</a> <button type="button" class="btn btn-danger" id="delete_format" data-toggle="modal" data-delete-id="{{ book.id }}" data-delete-format="{{ file.format }}" data-target="#deleteModal">{{_('Delete')}} - {{file.format}}</button>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@ -197,34 +197,7 @@
{% endblock %} {% endblock %}
{% block modal %} {% block modal %}
{% if g.user.role_delete_books() %} {{ delete_book(book.id) }}
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body text-center">
<p>
<span>{{_('This book will be permanently erased from database')}}</span>
<span>{{_('and hard disk')}}</span>
</p>
{% if config.config_kobo_sync %}
<p>
<span>{{_('Important Kobo Note: deleted books will remain on any paired Kobo device.')}}</span>
<span>{{_('Books must first be archived and the device synced before a book can safely be deleted.')}}</span>
</p>
{% endif %}
</div>
<div class="modal-footer">
<a href="{{ url_for('editbook.delete_book', book_id=book.id) }}" id="delete_confirm" class="btn btn-danger">{{_('Delete')}}</a>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endif %}
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel"> <div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">

@ -1,59 +1,99 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% macro text_table_row(parameter, edit_text, show_text, validate) -%}
<h1 class="{{page}}">{{_(title)}}</h1> <th data-field="{{ parameter }}" id="{{ parameter }}" data-sortable="true"
data-visible = "{{visiblility.get(parameter)}}"
<div class="filterheader hidden-xs hidden-sm"> {% if g.user.role_edit() %}
{% if entries.__len__() %} data-editable-type="text"
{% if data == 'author' %} data-editable-url="{{ url_for('editbook.edit_list_book', param=parameter)}}"
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button> data-editable-title="{{ edit_text }}"
{% endif %} data-edit="true"
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}" {% endif %}
{% endif %} {% endif %}
<button id="desc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> >{{ show_text }}</th>
<button id="asc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button> {%- endmacro %}
{% if charlist|length %}
<button id="all" class="btn btn-primary">{{_('All')}}</button>
{% endif %}
<div class="btn-group character" role="group">
{% for char in charlist%}
<button class="btn btn-primary char">{{char.char}}</button>
{% endfor %}
</div>
{% if title == "Series" %} {% block header %}
<button class="update-view btn btn-primary" href="#" data-target="series_view" data-view="grid">Grid</button> <link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
{% endif %} <link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
{% endblock %}
{% block body %}
<h2 class="{{page}}">{{_(title)}}</h2>
<div class="col-xs-12 col-sm-6">
<div class="row">
<div class="btn btn-default disabled" id="merge_books" data-toggle="modal" data-target="#mergeModal" aria-disabled="true">{{_('Merge selected books')}}</div>
<div class="btn btn-default disabled" id="delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
</div>
</div> </div>
<div class="container"> <div class="col-xs-12 col-sm-6">
<div id="list" class="col-xs-12 col-sm-6"> <div class="row">
{% for entry in entries %} <input type="checkbox" id="autoupdate_titlesort" name="autoupdate_titlesort" checked>
{% if loop.index0 == (loop.length/2+loop.length%2)|int and loop.length > 20 %} <label for="autoupdate_titlesort">{{_('Update Title Sort automatically')}}</label>
</div> </div>
<div id="second" class="col-xs-12 col-sm-6"> <div class="row">
<input type="checkbox" id="autoupdate_autorsort" name="autoupdate_autorsort" checked>
<label for="autoupdate_autorsort">{{_('Update Author Sort automatically')}}</label>
</div>
</div>
<table id="books-table" class="table table-no-bordered table-striped"
data-url="{{url_for('web.list_books')}}">
<thead>
<tr>
{% if g.user.role_edit() %}
<th data-field="state" data-checkbox="true" data-sortable="true"></th>
{% endif %} {% endif %}
<div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry.name %}{{entry.name}}{% else %}{{entry[0].name}}{% endif %}{% endif %}"> <th data-field="id" id="id" data-visible="false" data-switchable="false"></th>
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry.count}}</span></div> {{ text_table_row('title', _('Enter Title'),_('Title'), true) }}
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort='new', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].id )}}{% endif %}"> {{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false) }}
{% if entry.name %} {{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false) }}
<div class="rating"> {{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }}
{% for number in range(entry.name) %} {{ text_table_row('tags', _('Enter Categories'),_('Categories'), false) }}
<span class="glyphicon glyphicon-star good"></span> {{ text_table_row('series', _('Enter Series'),_('Series'), false) }}
{% if loop.last and loop.index < 5 %} <th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('editbook.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter title')}}"{% endif %}>{{_('Series Index')}}</th>
{% for numer in range(5 - loop.index) %} {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }}
<span class="glyphicon glyphicon-star"></span> <!--th data-field="pubdate" data-type="date" data-visible="{{visiblility.get('pubdate')}}" data-viewformat="dd.mm.yyyy" id="pubdate" data-sortable="true">{{_('Publishing Date')}}</th-->
{% endfor %} {{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false) }}
{% if g.user.role_edit() %}
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
{% endif %} {% endif %}
{% endfor %} </tr>
</thead>
</table>
{% endblock %}
{% block modal %}
{{ delete_book(0) }}
{% if g.user.role_edit() %}
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div> </div>
{% else %} <div class="modal-body">
{% if entry.format %} <p></p>
{{entry.format}} <div class="text-left">{{_('Books with Title will be merged from:')}}</div>
{% else %} <p></p>
{{entry[0].name}}{% endif %}{% endif %}</a></div> <div class=text-left" id="merge_from"></div>
<p></p>
<div class="text-left">{{_('Into Book with Title:')}}</div>
<p></p>
<div class=text-left" id="merge_to"></div>
</div> </div>
{% endfor %} <div class="modal-footer">
<input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div> </div>
</div> </div>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{{ url_for('static', filename='js/filter_list.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
<script>
</script>
{% endblock %} {% endblock %}

@ -92,7 +92,7 @@
<h2 id="title">{{entry.title|shortentitle(40)}}</h2> <h2 id="title">{{entry.title|shortentitle(40)}}</h2>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;
{% endif %} {% endif %}
@ -114,7 +114,7 @@
{% endif %} {% endif %}
{% if entry.series|length > 0 %} {% if entry.series|length > 0 %}
<p>{{_('Book')}} {{entry.series_index}} {{_('of')}} <a href="{{url_for('web.books_list', data='series',sort='abc', book_id=entry.series[0].id)}}">{{entry.series[0].name}}</a></p> <p>{{_('Book')}} {{entry.series_index}} {{_('of')}} <a href="{{url_for('web.books_list', data='series', sort_param='abc', book_id=entry.series[0].id)}}">{{entry.series[0].name}}</a></p>
{% endif %} {% endif %}
{% if entry.languages.__len__() > 0 %} {% if entry.languages.__len__() > 0 %}
@ -143,7 +143,7 @@
<span class="glyphicon glyphicon-tags"></span> <span class="glyphicon glyphicon-tags"></span>
{% for tag in entry.tags %} {% for tag in entry.tags %}
<a href="{{ url_for('web.books_list', data='category', sort='new', book_id=tag.id) }}" class="btn btn-xs btn-info" role="button">{{tag.name}}</a> <a href="{{ url_for('web.books_list', data='category', sort_param='new', book_id=tag.id) }}" class="btn btn-xs btn-info" role="button">{{tag.name}}</a>
{%endfor%} {%endfor%}
</p> </p>
@ -154,13 +154,13 @@
<div class="publishers"> <div class="publishers">
<p> <p>
<span>{{_('Publisher')}}: <span>{{_('Publisher')}}:
<a href="{{url_for('web.books_list', data='publisher', sort='new', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a> <a href="{{url_for('web.books_list', data='publisher', sort_param='new', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a>
</span> </span>
</p> </p>
</div> </div>
{% endif %} {% endif %}
{% if entry.pubdate[:10] != '0101-01-01' %} {% if (entry.pubdate|string)[:10] != '0101-01-01' %}
<div class="publishing-date"> <div class="publishing-date">
<p>{{_('Published')}}: {{entry.pubdate|formatdate}} </p> <p>{{_('Published')}}: {{entry.pubdate|formatdate}} </p>
</div> </div>
@ -281,7 +281,7 @@
{% if g.user.role_edit() %} {% if g.user.role_edit() %}
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group" aria-label="Edit/Delete book"> <div class="btn-group" role="group" aria-label="Edit/Delete book">
<a href="{{ url_for('editbook.edit_book', book_id=entry.id) }}" class="btn btn-sm btn-warning" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a> <a href="{{ url_for('editbook.edit_book', book_id=entry.id) }}" class="btn btn-sm btn-primary" id="edit_book" role="button"><span class="glyphicon glyphicon-edit"></span> {{_('Edit Metadata')}}</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}

@ -22,7 +22,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -30,7 +30,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>

@ -8,8 +8,8 @@
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button> <button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
{% endif %} {% endif %}
{% endif %} {% endif %}
<button id="desc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> <button id="desc" data-id="series" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button>
<button id="asc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button> <button id="asc" data-id="series" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button>
{% if charlist|length %} {% if charlist|length %}
<button id="all" class="btn btn-primary">{{_('All')}}</button> <button id="all" class="btn btn-primary">{{_('All')}}</button>
{% endif %} {% endif %}
@ -19,7 +19,7 @@
{% endfor %} {% endfor %}
</div> </div>
<button class="update-view btn btn-primary" href="#" data-target="series_view" data-view="list">List</button> <button class="update-view btn btn-primary" href="#" data-target="series_view" id='list-button' data-view="list">List</button>
</div> </div>
{% if entries[0] %} {% if entries[0] %}
@ -27,13 +27,13 @@
{% for entry in entries %} {% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}"> <div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}">
<div class="cover"> <div class="cover">
<a href="{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/> <img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
<span class="badge">{{entry.count}}</span> <span class="badge">{{entry.count}}</span>
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].series[0].id )}}">
<p class="title">{{entry[0].series[0].name|shortentitle}}</p> <p class="title">{{entry[0].series[0].name|shortentitle}}</p>
</a> </a>
</div> </div>

@ -21,7 +21,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -29,7 +29,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>
@ -54,14 +54,14 @@
<div class="discover load-more"> <div class="discover load-more">
<h2 class="{{title}}">{{_(title)}}</h2> <h2 class="{{title}}">{{_(title)}}</h2>
<div class="filterheader hidden-xs hidden-sm"> <div class="filterheader hidden-xs hidden-sm">
<a data-toggle="tooltip" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a data-toggle="tooltip" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a> <a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a> <a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<!--div class="btn-group character"> <!--div class="btn-group character">
<a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort='pubold')}}"><span class="glyphicon glyphicon-list"></span> <b>{{_('Group by series')}}</b></a> <a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-list"></span> <b>{{_('Group by series')}}</b></a>
</div--> </div-->
</div> </div>
@ -84,7 +84,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', book_id=author.id, sort='new') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='new') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -92,7 +92,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort='new') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', book_id=author.id, sort_param='new') }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.data %} {% for format in entry.data %}

@ -10,7 +10,7 @@
{% endif %} {% endif %}
<div class="row"> <div class="row">
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{lang_counter[loop.index0].bookcount}}</span></div> <div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{lang_counter[loop.index0].bookcount}}</span></div>
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{{url_for('web.books_list', book_id=lang.lang_code, data=data, sort='new')}}">{{lang.name}}</a></div> <div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{{url_for('web.books_list', book_id=lang.lang_code, data=data, sort_param='new')}}">{{lang.name}}</a></div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

@ -1,4 +1,4 @@
{% from 'modal_restriction.html' import restrict_modal %} {% from 'modal_dialogs.html' import restrict_modal, delete_book %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ g.user.locale }}"> <html lang="{{ g.user.locale }}">
<head> <head>
@ -128,7 +128,7 @@
<li class="nav-head hidden-xs">{{_('Browse')}}</li> <li class="nav-head hidden-xs">{{_('Browse')}}</li>
{% for element in sidebar %} {% for element in sidebar %}
{% if g.user.check_visibility(element['visibility']) and element['public'] %} {% if g.user.check_visibility(element['visibility']) and element['public'] %}
<li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{{url_for(element['link'], data=element['page'], sort='new')}}"><span class="glyphicon {{element['glyph']}}"></span>{{_(element['text'])}}</a></li> <li id="nav_{{element['id']}}" {% if page == element['page'] %}class="active"{% endif %}><a href="{{url_for(element['link'], data=element['page'], sort_param='stored')}}"><span class="glyphicon {{element['glyph']}}"></span>{{_(element['text'])}}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if g.user.is_authenticated or g.allow_anonymous %} {% if g.user.is_authenticated or g.allow_anonymous %}
@ -136,10 +136,6 @@
{% for shelf in g.shelves_access %} {% for shelf in g.shelves_access %}
<li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list shelf"></span>{{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li> <li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list shelf"></span>{{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
{% endfor %} {% endfor %}
<!--li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %}
<li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
{% endfor %}-->
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li> <li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li>
<li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li> <li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span> {{_('About')}}</a></li>
@ -155,23 +151,23 @@
{% if pagination and (pagination.has_next or pagination.has_prev) %} {% if pagination and (pagination.has_next or pagination.has_prev) %}
<div class="pagination"> <div class="pagination">
{% if pagination.has_prev %} {% if pagination.has_prev %}
<a class="previous" href="{{ (pagination.page - 1)|url_for_other_page <li class="page-item page-previous"><a class="page-link" aria-label="next page" href="{{ (pagination.page - 1)|url_for_other_page
}}">&laquo; {{_('Previous')}}</a> }}">&laquo; {{_('Previous')}}</a></li>
{% endif %} {% endif %}
{% for page in pagination.iter_pages() %} {% for page in pagination.iter_pages() %}
{% if page %} {% if page %}
{% if page != pagination.page %} {% if page != pagination.page %}
<a href="{{ (page)|url_for_other_page }}">{{ page }}</a> <li class="page-item"><a class="page-link" aria-label="to page {{ page }}" href="{{ (page)|url_for_other_page }}">{{ page }}</a></li>
{% else %} {% else %}
<strong>{{ page }}</strong> <li class="page-item active"><a class="page-link" aria-label="to page {{ page }}" href="{{ (page)|url_for_other_page }}">{{ page }}</a></li>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="ellipsis"></span> <li class="page-item page-last-separator disabled"><a class="page-link" aria-label=""></a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if pagination.has_next %} {% if pagination.has_next %}
<a class="next" href="{{ (pagination.page + 1)|url_for_other_page <li class="page-item page-next"><a class="page-link" aria-label="next page" href="{{ (pagination.page + 1)|url_for_other_page
}}">{{_('Next')}} &raquo;</a> }}">{{_('Next')}} &raquo;</a></li>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@ -196,7 +192,6 @@
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<!--script src="https://code.jquery.com/jquery.js"></script-->
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<!-- Include all compiled plugins (below), or include individual files as needed --> <!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="{{ url_for('static', filename='js/libs/bootstrap.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/bootstrap.min.js') }}"></script>
@ -227,10 +222,12 @@
}); });
$(document).ready(function() { $(document).ready(function() {
var inp = $('#query').first() var inp = $('#query').first()
if (inp.length) {
var val = inp.val() var val = inp.val()
if (val !== "undefined") { if (val.length) {
inp.val('').blur().focus().val(val) inp.val('').blur().focus().val(val)
} }
}
}); });
}); });
</script> </script>

@ -8,8 +8,8 @@
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button> <button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
{% endif %} {% endif %}
{% endif %} {% endif %}
<button id="desc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> <button id="desc" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button>
<button id="asc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button> <button id="asc" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button>
{% if charlist|length %} {% if charlist|length %}
<button id="all" class="btn btn-primary">{{_('All')}}</button> <button id="all" class="btn btn-primary">{{_('All')}}</button>
{% endif %} {% endif %}
@ -20,7 +20,7 @@
</div> </div>
{% if data == "series" %} {% if data == "series" %}
<button class="update-view btn btn-primary" href="#" data-target="series_view" data-view="grid">Grid</button> <button class="update-view btn btn-primary" href="#" data-target="series_view" id='grid-button' data-view="grid">Grid</button>
{% endif %} {% endif %}
</div> </div>
<div class="container"> <div class="container">
@ -32,7 +32,7 @@
{% endif %} {% endif %}
<div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry.name %}{{entry.name}}{% else %}{{entry[0].name}}{% endif %}{% endif %}"> <div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry.name %}{{entry.name}}{% else %}{{entry[0].name}}{% endif %}{% endif %}">
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry.count}}</span></div> <div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry.count}}</span></div>
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort='new', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].id )}}{% endif %}"> <div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort_param='new', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort_param='new', book_id=entry[0].id )}}{% endif %}">
{% if entry.name %} {% if entry.name %}
<div class="rating"> <div class="rating">
{% for number in range(entry.name) %} {% for number in range(entry.name) %}

@ -37,3 +37,34 @@
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro delete_book(bookid) %}
{% if g.user.role_delete_books() %}
<div class="modal fade" id="deleteModal" role="dialog" aria-labelledby="metaDeleteLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body text-center">
<p>
<span class="hidden" id="book_format">{{_('This book format will be permanently erased from database')}}</span>
<span class="hidden" id="book_complete">{{_('This book will be permanently erased from database')}}</span>
<span>{{_('and hard disk')}}</span>
</p>
{% if config.config_kobo_sync %}
<p>
<span>{{_('Important Kobo Note: deleted books will remain on any paired Kobo device.')}}</span>
<span>{{_('Books must first be archived and the device synced before a book can safely be deleted.')}}</span>
</p>
{% endif %}
</div>
<div class="modal-footer">
<input type="button" class="btn btn-danger" value="{{_('Delete')}}" name="delete_confirm" id="delete_confirm" data-dismiss="modal">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endmacro %}

@ -14,8 +14,13 @@
<script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/kthoom.js') }}"></script> <script src="{{ url_for('static', filename='js/io/bytestream.js') }}"></script>
<script src="{{ url_for('static', filename='js/io/bytebuffer.js') }}"></script>
<script src="{{ url_for('static', filename='js/io/bitstream.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive/archive.js') }}"></script> <script src="{{ url_for('static', filename='js/archive/archive.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive/rarvm.js') }}"></script>
<script src="{{ url_for('static', filename='js/archive/unrar5.js') }}"></script>
<script src="{{ url_for('static', filename='js/kthoom.js') }}"></script>
<script> <script>
var updateArrows = function() { var updateArrows = function() {
if ($('input[name="direction"]:checked').val() === "0") { if ($('input[name="direction"]:checked').val() === "0") {

@ -5,7 +5,7 @@
<h2>{{_('No Results Found')}} {{adv_searchterm}}</h2> <h2>{{_('No Results Found')}} {{adv_searchterm}}</h2>
<p>{{_('Search Term:')}} {{adv_searchterm}}</p> <p>{{_('Search Term:')}} {{adv_searchterm}}</p>
{% else %} {% else %}
<h2>{{entries|length}} {{_('Results for:')}} {{adv_searchterm}}</h2> <h2>{{result_count}} {{_('Results for:')}} {{adv_searchterm}}</h2>
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
{% if g.user.shelf.all() or g.shelves_access %} {% if g.user.shelf.all() or g.shelves_access %}
<div id="shelf-actions" class="btn-toolbar" role="toolbar"> <div id="shelf-actions" class="btn-toolbar" role="toolbar">
@ -25,18 +25,14 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<!--div class="filterheader hidden-xs hidden-sm"--><!-- ToDo: Implement filter for search results --> <div class="filterheader hidden-xs hidden-sm"><!-- ToDo: Implement filter for search results -->
<!--a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='new')}}"><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='new', query=query)}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='old')}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='old', query=query)}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
<a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a> <a id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='abc', query=query)}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
<a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a> <a id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='zyx', query=query)}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
<a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a> <a id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='pubnew', query=query)}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
<a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a> <a id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='pubold', query=query)}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
</div> </div>
<div class="btn-group character" role="group">
<a id="no_shelf" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort='pubold')}}"><span class="glyphicon glyphicon-list"></span><b>?</b></a>
<div id="all" class="btn btn-primary">{{_('All')}}</div>
</div-->
{% endif %} {% endif %}
<div class="row"> <div class="row">
@ -59,7 +55,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -67,7 +63,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for format in entry.data %} {% for format in entry.data %}

@ -1,7 +1,7 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<div class="col-md-10 col-lg-6"> <div class="col-md-10 col-lg-6">
<form role="form" id="search" action="{{ url_for('web.advanced_search') }}" method="GET"> <form role="form" id="search" action="{{ url_for('web.advanced_search_form') }}" method="POST">
<div class="form-group"> <div class="form-group">
<label for="book_title">{{_('Book Title')}}</label> <label for="book_title">{{_('Book Title')}}</label>
<input type="text" class="form-control" name="book_title" id="book_title" value=""> <input type="text" class="form-control" name="book_title" id="book_title" value="">

@ -31,7 +31,7 @@
{% if not loop.first %} {% if not loop.first %}
<span class="author-hidden-divider">&amp;</span> <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name author-hidden" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %} {% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a> <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %} {% endif %}
@ -39,7 +39,7 @@
{% if not loop.first %} {% if not loop.first %}
<span>&amp;</span> <span>&amp;</span>
{% endif %} {% endif %}
<a class="author-name" href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> <a class="author-name" href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>

@ -37,7 +37,7 @@
<p class="title">{{entry.title|shortentitle}}</p> <p class="title">{{entry.title|shortentitle}}</p>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('web.books_list', data='author', sort='new', book_id=author.id) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('web.books_list', data='author', sort_param='new', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;
{% endif %} {% endif %}

@ -140,20 +140,8 @@
{% endif %} {% endif %}
</div> </div>
</form> </form>
{% if downloads %}
<div class="col-sm-12">
<h2>{{_('Recent Downloads')}}</h2>
{% for entry in downloads %}
<div class="col-sm-2">
<a class="pull-left" href="{{ url_for('web.show_book', book_id=entry.id) }}">
<img class="media-object cover-small" src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="...">
</a>
</div>
{% endfor %}
</div>
{% endif %}
</div> </div>
<div class="modal fade" id="modal_kobo_token" tabindex="-1" role="dialog" aria-labelledby="kobo_tokenModalLabel"> <div class="modal fade" id="modal_kobo_token" tabindex="-1" role="dialog" aria-labelledby="kobo_tokenModalLabel">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">

@ -23,11 +23,12 @@ import sys
import datetime import datetime
import itertools import itertools
import uuid import uuid
from flask import session as flask_session
from binascii import hexlify from binascii import hexlify
from flask import g from flask import g
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import AnonymousUserMixin from flask_login import AnonymousUserMixin, current_user
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
try: try:
from flask_dance.consumer.backend.sqla import OAuthConsumerMixin from flask_dance.consumer.backend.sqla import OAuthConsumerMixin
@ -41,8 +42,9 @@ except ImportError:
oauth_support = False oauth_support = False
from sqlalchemy import create_engine, exc, exists, event from sqlalchemy import create_engine, exc, exists, event
from sqlalchemy import Column, ForeignKey from sqlalchemy import Column, ForeignKey
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float, JSON
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.orm import backref, relationship, sessionmaker, Session from sqlalchemy.orm import backref, relationship, sessionmaker, Session
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
@ -52,6 +54,7 @@ from . import constants
session = None session = None
app_DB_path = None app_DB_path = None
Base = declarative_base() Base = declarative_base()
searched_ids = {}
def get_sidebar_config(kwargs=None): def get_sidebar_config(kwargs=None):
@ -68,13 +71,17 @@ def get_sidebar_config(kwargs=None):
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot", "visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
"show_text": _('Show Hot Books'), "config_show": True}) "show_text": _('Show Hot Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
sidebar.append( sidebar.append(
{"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated", {"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated",
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated", "visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
"show_text": _('Show Top Rated Books'), "config_show": True}) "show_text": _('Show Top Rated Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read", sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "read", "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
"show_text": _('Show read and unread'), "config_show": content}) "page": "read", "show_text": _('Show read and unread'), "config_show": content})
sidebar.append( sidebar.append(
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread", {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread", "visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
@ -109,14 +116,21 @@ def get_sidebar_config(kwargs=None):
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived", {"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived", "visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
"show_text": _('Show archived books'), "config_show": content}) "show_text": _('Show archived books'), "config_show": content})
'''sidebar.append( sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_list', "id": "list", {"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list", "visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
"show_text": _('Show Books List'), "config_show": content})''' "show_text": _('Show Books List'), "config_show": content})
return sidebar return sidebar
def store_ids(result):
ids = list()
for element in result:
ids.append(element.id)
searched_ids[current_user.id] = ids
class UserBase: class UserBase:
@property @property
@ -191,6 +205,25 @@ class UserBase:
mct = self.allowed_column_value or "" mct = self.allowed_column_value or ""
return [t.strip() for t in mct.split(",")] return [t.strip() for t in mct.split(",")]
def get_view_property(self, page, property):
if not self.view_settings.get(page):
return None
return self.view_settings[page].get(property)
def set_view_property(self, page, property, value):
if not self.view_settings.get(page):
self.view_settings[page] = dict()
self.view_settings[page][property] = value
try:
flag_modified(self, "view_settings")
except AttributeError:
pass
try:
session.commit()
except (exc.OperationalError, exc.InvalidRequestError):
session.rollback()
# ToDo: Error message
def __repr__(self): def __repr__(self):
return '<User %r>' % self.nickname return '<User %r>' % self.nickname
@ -218,7 +251,8 @@ class User(UserBase, Base):
denied_column_value = Column(String, default="") denied_column_value = Column(String, default="")
allowed_column_value = Column(String, default="") allowed_column_value = Column(String, default="")
remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic') remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic')
series_view = Column(String(10), default="list") view_settings = Column(JSON, default={})
if oauth_support: if oauth_support:
@ -259,7 +293,11 @@ class Anonymous(AnonymousUserMixin, UserBase):
self.allowed_tags = data.allowed_tags self.allowed_tags = data.allowed_tags
self.denied_column_value = data.denied_column_value self.denied_column_value = data.denied_column_value
self.allowed_column_value = data.allowed_column_value self.allowed_column_value = data.allowed_column_value
self.series_view = data.series_view self.view_settings = data.view_settings
# Initialize flask_session once
if 'view' not in flask_session:
flask_session['view']={}
def role_admin(self): def role_admin(self):
return False return False
@ -276,6 +314,16 @@ class Anonymous(AnonymousUserMixin, UserBase):
def is_authenticated(self): def is_authenticated(self):
return False return False
def get_view_property(self, page, prop):
if not flask_session['view'].get(page):
return None
return flask_session['view'][page].get(prop)
def set_view_property(self, page, prop, value):
if not flask_session['view'].get(page):
flask_session['view'][page] = dict()
flask_session['view'][page][prop] = value
# Baseclass representing Shelfs in calibre-web in app.db # Baseclass representing Shelfs in calibre-web in app.db
class Shelf(Base): class Shelf(Base):
@ -567,10 +615,11 @@ def migrate_Database(session):
conn.execute("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''") conn.execute("ALTER TABLE user ADD column `allowed_column_value` String DEFAULT ''")
session.commit() session.commit()
try: try:
session.query(exists().where(User.series_view)).scalar() session.query(exists().where(User.view_settings)).scalar()
except exc.OperationalError: except exc.OperationalError:
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("ALTER TABLE user ADD column `series_view` VARCHAR(10) DEFAULT 'list'") conn.execute("ALTER TABLE user ADD column `view_settings` VARCHAR(10) DEFAULT '{}'")
session.commit()
if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \ if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() \
is None: is None:
@ -591,11 +640,12 @@ def migrate_Database(session):
"locale VARCHAR(2)," "locale VARCHAR(2),"
"sidebar_view INTEGER," "sidebar_view INTEGER,"
"default_language VARCHAR(3)," "default_language VARCHAR(3),"
"series_view VARCHAR(10)," # "series_view VARCHAR(10),"
"view_settings VARCHAR,"
"UNIQUE (nickname)," "UNIQUE (nickname),"
"UNIQUE (email))") "UNIQUE (email))")
conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale,"
"sidebar_view, default_language, series_view) " "sidebar_view, default_language, view_settings) "
"SELECT id, nickname, email, role, password, kindle_mail, locale," "SELECT id, nickname, email, role, password, kindle_mail, locale,"
"sidebar_view, default_language FROM user") "sidebar_view, default_language FROM user")
# delete old user table and rename new user_id table to user: # delete old user table and rename new user_id table to user:

@ -227,6 +227,7 @@ class Updater(threading.Thread):
os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep + 'client_secrets.json', 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 + '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 + 'access.log', os.sep + 'access.log1', os.sep + 'access.log2',
os.sep + '.calibre-web.log.swp'
) )
additional_path = self.is_venv() additional_path = self.is_venv()
if additional_path: if additional_path:

@ -30,17 +30,22 @@ import traceback
import binascii import binascii
import re import re
from babel import Locale as LC
from babel.dates import format_date from babel.dates import format_date
from babel import Locale as LC
from babel.core import UnknownLocaleError from babel.core import UnknownLocaleError
from flask import Blueprint from flask import Blueprint, jsonify
from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for
from flask import session as flask_session
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_login import login_user, logout_user, login_required, current_user, confirm_login from flask_login import login_user, logout_user, login_required, current_user, confirm_login
from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_ from sqlalchemy.sql.expression import text, func, true, false, not_, and_, or_
from sqlalchemy.orm.attributes import flag_modified
from werkzeug.exceptions import default_exceptions, InternalServerError from werkzeug.exceptions import default_exceptions, InternalServerError
from sqlalchemy.sql.functions import coalesce from sqlalchemy.sql.functions import coalesce
from .services.worker import WorkerThread
try: try:
from werkzeug.exceptions import FailedDependency from werkzeug.exceptions import FailedDependency
except ImportError: except ImportError:
@ -48,11 +53,11 @@ except ImportError:
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from . import constants, logger, isoLanguages, services, worker, cli from . import constants, logger, isoLanguages, services
from . import searched_ids, lm, babel, db, ub, config, get_locale, app from . import lm, babel, db, ub, config, get_locale, app
from . import calibre_db from . import calibre_db
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
from .helper import check_valid_domain, render_task_status, json_serial, \ from .helper import check_valid_domain, render_task_status, \
get_cc_columns, get_book_cover, get_download_link, send_mail, generate_random_password, \ 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 send_registration_mail, check_send_to_kindle, check_read_formats, tags_filters, reset_password
from .pagination import Pagination from .pagination import Pagination
@ -230,9 +235,8 @@ def admin_required(f):
def unconfigured(f): def unconfigured(f):
""" """
Checks if current_user.role == 1 Checks if calibre-web instance is not configured
""" """
@wraps(f) @wraps(f)
def inner(*args, **kwargs): def inner(*args, **kwargs):
if not config.db_configured: if not config.db_configured:
@ -285,14 +289,6 @@ def edit_required(f):
# ################################### Helper functions ################################################################ # ################################### Helper functions ################################################################
# Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar = ub.get_sidebar_config(kwargs)
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
accept=constants.EXTENSIONS_UPLOAD,
*args, **kwargs)
@web.before_app_request @web.before_app_request
def before_request(): def before_request():
if current_user.is_authenticated: if current_user.is_authenticated:
@ -384,12 +380,8 @@ def import_ldap_users():
@web.route("/ajax/emailstat") @web.route("/ajax/emailstat")
@login_required @login_required
def get_email_status_json(): def get_email_status_json():
tasks = worker.get_taskstatus() tasks = WorkerThread.getInstance().tasks
answer = render_task_status(tasks) return jsonify(render_task_status(tasks))
js = json.dumps(answer, default=json_serial)
response = make_response(js)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@web.route("/ajax/bookmark/<int:book_id>/<book_format>", methods=['POST']) @web.route("/ajax/bookmark/<int:book_id>/<book_format>", methods=['POST'])
@ -472,22 +464,17 @@ def toggle_archived(book_id):
@web.route("/ajax/view", methods=["POST"]) @web.route("/ajax/view", methods=["POST"])
@login_required @login_required_if_no_ano
def update_view(): def update_view():
to_save = request.form.to_dict() to_save = request.get_json()
allowed_view = ['grid', 'list']
if "series_view" in to_save and to_save["series_view"] in allowed_view:
current_user.series_view = to_save["series_view"]
else:
log.error("Invalid request received: %r %r", request, to_save)
return "Invalid request", 400
try: try:
ub.session.commit() for element in to_save:
except InvalidRequestError: for param in to_save[element]:
log.error("Invalid request received: %r ", request, ) current_user.set_view_property(element, param, to_save[element][param])
except Exception as e:
log.error("Could not save view_settings: %r %r: e", request, to_save, e)
return "Invalid request", 400 return "Invalid request", 400
return "", 200 return "1", 200
''' '''
@ -611,25 +598,20 @@ def get_matching_tags():
return json_dumps return json_dumps
# ################################### View Books list ################################################################## # Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar = ub.get_sidebar_config(kwargs)
@web.route("/", defaults={'page': 1}) return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
@web.route('/page/<int:page>') accept=constants.EXTENSIONS_UPLOAD,
@login_required_if_no_ano *args, **kwargs)
def index(page):
entries, random, pagination = calibre_db.fill_indexpage(page, db.Books, True, [db.Books.timestamp.desc()])
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Recently Added Books"), page="root")
@web.route('/<data>/<sort>', defaults={'page': 1, 'book_id': "1"}) def render_books_list(data, sort, book_id, page):
@web.route('/<data>/<sort>/', defaults={'page': 1, 'book_id': "1"})
@web.route('/<data>/<sort>/<book_id>', defaults={'page': 1})
@web.route('/<data>/<sort>/<book_id>/<int:page>')
@login_required_if_no_ano
def books_list(data, sort, book_id, page):
order = [db.Books.timestamp.desc()] order = [db.Books.timestamp.desc()]
if sort == 'stored':
sort = current_user.get_view_property(data, 'stored')
else:
current_user.set_view_property(data, 'stored', sort)
if sort == 'pubnew': if sort == 'pubnew':
order = [db.Books.pubdate.desc()] order = [db.Books.pubdate.desc()]
if sort == 'pubold': if sort == 'pubold':
@ -645,7 +627,7 @@ def books_list(data, sort, book_id, page):
if data == "rated": if data == "rated":
if current_user.check_visibility(constants.SIDEBAR_BEST_RATED): if current_user.check_visibility(constants.SIDEBAR_BEST_RATED):
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.ratings.any(db.Ratings.rating > 9), db.Books.ratings.any(db.Ratings.rating > 9),
order) order)
@ -655,7 +637,7 @@ def books_list(data, sort, book_id, page):
abort(404) abort(404)
elif data == "discover": elif data == "discover":
if current_user.check_visibility(constants.SIDEBAR_RANDOM): if current_user.check_visibility(constants.SIDEBAR_RANDOM):
entries, __, pagination = calibre_db.fill_indexpage(page, db.Books, True, [func.randomblob(2)]) entries, __, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, [func.randomblob(2)])
pagination = Pagination(1, config.config_books_per_page, config.config_books_per_page) 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('discover.html', entries=entries, pagination=pagination, id=book_id,
title=_(u"Discover (Random Books)"), page="discover") title=_(u"Discover (Random Books)"), page="discover")
@ -667,6 +649,8 @@ def books_list(data, sort, book_id, page):
return render_read_books(page, True, order=order) return render_read_books(page, True, order=order)
elif data == "hot": elif data == "hot":
return render_hot_books(page) return render_hot_books(page)
elif data == "download":
return render_downloaded_books(page, order)
elif data == "author": elif data == "author":
return render_author_books(page, book_id, order) return render_author_books(page, book_id, order)
elif data == "publisher": elif data == "publisher":
@ -683,10 +667,19 @@ def books_list(data, sort, book_id, page):
return render_language_books(page, book_id, order) return render_language_books(page, book_id, order)
elif data == "archived": elif data == "archived":
return render_archived_books(page, order) return render_archived_books(page, order)
else: elif data == "search":
entries, random, pagination = calibre_db.fill_indexpage(page, db.Books, True, order) term = (request.args.get('query') or '')
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'])
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)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Books"), page="newest") title=_(u"Books"), page=website)
def render_hot_books(page): def render_hot_books(page):
@ -718,8 +711,44 @@ def render_hot_books(page):
abort(404) abort(404)
def render_author_books(page, author_id, order): def render_downloaded_books(page, order):
if current_user.check_visibility(constants.SIDEBAR_DOWNLOAD):
# order = order or []
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()
# off = int(int(config.config_books_per_page) * (page - 1))
'''entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books,
db_filter,
order,
ub.ReadBook, db.Books.id==ub.ReadBook.book_id)'''
entries, __, pagination = calibre_db.fill_indexpage(page, entries, __, pagination = calibre_db.fill_indexpage(page,
0,
db.Books,
ub.Downloads.user_id == int(current_user.id),
order,
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)
return render_title_template('index.html',
random=random,
entries=entries,
pagination=pagination,
title=_(u"Downloaded books by %(user)s",user=current_user.nickname),
page="download")
else:
abort(404)
def render_author_books(page, author_id, order):
entries, __, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.authors.any(db.Authors.id == author_id), db.Books.authors.any(db.Authors.id == author_id),
[order[0], db.Series.name, db.Books.series_index], [order[0], db.Series.name, db.Books.series_index],
@ -747,7 +776,7 @@ def render_author_books(page, author_id, order):
def render_publisher_books(page, book_id, order): def render_publisher_books(page, book_id, order):
publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first() publisher = calibre_db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first()
if publisher: if publisher:
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.publishers.any(db.Publishers.id == book_id), db.Books.publishers.any(db.Publishers.id == book_id),
[db.Series.name, order[0], db.Books.series_index], [db.Series.name, order[0], db.Books.series_index],
@ -762,10 +791,10 @@ def render_publisher_books(page, book_id, order):
def render_series_books(page, book_id, order): def render_series_books(page, book_id, order):
name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first() name = calibre_db.session.query(db.Series).filter(db.Series.id == book_id).first()
if name: if name:
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.series.any(db.Series.id == book_id), db.Books.series.any(db.Series.id == book_id),
[db.Books.series_index, order[0]]) [order[0]])
return render_title_template('index.html', random=random, pagination=pagination, entries=entries, id=book_id, 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") title=_(u"Series: %(serie)s", serie=name.name), page="series")
else: else:
@ -774,7 +803,7 @@ def render_series_books(page, book_id, order):
def render_ratings_books(page, book_id, order): def render_ratings_books(page, book_id, order):
name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first() name = calibre_db.session.query(db.Ratings).filter(db.Ratings.id == book_id).first()
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.ratings.any(db.Ratings.id == book_id), db.Books.ratings.any(db.Ratings.id == book_id),
[db.Books.timestamp.desc(), order[0]]) [db.Books.timestamp.desc(), order[0]])
@ -788,7 +817,7 @@ def render_ratings_books(page, book_id, order):
def render_formats_books(page, book_id, order): def render_formats_books(page, book_id, order):
name = calibre_db.session.query(db.Data).filter(db.Data.format == book_id.upper()).first() name = calibre_db.session.query(db.Data).filter(db.Data.format == book_id.upper()).first()
if name: if name:
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.data.any(db.Data.format == book_id.upper()), db.Books.data.any(db.Data.format == book_id.upper()),
[db.Books.timestamp.desc(), order[0]]) [db.Books.timestamp.desc(), order[0]])
@ -801,7 +830,7 @@ def render_formats_books(page, book_id, order):
def render_category_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() name = calibre_db.session.query(db.Tags).filter(db.Tags.id == book_id).first()
if name: if name:
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.tags.any(db.Tags.id == book_id), db.Books.tags.any(db.Tags.id == book_id),
[order[0], db.Series.name, db.Books.series_index], [order[0], db.Series.name, db.Books.series_index],
@ -821,27 +850,210 @@ def render_language_books(page, name, order):
lang_name = _(isoLanguages.get(part3=name).name) lang_name = _(isoLanguages.get(part3=name).name)
except KeyError: except KeyError:
abort(404) abort(404)
entries, random, pagination = calibre_db.fill_indexpage(page, entries, random, pagination = calibre_db.fill_indexpage(page, 0,
db.Books, db.Books,
db.Books.languages.any(db.Languages.lang_code == name), db.Books.languages.any(db.Languages.lang_code == name),
[db.Books.timestamp.desc(), order[0]]) [db.Books.timestamp.desc(), order[0]])
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name, return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name,
title=_(u"Language: %(name)s", name=lang_name), page="language") title=_(u"Language: %(name)s", name=lang_name), page="language")
def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs):
order = order or []
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,
order,
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,
order,
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)
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
if as_xml:
return entries, pagination
else:
if are_read:
name = _(u'Read Books') + ' (' + str(pagination.total_count) + ')'
pagename = "read"
else:
name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
pagename = "unread"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename)
def render_archived_books(page, order):
order = order 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)
entries, random, pagination = calibre_db.fill_indexpage_with_archived_books(page, 0,
db.Books,
archived_filter,
order,
allow_show_archived=True)
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
pagename = "archived"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename)
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()
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, title=_(u"search"), cc=cc, page="advsearch")
def render_search_results(term, offset=None, order=None, limit=None):
entries, result_count, pagination = calibre_db.get_search_results(term, offset, order, limit)
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")
# ################################### View Books list ##################################################################
'''@web.route("/table") @web.route("/", defaults={'page': 1})
@web.route('/page/<int:page>')
@login_required_if_no_ano @login_required_if_no_ano
def index(page):
sort_param = (request.args.get('sort') or 'stored').lower()
return render_books_list("newest", sort_param, 1, page)
@web.route('/<data>/<sort_param>', defaults={'page': 1, 'book_id': "1"})
@web.route('/<data>/<sort_param>/', defaults={'page': 1, 'book_id': "1"})
@web.route('/<data>/<sort_param>/<book_id>', defaults={'page': 1})
@web.route('/<data>/<sort_param>/<book_id>/<int:page>')
@login_required_if_no_ano
def books_list(data, sort_param, book_id, page):
return render_books_list(data, sort_param, book_id, page)
@web.route("/table")
@login_required
def books_table(): def books_table():
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, id=name, visibility = current_user.view_settings.get('table', {})
title=_(u"Language: %(name)s", name=lang_name), page="language")''' return render_title_template('book_table.html', title=_(u"Books list"), page="book_table",
visiblility=visibility)
@web.route("/ajax/listbooks")
@login_required
def list_books():
off = request.args.get("offset") or 0
limit = request.args.get("limit") or config.config_books_per_page
# sort = request.args.get("sort")
if request.args.get("order") == 'desc':
order = [db.Books.timestamp.desc()]
else:
order = [db.Books.timestamp.asc()]
search = request.args.get("search")
total_count = calibre_db.session.query(db.Books).count()
if search:
entries, filtered_count, pagination = calibre_db.get_search_results(search, off, order, limit)
else:
entries, __, __ = calibre_db.fill_indexpage((int(off) / (int(limit)) + 1), limit, db.Books, True, order)
filtered_count = total_count
for entry in entries:
for index in range(0, len(entry.languages)):
try:
entry.languages[index].language_name = LC.parse(entry.languages[index].lang_code)\
.get_language_name(get_locale())
except UnknownLocaleError:
entry.languages[index].language_name = _(
isoLanguages.get(part3=entry.languages[index].lang_code).name)
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": entries}
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
response = make_response(js_list)
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@web.route("/ajax/table_settings", methods=['POST'])
@login_required
def update_table_settings():
# vals = request.get_json()
# ToDo: Save table settings
current_user.view_settings['table'] = json.loads(request.data)
try:
try:
flag_modified(current_user, "view_settings")
except AttributeError:
pass
ub.session.commit()
except InvalidRequestError:
log.error("Invalid request received: %r ", request, )
return "Invalid request", 400
return ""
@web.route("/author") @web.route("/author")
@login_required_if_no_ano @login_required_if_no_ano
def author_list(): def author_list():
if current_user.check_visibility(constants.SIDEBAR_AUTHOR): if current_user.check_visibility(constants.SIDEBAR_AUTHOR):
if current_user.get_view_property('author', 'dir') == 'desc':
order = db.Authors.sort.desc()
else:
order = db.Authors.sort.asc()
entries = calibre_db.session.query(db.Authors, func.count('books_authors_link.book').label('count')) \ 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()) \ .join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_authors_link.author')).order_by(db.Authors.sort).all() .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')) \ 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()) \ .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() .group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all()
@ -856,10 +1068,14 @@ def author_list():
@web.route("/publisher") @web.route("/publisher")
@login_required_if_no_ano @login_required_if_no_ano
def publisher_list(): def publisher_list():
if current_user.get_view_property('publisher', 'dir') == 'desc':
order = db.Publishers.name.desc()
else:
order = db.Publishers.name.asc()
if current_user.check_visibility(constants.SIDEBAR_PUBLISHER): if current_user.check_visibility(constants.SIDEBAR_PUBLISHER):
entries = calibre_db.session.query(db.Publishers, func.count('books_publishers_link.book').label('count')) \ 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()) \ .join(db.books_publishers_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_publishers_link.publisher')).order_by(db.Publishers.name).all() .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')) \ 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()) \ .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() .group_by(func.upper(func.substr(db.Publishers.name, 1, 1))).all()
@ -873,10 +1089,14 @@ def publisher_list():
@login_required_if_no_ano @login_required_if_no_ano
def series_list(): def series_list():
if current_user.check_visibility(constants.SIDEBAR_SERIES): if current_user.check_visibility(constants.SIDEBAR_SERIES):
if current_user.series_view == 'list': if current_user.get_view_property('series', 'dir') == 'desc':
order = db.Series.sort.desc()
else:
order = db.Series.sort.asc()
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')) \ 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()) \ .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series')).order_by(db.Series.sort).all() .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')) \ 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()) \ .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() .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
@ -885,7 +1105,7 @@ def series_list():
else: else:
entries = calibre_db.session.query(db.Books, func.count('books_series_link').label('count')) \ entries = calibre_db.session.query(db.Books, func.count('books_series_link').label('count')) \
.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(db.Series.sort).all() .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')) \ 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()) \ .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() .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
@ -900,10 +1120,14 @@ def series_list():
@login_required_if_no_ano @login_required_if_no_ano
def ratings_list(): def ratings_list():
if current_user.check_visibility(constants.SIDEBAR_RATING): if current_user.check_visibility(constants.SIDEBAR_RATING):
if current_user.get_view_property('ratings', 'dir') == 'desc':
order = db.Ratings.rating.desc()
else:
order = db.Ratings.rating.asc()
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'), 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()) \ .join(db.books_ratings_link).join(db.Books).filter(calibre_db.common_filters()) \
.group_by(text('books_ratings_link.rating')).order_by(db.Ratings.rating).all() .group_by(text('books_ratings_link.rating')).order_by(order).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(), return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
title=_(u"Ratings list"), page="ratingslist", data="ratings") title=_(u"Ratings list"), page="ratingslist", data="ratings")
else: else:
@ -914,11 +1138,15 @@ def ratings_list():
@login_required_if_no_ano @login_required_if_no_ano
def formats_list(): def formats_list():
if current_user.check_visibility(constants.SIDEBAR_FORMAT): if current_user.check_visibility(constants.SIDEBAR_FORMAT):
if current_user.get_view_property('ratings', 'dir') == 'desc':
order = db.Data.format.desc()
else:
order = db.Data.format.asc()
entries = calibre_db.session.query(db.Data, entries = calibre_db.session.query(db.Data,
func.count('data.book').label('count'), func.count('data.book').label('count'),
db.Data.format.label('format')) \ db.Data.format.label('format')) \
.join(db.Books).filter(calibre_db.common_filters()) \ .join(db.Books).filter(calibre_db.common_filters()) \
.group_by(db.Data.format).order_by(db.Data.format).all() .group_by(db.Data.format).order_by(order).all()
return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(), return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=list(),
title=_(u"File formats list"), page="formatslist", data="formats") title=_(u"File formats list"), page="formatslist", data="formats")
else: else:
@ -958,8 +1186,12 @@ def language_overview():
@login_required_if_no_ano @login_required_if_no_ano
def category_list(): def category_list():
if current_user.check_visibility(constants.SIDEBAR_CATEGORY): if current_user.check_visibility(constants.SIDEBAR_CATEGORY):
if current_user.get_view_property('category', 'dir') == 'desc':
order = db.Tags.name.desc()
else:
order = db.Tags.name.asc()
entries = calibre_db.session.query(db.Tags, func.count('books_tags_link.book').label('count')) \ 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(db.Tags.name).filter(calibre_db.common_filters()) \ .join(db.books_tags_link).join(db.Books).order_by(order).filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag')).all() .group_by(text('books_tags_link.tag')).all()
charlist = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('char')) \ 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()) \ .join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters()) \
@ -977,7 +1209,7 @@ def category_list():
@login_required @login_required
def get_tasks_status(): def get_tasks_status():
# if current user admin, show all email, otherwise only own emails # if current user admin, show all email, otherwise only own emails
tasks = worker.get_taskstatus() tasks = WorkerThread.getInstance().tasks
answer = render_task_status(tasks) answer = render_task_status(tasks)
return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks") return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks")
@ -990,55 +1222,51 @@ def reconnect():
# ################################### Search functions ################################################################ # ################################### Search functions ################################################################
@web.route("/search", methods=["GET"]) @web.route("/search", methods=["GET"])
@login_required_if_no_ano @login_required_if_no_ano
def search(): def search():
term = request.args.get("query") term = request.args.get("query")
if term: if term:
entries = calibre_db.get_search_results(term) return render_search_results(term, 0, None, config.config_books_per_page)
ids = list()
for element in entries:
ids.append(element.id)
searched_ids[current_user.id] = ids
return render_title_template('search.html',
searchterm=term,
adv_searchterm=term,
entries=entries,
title=_(u"Search"),
page="search")
else: else:
return render_title_template('search.html', return render_title_template('search.html',
searchterm="", searchterm="",
result_count=0,
title=_(u"Search"), title=_(u"Search"),
page="search") page="search")
@web.route("/advanced_search", methods=['GET']) @web.route("/advanced_search", methods=['POST'])
@login_required_if_no_ano @login_required_if_no_ano
def advanced_search(): def advanced_search():
# Build custom columns names term = request.form
return render_adv_search_results(term, 0, None, config.config_books_per_page)
def render_adv_search_results(term, offset=None, order=None, limit=None):
order = order or [db.Books.sort]
pagination = None
cc = get_cc_columns(filter_config_custom_read=True) cc = get_cc_columns(filter_config_custom_read=True)
calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase) calibre_db.session.connection().connection.connection.create_function("lower", 1, db.lcase)
q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True)).order_by(db.Books.sort) q = calibre_db.session.query(db.Books).filter(calibre_db.common_filters(True))
include_tag_inputs = request.args.getlist('include_tag') include_tag_inputs = request.form.getlist('include_tag')
exclude_tag_inputs = request.args.getlist('exclude_tag') exclude_tag_inputs = request.form.getlist('exclude_tag')
include_series_inputs = request.args.getlist('include_serie') include_series_inputs = request.form.getlist('include_serie')
exclude_series_inputs = request.args.getlist('exclude_serie') exclude_series_inputs = request.form.getlist('exclude_serie')
include_languages_inputs = request.args.getlist('include_language') include_languages_inputs = request.form.getlist('include_language')
exclude_languages_inputs = request.args.getlist('exclude_language') exclude_languages_inputs = request.form.getlist('exclude_language')
include_extension_inputs = request.args.getlist('include_extension') include_extension_inputs = request.form.getlist('include_extension')
exclude_extension_inputs = request.args.getlist('exclude_extension') exclude_extension_inputs = request.form.getlist('exclude_extension')
author_name = request.args.get("author_name") author_name = term.get("author_name")
book_title = request.args.get("book_title") book_title = term.get("book_title")
publisher = request.args.get("publisher") publisher = term.get("publisher")
pub_start = request.args.get("Publishstart") pub_start = term.get("Publishstart")
pub_end = request.args.get("Publishend") pub_end = term.get("Publishend")
rating_low = request.args.get("ratinghigh") rating_low = term.get("ratinghigh")
rating_high = request.args.get("ratinglow") rating_high = term.get("ratinglow")
description = request.args.get("comment") description = term.get("comment")
if author_name: if author_name:
author_name = author_name.strip().lower().replace(',', '|') author_name = author_name.strip().lower().replace(',', '|')
if book_title: if book_title:
@ -1049,8 +1277,8 @@ def advanced_search():
searchterm = [] searchterm = []
cc_present = False cc_present = False
for c in cc: for c in cc:
if request.args.get('custom_column_' + str(c.id)): if request.form.get('custom_column_' + str(c.id)):
searchterm.extend([(u"%s: %s" % (c.name, request.args.get('custom_column_' + str(c.id))))]) searchterm.extend([(u"%s: %s" % (c.name, request.form.get('custom_column_' + str(c.id))))])
cc_present = True cc_present = True
if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \ if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \
@ -1089,8 +1317,8 @@ def advanced_search():
searchterm.extend(ext for ext in exclude_extension_inputs) searchterm.extend(ext for ext in exclude_extension_inputs)
# handle custom columns # handle custom columns
for c in cc: for c in cc:
if request.args.get('custom_column_' + str(c.id)): if request.form.get('custom_column_' + str(c.id)):
searchterm.extend([(u"%s: %s" % (c.name, request.args.get('custom_column_' + str(c.id))))]) searchterm.extend([(u"%s: %s" % (c.name, request.form.get('custom_column_' + str(c.id))))])
searchterm = " + ".join(filter(None, searchterm)) searchterm = " + ".join(filter(None, searchterm))
q = q.filter() q = q.filter()
if author_name: if author_name:
@ -1133,7 +1361,7 @@ def advanced_search():
# search custom culumns # search custom culumns
for c in cc: for c in cc:
custom_query = request.args.get('custom_column_' + str(c.id)) custom_query = request.form.get('custom_column_' + str(c.id))
if custom_query != '' and custom_query is not None: if custom_query != '' and custom_query is not None:
if c.datatype == 'bool': if c.datatype == 'bool':
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any( q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
@ -1147,107 +1375,34 @@ def advanced_search():
else: else:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any( q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%"))) func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%")))
q = q.all() q = q.order_by(*order).all()
ids = list() flask_session['query'] = json.dumps(term)
for element in q: ub.store_ids(q)
ids.append(element.id) # entries, result_count, pagination = calibre_db.get_search_results(term, offset, order, limit)
searched_ids[current_user.id] = ids result_count = len(q)
return render_title_template('search.html', adv_searchterm=searchterm, if offset != None and limit != None:
entries=q, title=_(u"search"), page="search") offset = int(offset)
# prepare data for search-form limit_all = offset + int(limit)
tags = calibre_db.session.query(db.Tags)\ pagination = Pagination((offset / (int(limit)) + 1), limit, result_count)
.join(db.books_tags_link)\ else:
.join(db.Books)\ offset = 0
.filter(calibre_db.common_filters()) \ limit_all = result_count
.group_by(text('books_tags_link.tag'))\ return render_title_template('search.html',
.order_by(db.Tags.name).all() adv_searchterm=searchterm,
series = calibre_db.session.query(db.Series)\ pagination=pagination,
.join(db.books_series_link)\ entries=q[offset:limit_all],
.join(db.Books)\ result_count=result_count,
.filter(calibre_db.common_filters()) \ title=_(u"search"), page="advsearch")
.group_by(text('books_series_link.series'))\
.order_by(db.Series.name)\
.filter(calibre_db.common_filters()).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, title=_(u"search"), cc=cc, page="advsearch")
def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs):
order = order or []
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,
db.Books,
db_filter,
order,
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,
db.Books,
db_filter,
order,
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)
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
if as_xml:
return entries, pagination
else:
if are_read:
name = _(u'Read Books') + ' (' + str(pagination.total_count) + ')'
pagename = "read"
else:
name = _(u'Unread Books') + ' (' + str(pagination.total_count) + ')'
pagename = "unread"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename)
def render_archived_books(page, order):
order = order 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)
entries, random, pagination = calibre_db.fill_indexpage_with_archived_books(page, @web.route("/advanced_search", methods=['GET'])
db.Books, @login_required_if_no_ano
archived_filter, def advanced_search_form():
order, # Build custom columns names
allow_show_archived=True) cc = get_cc_columns(filter_config_custom_read=True)
return render_prepare_search_form(cc)
name = _(u'Archived Books') + ' (' + str(len(archived_book_ids)) + ')'
pagename = "archived"
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=name, page=pagename)
# ################################### Download/Send ################################################################## # ################################### Download/Send ##################################################################
@ -1551,21 +1706,24 @@ def token_verified():
@web.route("/me", methods=["GET", "POST"]) @web.route("/me", methods=["GET", "POST"])
@login_required @login_required
def profile(): def profile():
downloads = list() # downloads = list()
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language()
translations = babel.list_translations() + [LC('en')] translations = babel.list_translations() + [LC('en')]
kobo_support = feature_support['kobo'] and config.config_kobo_sync kobo_support = feature_support['kobo'] and config.config_kobo_sync
if feature_support['oauth']: if feature_support['oauth'] and config.config_login_type == 2:
oauth_status = get_oauth_status() oauth_status = get_oauth_status()
local_oauth_check = oauth_check
else: else:
oauth_status = None oauth_status = None
local_oauth_check = {}
'''entries, __, pagination = calibre_db.fill_indexpage(page,
0,
db.Books,
ub.Downloads.user_id == int(current_user.id), # True,
[],
ub.Downloads, db.Books.id == ub.Downloads.book_id)'''
for book in current_user.downloads:
downloadBook = calibre_db.get_book(book.book_id)
if downloadBook:
downloads.append(downloadBook)
else:
ub.delete_download(book.book_id)
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() to_save = request.form.to_dict()
current_user.random_books = 0 current_user.random_books = 0
@ -1579,10 +1737,11 @@ def profile():
if "email" in to_save and to_save["email"] != current_user.email: if "email" in to_save and to_save["email"] != current_user.email:
if config.config_public_reg and not check_valid_domain(to_save["email"]): if config.config_public_reg and not check_valid_domain(to_save["email"]):
flash(_(u"E-mail is not from valid domain"), category="error") flash(_(u"E-mail is not from valid domain"), category="error")
return render_title_template("user_edit.html", content=current_user, downloads=downloads, return render_title_template("user_edit.html", content=current_user,
title=_(u"%(name)s's profile", name=current_user.nickname), page="me", title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
kobo_support=kobo_support, kobo_support=kobo_support,
registered_oauth=oauth_check, oauth_status=oauth_status) registered_oauth=local_oauth_check, oauth_status=oauth_status)
current_user.email = to_save["email"]
if "nickname" in to_save and to_save["nickname"] != current_user.nickname: if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
# Query User nickname, if not existing, change # Query User nickname, if not existing, change
if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar(): if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar():
@ -1594,12 +1753,10 @@ def profile():
languages=languages, languages=languages,
kobo_support=kobo_support, kobo_support=kobo_support,
new_user=0, content=current_user, new_user=0, content=current_user,
downloads=downloads, registered_oauth=local_oauth_check,
registered_oauth=oauth_check,
title=_(u"Edit User %(nick)s", title=_(u"Edit User %(nick)s",
nick=current_user.nickname), nick=current_user.nickname),
page="edituser") page="edituser")
current_user.email = to_save["email"]
if "show_random" in to_save and to_save["show_random"] == "on": if "show_random" in to_save and to_save["show_random"] == "on":
current_user.random_books = 1 current_user.random_books = 1
if "default_language" in to_save: if "default_language" in to_save:
@ -1615,24 +1772,32 @@ def profile():
if "Show_detail_random" in to_save: if "Show_detail_random" in to_save:
current_user.sidebar_view += constants.DETAIL_RANDOM current_user.sidebar_view += constants.DETAIL_RANDOM
# current_user.mature_content = "Show_mature_content" in to_save
try: try:
ub.session.commit() ub.session.commit()
flash(_(u"Profile updated"), category="success")
log.debug(u"Profile updated")
except IntegrityError: except IntegrityError:
ub.session.rollback() ub.session.rollback()
flash(_(u"Found an existing account for this e-mail address."), category="error") flash(_(u"Found an existing account for this e-mail address."), category="error")
log.debug(u"Found an existing account for this e-mail address.") log.debug(u"Found an existing account for this e-mail address.")
return render_title_template("user_edit.html", content=current_user, downloads=downloads, '''return render_title_template("user_edit.html",
translations=translations, kobo_support=kobo_support, content=current_user,
title=_(u"%(name)s's profile", name=current_user.nickname), page="me", translations=translations,
registered_oauth=oauth_check, oauth_status=oauth_status) kobo_support=kobo_support,
flash(_(u"Profile updated"), category="success") title=_(u"%(name)s's profile", name=current_user.nickname),
log.debug(u"Profile updated") page="me",
return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages, registered_oauth=local_oauth_check,
content=current_user, downloads=downloads, kobo_support=kobo_support, oauth_status=oauth_status)'''
return render_title_template("user_edit.html",
translations=translations,
profile=1,
languages=languages,
content=current_user,
kobo_support=kobo_support,
title=_(u"%(name)s's profile", name=current_user.nickname), title=_(u"%(name)s's profile", name=current_user.nickname),
page="me", registered_oauth=oauth_check, oauth_status=oauth_status) page="me",
registered_oauth=local_oauth_check,
oauth_status=oauth_status)
# ###################################Show single book ################################################################## # ###################################Show single book ##################################################################

@ -1,602 +0,0 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, bodybybuddha, janeczku
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys
import os
import re
import smtplib
import socket
import time
import threading
try:
import queue
except ImportError:
import Queue as queue
from glob import glob
from shutil import copyfile
from datetime import datetime
try:
from StringIO import StringIO
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
except ImportError:
from io import StringIO
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email import encoders
from email.utils import formatdate
from email.utils import make_msgid
from email.generator import Generator
from flask_babel import gettext as _
from . import calibre_db, db
from . import logger, config
from .subproc_wrapper import process_open
from . import gdriveutils
log = logger.create()
chunksize = 8192
# task 'status' consts
STAT_WAITING = 0
STAT_FAIL = 1
STAT_STARTED = 2
STAT_FINISH_SUCCESS = 3
#taskType consts
TASK_EMAIL = 1
TASK_CONVERT = 2
TASK_UPLOAD = 3
TASK_CONVERT_ANY = 4
RET_FAIL = 0
RET_SUCCESS = 1
def _get_main_thread():
for t in threading.enumerate():
if t.__class__.__name__ == '_MainThread':
return t
raise Exception("main thread not found?!")
# For gdrive download book from gdrive to calibredir (temp dir for books), read contents in both cases and append
# it in MIME Base64 encoded to
def get_attachment(bookpath, filename):
"""Get file as MIMEBase message"""
calibrepath = config.config_calibre_dir
if config.config_use_google_drive:
df = gdriveutils.getFileFromEbooksFolder(bookpath, filename)
if df:
datafile = os.path.join(calibrepath, bookpath, filename)
if not os.path.exists(os.path.join(calibrepath, bookpath)):
os.makedirs(os.path.join(calibrepath, bookpath))
df.GetContentFile(datafile)
else:
return None
file_ = open(datafile, 'rb')
data = file_.read()
file_.close()
os.remove(datafile)
else:
try:
file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb')
data = file_.read()
file_.close()
except IOError as e:
log.exception(e)
log.error(u'The requested file could not be read. Maybe wrong permissions?')
return None
attachment = MIMEBase('application', 'octet-stream')
attachment.set_payload(data)
encoders.encode_base64(attachment)
attachment.add_header('Content-Disposition', 'attachment',
filename=filename)
return attachment
# Class for sending email with ability to get current progress
class emailbase():
transferSize = 0
progress = 0
def data(self, msg):
self.transferSize = len(msg)
(code, resp) = smtplib.SMTP.data(self, msg)
self.progress = 0
return (code, resp)
def send(self, strg):
"""Send `strg' to the server."""
log.debug('send: %r', strg[:300])
if hasattr(self, 'sock') and self.sock:
try:
if self.transferSize:
lock=threading.Lock()
lock.acquire()
self.transferSize = len(strg)
lock.release()
for i in range(0, self.transferSize, chunksize):
if isinstance(strg, bytes):
self.sock.send((strg[i:i+chunksize]))
else:
self.sock.send((strg[i:i + chunksize]).encode('utf-8'))
lock.acquire()
self.progress = i
lock.release()
else:
self.sock.sendall(strg.encode('utf-8'))
except socket.error:
self.close()
raise smtplib.SMTPServerDisconnected('Server not connected')
else:
raise smtplib.SMTPServerDisconnected('please run connect() first')
@classmethod
def _print_debug(self, *args):
log.debug(args)
def getTransferStatus(self):
if self.transferSize:
lock2 = threading.Lock()
lock2.acquire()
value = int((float(self.progress) / float(self.transferSize))*100)
lock2.release()
return str(value) + ' %'
else:
return "100 %"
# Class for sending email with ability to get current progress, derived from emailbase class
class email(emailbase, smtplib.SMTP):
def __init__(self, *args, **kwargs):
smtplib.SMTP.__init__(self, *args, **kwargs)
# Class for sending ssl encrypted email with ability to get current progress, , derived from emailbase class
class email_SSL(emailbase, smtplib.SMTP_SSL):
def __init__(self, *args, **kwargs):
smtplib.SMTP_SSL.__init__(self, *args, **kwargs)
#Class for all worker tasks in the background
class WorkerThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.status = 0
self.current = 0
self.last = 0
self.queue = list()
self.UIqueue = list()
self.asyncSMTP = None
self.id = 0
self.db_queue = queue.Queue()
calibre_db.add_queue(self.db_queue)
self.doLock = threading.Lock()
# Main thread loop starting the different tasks
def run(self):
main_thread = _get_main_thread()
while main_thread.is_alive():
try:
self.doLock.acquire()
if self.current != self.last:
index = self.current
log.info(index)
log.info(len(self.queue))
self.doLock.release()
if self.queue[index]['taskType'] == TASK_EMAIL:
self._send_raw_email()
elif self.queue[index]['taskType'] in (TASK_CONVERT, TASK_CONVERT_ANY):
self._convert_any_format()
# TASK_UPLOAD is handled implicitly
self.doLock.acquire()
self.current += 1
if self.current > self.last:
self.current = self.last
self.doLock.release()
else:
self.doLock.release()
except Exception as e:
log.exception(e)
self.doLock.release()
if main_thread.is_alive():
time.sleep(1)
def get_send_status(self):
if self.asyncSMTP:
return self.asyncSMTP.getTransferStatus()
else:
return "0 %"
def _delete_completed_tasks(self):
for index, task in reversed(list(enumerate(self.UIqueue))):
if task['progress'] == "100 %":
# delete tasks
self.queue.pop(index)
self.UIqueue.pop(index)
# if we are deleting entries before the current index, adjust the index
if index <= self.current and self.current:
self.current -= 1
self.last = len(self.queue)
def get_taskstatus(self):
self.doLock.acquire()
if self.current < len(self.queue):
if self.UIqueue[self.current]['stat'] == STAT_STARTED:
if self.queue[self.current]['taskType'] == TASK_EMAIL:
self.UIqueue[self.current]['progress'] = self.get_send_status()
self.UIqueue[self.current]['formRuntime'] = datetime.now() - self.queue[self.current]['starttime']
self.UIqueue[self.current]['rt'] = self.UIqueue[self.current]['formRuntime'].days*24*60 \
+ self.UIqueue[self.current]['formRuntime'].seconds \
+ self.UIqueue[self.current]['formRuntime'].microseconds
self.doLock.release()
return self.UIqueue
def _convert_any_format(self):
# convert book, and upload in case of google drive
self.doLock.acquire()
index = self.current
self.doLock.release()
self.UIqueue[index]['stat'] = STAT_STARTED
self.queue[index]['starttime'] = datetime.now()
self.UIqueue[index]['formStarttime'] = self.queue[index]['starttime']
curr_task = self.queue[index]['taskType']
filename = self._convert_ebook_format()
if filename:
if config.config_use_google_drive:
gdriveutils.updateGdriveCalibreFromLocal()
if curr_task == TASK_CONVERT:
self.add_email(self.queue[index]['settings']['subject'], self.queue[index]['path'],
filename, self.queue[index]['settings'], self.queue[index]['kindle'],
self.UIqueue[index]['user'], self.queue[index]['title'],
self.queue[index]['settings']['body'], internal=True)
def _convert_ebook_format(self):
error_message = None
self.doLock.acquire()
index = self.current
self.doLock.release()
file_path = self.queue[index]['file_path']
book_id = self.queue[index]['bookid']
format_old_ext = u'.' + self.queue[index]['settings']['old_book_format'].lower()
format_new_ext = u'.' + self.queue[index]['settings']['new_book_format'].lower()
# check to see if destination format already exists -
# if it does - mark the conversion task as complete and return a success
# this will allow send to kindle workflow to continue to work
if os.path.isfile(file_path + format_new_ext):
log.info("Book id %d already converted to %s", book_id, format_new_ext)
cur_book = calibre_db.get_book(book_id)
self.queue[index]['path'] = file_path
self.queue[index]['title'] = cur_book.title
self._handleSuccess()
return file_path + format_new_ext
else:
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
book_id,
format_new_ext)
if config.config_kepubifypath and format_old_ext == '.epub' and format_new_ext == '.kepub':
check, error_message = self._convert_kepubify(file_path,
format_old_ext,
format_new_ext,
index)
else:
# check if calibre converter-executable is existing
if not os.path.exists(config.config_converterpath):
# ToDo Text is not translated
self._handleError(_(u"Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
return
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext, index)
if check == 0:
cur_book = calibre_db.get_book(book_id)
if os.path.isfile(file_path + format_new_ext):
# self.db_queue.join()
new_format = db.Data(name=cur_book.data[0].name,
book_format=self.queue[index]['settings']['new_book_format'].upper(),
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
task = {'task':'add_format','id': book_id, 'format': new_format}
self.db_queue.put(task)
# To Do how to handle error?
'''cur_book.data.append(new_format)
try:
# db.session.merge(cur_book)
calibre_db.session.commit()
except OperationalError as e:
calibre_db.session.rollback()
log.error("Database error: %s", e)
self._handleError(_(u"Database error: %(error)s.", error=e))
return'''
self.queue[index]['path'] = cur_book.path
self.queue[index]['title'] = cur_book.title
if config.config_use_google_drive:
os.remove(file_path + format_old_ext)
self._handleSuccess()
return file_path + format_new_ext
else:
error_message = format_new_ext.upper() + ' format not found on disk'
log.info("ebook converter failed with error while converting book")
if not error_message:
error_message = 'Ebook converter failed with unknown error'
self._handleError(error_message)
return
def _convert_calibre(self, file_path, format_old_ext, format_new_ext, index):
try:
# Linux py2.7 encode as list without quotes no empty element for parameters
# linux py3.x no encode and as list without quotes no empty element for parameters
# windows py2.7 encode as string with quotes empty element for parameters is okay
# windows py 3.x no encode and as string with quotes empty element for parameters is okay
# separate handling for windows and linux
quotes = [1, 2]
command = [config.config_converterpath, (file_path + format_old_ext),
(file_path + format_new_ext)]
quotes_index = 3
if config.config_calibre:
parameters = config.config_calibre.split(" ")
for param in parameters:
command.append(param)
quotes.append(quotes_index)
quotes_index += 1
p = process_open(command, quotes)
except OSError as e:
return 1, _(u"Ebook-converter failed: %(error)s", error=e)
while p.poll() is None:
nextline = p.stdout.readline()
if os.name == 'nt' and sys.version_info < (3, 0):
nextline = nextline.decode('windows-1252')
elif os.name == 'posix' and sys.version_info < (3, 0):
nextline = nextline.decode('utf-8')
log.debug(nextline.strip('\r\n'))
# parse progress string from calibre-converter
progress = re.search(r"(\d+)%\s.*", nextline)
if progress:
self.UIqueue[index]['progress'] = progress.group(1) + ' %'
# process returncode
check = p.returncode
calibre_traceback = p.stderr.readlines()
error_message = ""
for ele in calibre_traceback:
if sys.version_info < (3, 0):
ele = ele.decode('utf-8')
log.debug(ele.strip('\n'))
if not ele.startswith('Traceback') and not ele.startswith(' File'):
error_message = "Calibre failed with error: %s" % ele.strip('\n')
return check, error_message
def _convert_kepubify(self, file_path, format_old_ext, format_new_ext, index):
quotes = [1, 3]
command = [config.config_kepubifypath, (file_path + format_old_ext), '-o', os.path.dirname(file_path)]
try:
p = process_open(command, quotes)
except OSError as e:
return 1, _(u"Kepubify-converter failed: %(error)s", error=e)
self.UIqueue[index]['progress'] = '1 %'
while True:
nextline = p.stdout.readlines()
nextline = [x.strip('\n') for x in nextline if x != '\n']
if sys.version_info < (3, 0):
nextline = [x.decode('utf-8') for x in nextline]
for line in nextline:
log.debug(line)
if p.poll() is not None:
break
# ToD Handle
# process returncode
check = p.returncode
# move file
if check == 0:
converted_file = glob(os.path.join(os.path.dirname(file_path), "*.kepub.epub"))
if len(converted_file) == 1:
copyfile(converted_file[0], (file_path + format_new_ext))
os.unlink(converted_file[0])
else:
return 1, _(u"Converted file not found or more than one file in folder %(folder)s",
folder=os.path.dirname(file_path))
return check, None
def add_convert(self, file_path, bookid, user_name, taskMessage, settings, kindle_mail=None):
self.doLock.acquire()
if self.last >= 20:
self._delete_completed_tasks()
# progress, runtime, and status = 0
self.id += 1
task = TASK_CONVERT_ANY
if kindle_mail:
task = TASK_CONVERT
self.queue.append({'file_path':file_path, 'bookid':bookid, 'starttime': 0, 'kindle': kindle_mail,
'taskType': task, 'settings':settings})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': task } )
self.last=len(self.queue)
self.doLock.release()
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, taskMessage,
text, internal=False):
# if more than 20 entries in the list, clean the list
self.doLock.acquire()
if self.last >= 20:
self._delete_completed_tasks()
if internal:
self.current-= 1
# progress, runtime, and status = 0
self.id += 1
self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath,
'settings':settings, 'recipent':recipient, 'starttime': 0,
'taskType': TASK_EMAIL, 'text':text})
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': TASK_EMAIL })
self.last=len(self.queue)
self.doLock.release()
def add_upload(self, user_name, taskMessage):
# if more than 20 entries in the list, clean the list
self.doLock.acquire()
if self.last >= 20:
self._delete_completed_tasks()
# progress=100%, runtime=0, and status finished
self.id += 1
starttime = datetime.now()
self.queue.append({'starttime': starttime, 'taskType': TASK_UPLOAD})
self.UIqueue.append({'user': user_name, 'formStarttime': starttime, 'progress': "100 %", 'taskMess': taskMessage,
'runtime': '0 s', 'stat': STAT_FINISH_SUCCESS,'id': self.id, 'taskType': TASK_UPLOAD})
self.last=len(self.queue)
self.doLock.release()
def _send_raw_email(self):
self.doLock.acquire()
index = self.current
self.doLock.release()
self.queue[index]['starttime'] = datetime.now()
self.UIqueue[index]['formStarttime'] = self.queue[index]['starttime']
self.UIqueue[index]['stat'] = STAT_STARTED
obj=self.queue[index]
# create MIME message
msg = MIMEMultipart()
msg['Subject'] = self.queue[index]['subject']
msg['Message-Id'] = make_msgid('calibre-web')
msg['Date'] = formatdate(localtime=True)
text = self.queue[index]['text']
msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8'))
if obj['attachment']:
result = get_attachment(obj['filepath'], obj['attachment'])
if result:
msg.attach(result)
else:
self._handleError(u"Attachment not found")
return
msg['From'] = obj['settings']["mail_from"]
msg['To'] = obj['recipent']
use_ssl = int(obj['settings'].get('mail_use_ssl', 0))
try:
# convert MIME message to string
fp = StringIO()
gen = Generator(fp, mangle_from_=False)
gen.flatten(msg)
msg = fp.getvalue()
# send email
timeout = 600 # set timeout to 5mins
# redirect output to logfile on python2 pn python3 debugoutput is caught with overwritten
# _print_debug function
if sys.version_info < (3, 0):
org_smtpstderr = smtplib.stderr
smtplib.stderr = logger.StderrLogger('worker.smtp')
if use_ssl == 2:
self.asyncSMTP = email_SSL(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout=timeout)
else:
self.asyncSMTP = email(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout=timeout)
# link to logginglevel
if logger.is_debug_enabled():
self.asyncSMTP.set_debuglevel(1)
if use_ssl == 1:
self.asyncSMTP.starttls()
if obj['settings']["mail_password"]:
self.asyncSMTP.login(str(obj['settings']["mail_login"]), str(obj['settings']["mail_password"]))
self.asyncSMTP.sendmail(obj['settings']["mail_from"], obj['recipent'], msg)
self.asyncSMTP.quit()
self._handleSuccess()
if sys.version_info < (3, 0):
smtplib.stderr = org_smtpstderr
except (MemoryError) as e:
log.exception(e)
self._handleError(u'MemoryError sending email: ' + str(e))
return None
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
if hasattr(e, "smtp_error"):
text = e.smtp_error.decode('utf-8').replace("\n",'. ')
elif hasattr(e, "message"):
text = e.message
else:
log.exception(e)
text = ''
self._handleError(u'Smtplib Error sending email: ' + text)
return None
except (socket.error) as e:
self._handleError(u'Socket Error sending email: ' + e.strerror)
return None
def _handleError(self, error_message):
log.error(error_message)
self.doLock.acquire()
index = self.current
self.doLock.release()
self.UIqueue[index]['stat'] = STAT_FAIL
self.UIqueue[index]['progress'] = "100 %"
self.UIqueue[index]['formRuntime'] = datetime.now() - self.queue[index]['starttime']
self.UIqueue[index]['message'] = error_message
def _handleSuccess(self):
self.doLock.acquire()
index = self.current
self.doLock.release()
self.UIqueue[index]['stat'] = STAT_FINISH_SUCCESS
self.UIqueue[index]['progress'] = "100 %"
self.UIqueue[index]['formRuntime'] = datetime.now() - self.queue[index]['starttime']
def get_taskstatus():
return _worker.get_taskstatus()
def add_email(subject, filepath, attachment, settings, recipient, user_name, taskMessage, text):
return _worker.add_email(subject, filepath, attachment, settings, recipient, user_name, taskMessage, text)
def add_upload(user_name, taskMessage):
return _worker.add_upload(user_name, taskMessage)
def add_convert(file_path, bookid, user_name, taskMessage, settings, kindle_mail=None):
return _worker.add_convert(file_path, bookid, user_name, taskMessage, settings, kindle_mail)
_worker = WorkerThread()
_worker.start()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save