From a2a48515d48829a95964ea635483d87760ca7d17 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Sun, 22 Jan 2017 14:07:54 +0100 Subject: [PATCH 01/19] Fixed language and locale preset for new users --- cps/templates/user_edit.html | 4 ++-- cps/web.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index bb6368d9..bdace538 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -27,14 +27,14 @@
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +{% endblock %} diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 9f210e68..a7b8c78e 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -191,7 +191,6 @@ - {% endfor %} diff --git a/cps/templates/shelf.html b/cps/templates/shelf.html index 3074f742..4f8a5f94 100644 --- a/cps/templates/shelf.html +++ b/cps/templates/shelf.html @@ -36,7 +36,6 @@ {% endif %} - {% endfor %} diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index bdace538..d9b6f466 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -41,23 +41,23 @@
- +
- +
- +
- +
- +
diff --git a/cps/ub.py b/cps/ub.py index 1eeb212b..32b32f5b 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -7,12 +7,12 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import * from flask_login import AnonymousUserMixin import os -import config +# import config import traceback from werkzeug.security import generate_password_hash from flask_babel import gettext as _ -dbpath = os.path.join(config.APP_DB_ROOT, "app.db") +dbpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__))+os.sep+".."+os.sep), "app.db") engine = create_engine('sqlite:///{0}'.format(dbpath), echo=False) Base = declarative_base() @@ -96,6 +96,23 @@ class UserBase(): def __repr__(self): return '' % self.nickname +class Config(): + def __init__(self): + self.loadSettings() + + def loadSettings(self): + data=session.query(Settings).first() + self.config_calibre_dir = data.config_calibre_dir + self.config_port = data.config_port + self.config_calibre_web_title = data.config_calibre_web_title + self.config_books_per_page = data.config_books_per_page + self.config_random_books = data.config_random_books + self.config_title_regex = data.config_title_regex + self.config_log_level = data.config_log_level + self.config_uploading = data.config_uploading + self.config_anonbrowse = data.config_anonbrowse + self.config_public_reg = data.config_public_reg + class User(UserBase,Base): __tablename__ = 'user' @@ -118,11 +135,14 @@ class User(UserBase,Base): class Anonymous(AnonymousUserMixin,UserBase): + anon_browse = None + def __init__(self): self.loadSettings() def loadSettings(self): data=session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() + settings=session.query(Settings).first() self.nickname = data.nickname self.role = data.role self.random_books = data.random_books @@ -133,6 +153,7 @@ class Anonymous(AnonymousUserMixin,UserBase): self.hot_books = data.hot_books self.default_language = data.default_language self.locale = data.locale + self.anon_browse = settings.config_anonbrowse def role_admin(self): return False @@ -141,7 +162,7 @@ class Anonymous(AnonymousUserMixin,UserBase): return False def is_anonymous(self): - return config.ANON_BROWSE + return self.anon_browse class Shelf(Base): @@ -187,6 +208,16 @@ class Settings(Base): mail_login = Column(String) mail_password = Column(String) mail_from = Column(String) + config_calibre_dir = Column(String) + config_port = Column(Integer, default = 8083) + config_calibre_web_title = Column(String,default = u'Calibre-web') + config_books_per_page = Column(Integer, default = 60) + config_random_books = Column(Integer, default = 4) + config_title_regex = Column(String,default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+') + config_log_level = Column(String, default=u'INFO') + config_uploading = Column(SmallInteger, default = 0) + config_anonbrowse = Column(SmallInteger, default = 0) + config_public_reg = Column(SmallInteger, default = 0) def __repr__(self): #return '' % (self.mail_server) @@ -216,14 +247,22 @@ def migrate_Database(): conn.execute("ALTER TABLE user ADD column hot_books INTEGER DEFAULT 1") session.commit() try: - session.query(exists().where(BookShelf.order)).scalar() + session.query(exists().where(Settings.config_calibre_dir)).scalar() session.commit() except exc.OperationalError: # Database is not compatible, some rows are missing conn = engine.connect() - conn.execute("ALTER TABLE book_shelf_link ADD column `order` INTEGER DEFAULT 1") + conn.execute("ALTER TABLE Settings ADD column `config_calibre_dir` String") + conn.execute("ALTER TABLE Settings ADD column `config_port` INTEGER DEFAULT 8083") + conn.execute("ALTER TABLE Settings ADD column `config_calibre_web_title` String DEFAULT 'Calibre-web'") + conn.execute("ALTER TABLE Settings ADD column `config_books_per_page` INTEGER DEFAULT 60") + conn.execute("ALTER TABLE Settings ADD column `config_random_books` INTEGER DEFAULT 4") + conn.execute("ALTER TABLE Settings ADD column `config_title_regex` String DEFAULT '^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+'") + conn.execute("ALTER TABLE Settings ADD column `config_log_level` String DEFAULT 'INFO'") + conn.execute("ALTER TABLE Settings ADD column `config_uploading` SmallInteger DEFAULT 0") + conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0") + conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0") session.commit() - def create_default_config(): settings = Settings() settings.mail_server = "mail.example.com" diff --git a/cps/web.py b/cps/web.py index 2ee71662..3261b6e4 100755 --- a/cps/web.py +++ b/cps/web.py @@ -7,7 +7,7 @@ from logging.handlers import RotatingFileHandler import textwrap from flask import Flask, render_template, session, request, Response, redirect, url_for, send_from_directory, \ make_response, g, flash, abort -import db, config, ub, helper +import ub, helper import os import errno from sqlalchemy.sql.expression import func @@ -33,32 +33,24 @@ from uuid import uuid4 import os.path import sys import subprocess -import shutil import re -from shutil import move +import db +from shutil import move, copyfile +from tornado.ioloop import IOLoop try: from wand.image import Image - use_generic_pdf_cover = False except ImportError, e: use_generic_pdf_cover = True - -from shutil import copyfile from cgi import escape -mimetypes.init() -mimetypes.add_type('application/xhtml+xml', '.xhtml') -mimetypes.add_type('application/epub+zip', '.epub') -mimetypes.add_type('application/x-mobipocket-ebook', '.mobi') -mimetypes.add_type('application/x-mobipocket-ebook', '.prc') -mimetypes.add_type('application/vnd.amazon.ebook', '.azw') -mimetypes.add_type('application/x-cbr', '.cbr') -mimetypes.add_type('application/x-cbz', '.cbz') -mimetypes.add_type('application/x-cbt', '.cbt') -mimetypes.add_type('image/vnd.djvu', '.djvu') +########################################## Global variables ######################################################## +global global_task +global_task = None +########################################## Proxy Helper class ###################################################### class ReverseProxied(object): """Wrap the application in this middleware and configure the front-end server to add these headers, to let you quietly bind @@ -96,11 +88,23 @@ class ReverseProxied(object): environ['HTTP_HOST'] = server return self.app(environ, start_response) +########################################## Main code ############################################################## +mimetypes.init() +mimetypes.add_type('application/xhtml+xml', '.xhtml') +mimetypes.add_type('application/epub+zip', '.epub') +mimetypes.add_type('application/x-mobipocket-ebook', '.mobi') +mimetypes.add_type('application/x-mobipocket-ebook', '.prc') +mimetypes.add_type('application/vnd.amazon.ebook', '.azw') +mimetypes.add_type('application/x-cbr', '.cbr') +mimetypes.add_type('application/x-cbz', '.cbz') +mimetypes.add_type('application/x-cbt', '.cbt') +mimetypes.add_type('image/vnd.djvu', '.djvu') + app = (Flask(__name__)) app.wsgi_app = ReverseProxied(app.wsgi_app) -formatter = logging.Formatter( +'''formatter = logging.Formatter( "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s") file_handler = RotatingFileHandler(os.path.join(config.LOG_DIR, "calibre-web.log"), maxBytes=50000, backupCount=1) file_handler.setFormatter(formatter) @@ -112,7 +116,7 @@ else: app.logger.info('Starting Calibre Web...') logging.getLogger("book_formats").addHandler(file_handler) -logging.getLogger("book_formats").setLevel(logging.INFO) +logging.getLogger("book_formats").setLevel(logging.INFO)''' Principal(app) @@ -120,10 +124,6 @@ babel = Babel(app) import uploader -global global_queue -global_queue = None - - lm = LoginManager(app) lm.init_app(app) lm.login_view = 'login' @@ -131,6 +131,10 @@ lm.anonymous_user = ub.Anonymous app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' +# establish connection to calibre-db +config=ub.Config() +db.setup_db(config) + @babel.localeselector def get_locale(): @@ -190,7 +194,7 @@ def requires_basic_auth_if_no_ano(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization - if config.ANON_BROWSE != 1: + if config.config_anonbrowse != 1: if not auth or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) @@ -257,7 +261,7 @@ app.jinja_env.globals['url_for_other_page'] = url_for_other_page def login_required_if_no_ano(func): - if config.ANON_BROWSE == 1: + if config.config_anonbrowse == 1: return func return login_required(func) @@ -284,7 +288,6 @@ def admin_required(f): """ Checks if current_user.role == 1 """ - @wraps(f) def inner(*args, **kwargs): if current_user.role_admin(): @@ -293,6 +296,18 @@ def admin_required(f): return inner +def unconfigured(f): + """ + Checks if current_user.role == 1 + """ + @wraps(f) + def inner(*args, **kwargs): + if not config.config_calibre_dir: + return f(*args, **kwargs) + abort(403) + + return inner + def download_required(f): @wraps(f) @@ -331,14 +346,14 @@ def fill_indexpage(page, database, db_filter, order): else: filter = True if current_user.show_random_books(): - random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.RANDOM_BOOKS) + random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_random_books) else: random = false - off = int(int(config.NEWEST_BOOKS) * (page - 1)) - pagination = Pagination(page, config.NEWEST_BOOKS, + off = int(int(config.config_books_per_page) * (page - 1)) + pagination = Pagination(page, config.config_books_per_page, len(db.session.query(database).filter(db_filter).filter(filter).all())) entries = db.session.query(database).filter(db_filter).filter(filter).order_by(order).offset(off).limit( - config.NEWEST_BOOKS) + config.config_books_per_page) return entries, random, pagination @@ -400,9 +415,12 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session def before_request(): g.user = current_user g.public_shelfes = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1).all() - g.allow_registration = config.PUBLIC_REG - g.allow_upload = config.UPLOADING + g.allow_registration = config.config_public_reg + g.allow_upload = config.config_uploading +'''################################################################################################################# +########################################## Routing functions ####################################################### +#################################################################################################################''' @app.route("/opds") @requires_basic_auth_if_no_ano @@ -467,8 +485,8 @@ def feed_new(): if not off: off = 0 entries = db.session.query(db.Books).filter(filter).order_by(db.Books.timestamp.desc()).offset(off).limit( - config.NEWEST_BOOKS) - pagination = Pagination((int(off)/(int(config.NEWEST_BOOKS))+1), config.NEWEST_BOOKS, + config.config_books_per_page) + pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, len(db.session.query(db.Books).filter(filter).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) @@ -486,8 +504,8 @@ def feed_discover(): filter = True # if not off: # off = 0 - entries = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.NEWEST_BOOKS) - pagination = Pagination(1, config.NEWEST_BOOKS,int(config.NEWEST_BOOKS)) + entries = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_books_per_page) + pagination = Pagination(1, config.config_books_per_page,int(config.config_books_per_page)) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -505,8 +523,8 @@ def feed_hot(): if not off: off = 0 entries = db.session.query(db.Books).filter(filter).filter(db.Books.ratings.any(db.Ratings.rating > 9)).offset( - off).limit(config.NEWEST_BOOKS) - pagination = Pagination((int(off)/(int(config.NEWEST_BOOKS))+1), config.NEWEST_BOOKS, + off).limit(config.config_books_per_page) + pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, len(db.session.query(db.Books).filter(filter).filter(db.Books.ratings.any(db.Ratings.rating > 9)).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) @@ -525,8 +543,8 @@ def feed_authorindex(): filter = True if not off: off = 0 - authors = db.session.query(db.Authors).order_by(db.Authors.sort).offset(off).limit(config.NEWEST_BOOKS) - pagination = Pagination((int(off)/(int(config.NEWEST_BOOKS))+1), config.NEWEST_BOOKS, + authors = db.session.query(db.Authors).order_by(db.Authors.sort).offset(off).limit(config.config_books_per_page) + pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, len(db.session.query(db.Authors).all())) xml = render_template('feed.xml', authors=authors, pagination=pagination) response = make_response(xml) @@ -545,8 +563,8 @@ def feed_author(id): if not off: off = 0 entries = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id )).filter( - filter).offset(off).limit(config.NEWEST_BOOKS) - pagination = Pagination((int(off)/(int(config.NEWEST_BOOKS))+1), config.NEWEST_BOOKS, + filter).offset(off).limit(config.config_books_per_page) + pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, len(db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id )).filter(filter).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) @@ -560,8 +578,8 @@ def feed_categoryindex(): off = request.args.get("offset") if not off: off = 0 - entries = db.session.query(db.Tags).order_by(db.Tags.name).offset(off).limit(config.NEWEST_BOOKS) - pagination = Pagination((int(off)/(int(config.NEWEST_BOOKS))+1), config.NEWEST_BOOKS, + entries = db.session.query(db.Tags).order_by(db.Tags.name).offset(off).limit(config.config_books_per_page) + pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, len(db.session.query(db.Tags).all())) xml = render_template('feed.xml', categorys=entries, pagination=pagination) response = make_response(xml) @@ -580,8 +598,8 @@ def feed_category(id): if not off: off = 0 entries = db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id==id)).order_by( - db.Books.timestamp.desc()).filter(filter).offset(off).limit(config.NEWEST_BOOKS) - pagination = Pagination((int(off)/(int(config.NEWEST_BOOKS))+1), config.NEWEST_BOOKS, + db.Books.timestamp.desc()).filter(filter).offset(off).limit(config.config_books_per_page) + pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, len(db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id==id)).filter(filter).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) @@ -599,8 +617,8 @@ def feed_seriesindex(): filter = True if not off: off = 0 - entries = db.session.query(db.Series).order_by(db.Series.name).offset(off).limit(config.NEWEST_BOOKS) - pagination = Pagination((int(off)/(int(config.NEWEST_BOOKS))+1), config.NEWEST_BOOKS, + entries = db.session.query(db.Series).order_by(db.Series.name).offset(off).limit(config.config_books_per_page) + pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, len(db.session.query(db.Series).all())) xml = render_template('feed.xml', series=entries, pagination=pagination) response = make_response(xml) @@ -619,8 +637,8 @@ def feed_series(id): if not off: off = 0 entries = db.session.query(db.Books).filter(db.Books.series.any(db.Series.id == id)).order_by( - db.Books.timestamp.desc()).filter(filter).offset(off).limit(config.NEWEST_BOOKS) - pagination = Pagination((int(off)/(int(config.NEWEST_BOOKS))+1), config.NEWEST_BOOKS, + db.Books.timestamp.desc()).filter(filter).offset(off).limit(config.config_books_per_page) + pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, len(db.session.query(db.Books).filter(db.Books.series.any(db.Series.id == id)).filter(filter).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) @@ -754,15 +772,15 @@ def hot_books(page): random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.RANDOM_BOOKS) else: random = false - off = int(int(config.NEWEST_BOOKS) * (page - 1)) + off = int(int(config.config_books_per_page) * (page - 1)) all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by( ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id) - hot_books = all_books.offset(off).limit(config.NEWEST_BOOKS) + hot_books = all_books.offset(off).limit(config.config_books_per_page) entries = list() for book in hot_books: entries.append(db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first()) numBooks = entries.__len__() - pagination = Pagination(page, config.NEWEST_BOOKS, numBooks) + pagination = Pagination(page, config.config_books_per_page, numBooks) return render_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Hot Books (most downloaded)")) @@ -958,13 +976,27 @@ def stats(): @app.route("/shutdown") @login_required +@admin_required def shutdown(): - # logout_user() - # add restart command to queue - global_queue.put("something") - flash(_(u"Server restarts"), category="info") - return redirect(url_for("index")) - + global global_task + task = int(request.args.get("parameter").strip()) + global_task = task + if task == 1 or task == 0: # valid commandos received + # close all database connections + db.session.close() + db.engine.dispose() + ub.session.close() + ub.engine.dispose() + # stop tornado server + server=IOLoop.instance() + server.add_callback(server.stop) + if task == 0: + text['text']=_(u'Performing Restart, please reload page') + else: + text['text']= _(u'Performing shutdown of server, please close window') + return json.dumps(text) + else: + abort(404) @app.route("/search", methods=["GET"]) @login_required_if_no_ano @@ -1200,6 +1232,9 @@ def register(): def login(): error = None + if config.config_calibre_dir == None: + return redirect(url_for('basic_configuration')) + if current_user is not None and current_user.is_authenticated: return redirect(url_for('index')) @@ -1234,7 +1269,7 @@ def send_to_kindle(book_id): if settings.get("mail_server", "mail.example.com") == "mail.example.com": flash(_(u"Please configure the SMTP mail settings first..."), category="error") elif current_user.kindle_mail: - result = helper.send_mail(book_id, current_user.kindle_mail) + result = helper.send_mail(book_id, current_user.kindle_mail,config.config_calibre_dir) if result is None: flash(_(u"Book successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail), category="success") @@ -1357,7 +1392,7 @@ def delete_shelf(shelf_id): if deleted: ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete() ub.session.commit() - flash(_("successfully deleted shelf %(name)s", name=cur_shelf.name, category="success")) + flash(_(u"successfully deleted shelf %(name)s", name=cur_shelf.name, category="success")) return redirect(url_for('index')) @@ -1486,9 +1521,56 @@ def admin(): @login_required @admin_required def configuration(): - content = ub.session.query(ub.User).all() - settings = ub.session.query(ub.Settings).first() - return render_template("admin.html", content=content, email=settings, config=config, title=_(u"Admin page")) + return render_template("config_edit.html", content=config, title=_(u"Basic Configuration")) + +@app.route("/config", methods=["GET", "POST"] ) +@unconfigured +def basic_configuration(): + global global_task + if request.method == "POST": + to_save = request.form.to_dict() + content = ub.session.query(ub.Settings).first() + if "config_calibre_dir" in to_save: + content.config_calibre_dir = to_save["config_calibre_dir"] + if "config_port" in to_save: + content.config_port = to_save["config_port"] + if "config_calibre_web_title" in to_save: + content.config_calibre_web_title = to_save["config_calibre_web_title"] + if "config_calibre_web_title" in to_save: + content.config_calibre_web_title = to_save["config_calibre_web_title"] + if "config_title_regex" in to_save: + content.config_title_regex = to_save["config_title_regex"] + if "config_log_level" in to_save: + content.config_log_level = to_save["config_log_level"] + if "config_random_books" in to_save: + content.config_random_books = int(to_save["config_random_books"]) + if "config_books_per_page" in to_save: + content.config_books_per_page = int(to_save["config_books_per_page"]) + content.config_uploading = 0 + content.config_anonbrowse = 0 + content.config_public_reg = 0 + if "config_uploading" in to_save and to_save["config_uploading"] == "on": + content.config_uploading = 1 + if "config_anonbrowse" in to_save and to_save["config_anonbrowse"] == "on": + content.config_anonbrowse = 1 + if "config_public_reg" in to_save and to_save["config_public_reg"] == "on": + content.config_public_reg = 1 + try: + ub.session.commit() + flash(_(u"Calibre-web configuration updated"), category="success") + config.loadSettings() + except e: + flash(e, category="error") + return render_template("config_edit.html", content=config, title=_(u"Basic Configuration")) + + ub.session.close() + ub.engine.dispose() + # stop tornado server + server = IOLoop.instance() + server.add_callback(server.stop) + global_task = 0 + + return render_template("config_edit.html", content=config, title=_(u"Basic Configuration")) @app.route("/admin/user/new", methods=["GET", "POST"]) @@ -1513,6 +1595,7 @@ def new_user(): content.nickname = to_save["nickname"] content.email = to_save["email"] content.default_language = to_save["default_language"] + if "locale" in to_save: content.locale = to_save["locale"] content.random_books = 0 content.language_books = 0 @@ -1541,13 +1624,13 @@ def new_user(): try: ub.session.add(content) ub.session.commit() - flash(_("User '%(user)s' created", user=content.nickname), category="success") + flash(_(u"User '%(user)s' created", user=content.nickname), category="success") return redirect(url_for('admin')) except IntegrityError: ub.session.rollback() flash(_(u"Found an existing account for this email address or nickname."), category="error") return render_template("user_edit.html", new_user=1, content=content, translations=translations, - languages=languages, title=_("Add new user")) + languages=languages, title=_(u"Add new user")) @app.route("/admin/mailsettings", methods=["GET", "POST"]) @@ -1575,7 +1658,7 @@ def edit_mailsettings(): category="success") else: flash(_(u"There was an error sending the Test E-Mail: %(res)s", res=result), category="error") - return render_template("email_edit.html", content=content, title=_("Edit mail settings")) + return render_template("email_edit.html", content=content, title=_(u"Edit mail settings")) @app.route("/admin/user/", methods=["GET", "POST"]) @@ -1863,13 +1946,13 @@ def edit_book(book_id): for author in book.authors: author_names.append(author.name) for b in edited_books_id: - helper.update_dir_stucture(b) + helper.update_dir_stucture(b,config.config_calibre_dir) if "detail_view" in to_save: return redirect(url_for('show_book', id=book.id)) else: - return render_template('edit_book.html', book=book, authors=author_names, cc=cc) + return render_template('book_edit.html', book=book, authors=author_names, cc=cc) else: - return render_template('edit_book.html', book=book, authors=author_names, cc=cc) + return render_template('book_edit.html', book=book, authors=author_names, cc=cc) else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") return redirect(url_for("index")) @@ -1942,6 +2025,6 @@ def upload(): author_names.append(author.name) cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() if current_user.role_edit() or current_user.role_admin(): - return render_template('edit_book.html', book=db_book, authors=author_names, cc=cc) + return render_template('book_edit.html', book=db_book, authors=author_names, cc=cc) book_in_shelfs = [] return render_template('detail.html', entry=db_book, cc=cc, title=db_book.title, books_shelfs=book_in_shelfs) From 75c89c28e10f3b92e87f22745746fcdb57c817c3 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Sun, 22 Jan 2017 21:30:36 +0100 Subject: [PATCH 03/19] Bugfix for accessing config database Title now displaed on every page Title can be changed from within application --- cps/db.py | 18 ++- cps/helper.py | 2 +- cps/templates/admin.html | 18 +-- cps/templates/config_edit.html | 2 +- cps/templates/layout.html | 2 +- cps/templates/read.html | 2 +- cps/ub.py | 18 ++- cps/web.py | 203 ++++++++++++++++++--------------- 8 files changed, 149 insertions(+), 116 deletions(-) diff --git a/cps/db.py b/cps/db.py index 97707922..a16672ae 100755 --- a/cps/db.py +++ b/cps/db.py @@ -5,13 +5,15 @@ from sqlalchemy import * from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import * import os -# import config import re import ast +from ub import Config -global session - +# user defined sort function for calibre databases (Series, etc.) def title_sort(title): + # calibre sort stuff + config=Config() + title_pat = re.compile(config.config_title_regex, re.IGNORECASE) match = title_pat.search(title) if match: prep = match.group(1) @@ -250,20 +252,24 @@ class Custom_Columns(Base): display_dict = ast.literal_eval(self.display) return display_dict + def setup_db(config): global session - # calibre sort stuff - title_pat = re.compile(config.config_title_regex, re.IGNORECASE) + global cc_exceptions + global cc_classes + global cc_ids + global books_custom_column_links if config.config_calibre_dir is None: return + dbpath = os.path.join(config.config_calibre_dir, "metadata.db") engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False) conn = engine.connect() conn.connection.create_function('title_sort', 1, title_sort) - cc = conn.execute("SELECT id, datatype FROM custom_columns") + cc_ids = [] cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series'] books_custom_column_links = {} diff --git a/cps/helper.py b/cps/helper.py index e551627b..7812131c 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -255,7 +255,7 @@ def update_dir_stucture(book_id,calibrepath): book.path = book.path.split(os.sep)[0] + os.sep + new_titledir if authordir != new_authordir: - new_author_path = os.path.join(os.path.join(config.DB_ROOT, new_authordir), os.path.basename(path)) + new_author_path = os.path.join(os.path.join(calibrepath, new_authordir), os.path.basename(path)) os.renames(path, new_author_path) book.path = new_authordir + os.sep + book.path.split(os.sep)[1] db.session.commit() diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 4f4fd77c..69cadaee 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -15,7 +15,7 @@ {{_('Passwd')}} {% for user in content %} - {% if not user.role_anonymous() or config.ANON_BROWSE %} + {% if not user.role_anonymous() or config.config_anonbrowse %} {{user.nickname}} {{user.email}} @@ -56,7 +56,7 @@

{{_('Configuration')}}

- + @@ -65,13 +65,13 @@ - - - - - - - + + + + + + +
{{_('Log File')}}{{_('Calibre DB dir')}} {{_('Log Level')}} {{_('Port')}} {{_('Books per page')}}{{_('Anonymous browsing')}}
{{config.LOG_DIR}}{{config.LOG_DIR}}{{config.PORT}}{{config.NEWEST_BOOKS}}{% if config.UPLOADING %}{% else %}{% endif %}{% if config.PUBLIC_REG %}{% else %}{% endif %}{% if config.ANON_BROWSE %}{% else %}{% endif %}{{config.config_calibre_dir}}{{config.config_log_level}}{{config.config_port}}{{config.config_books_per_page}}{% if config.config_uploading %}{% else %}{% endif %}{% if config.config_public_reg %}{% else %}{% endif %}{% if config.config_anonbrowse %}{% else %}{% endif %}

{{_('Administration')}}

diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 587793f4..80c5d7f7 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -9,7 +9,7 @@
- +
diff --git a/cps/templates/layout.html b/cps/templates/layout.html index b2d22d0e..7205476d 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -1,7 +1,7 @@ - calibre web | {{title}} + {{instance}} | {{title}} diff --git a/cps/templates/read.html b/cps/templates/read.html index 384e52a6..718de1e8 100644 --- a/cps/templates/read.html +++ b/cps/templates/read.html @@ -57,7 +57,7 @@ - + diff --git a/cps/ub.py b/cps/ub.py index 32b32f5b..0268751b 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -7,7 +7,6 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import * from flask_login import AnonymousUserMixin import os -# import config import traceback from werkzeug.security import generate_password_hash from flask_babel import gettext as _ @@ -98,6 +97,9 @@ class UserBase(): class Config(): def __init__(self): + self.config_main_dir=os.path.join(os.path.normpath(os.path.dirname( + os.path.realpath(__file__)) + os.sep + ".." + os.sep)) + self.db_configured=None self.loadSettings() def loadSettings(self): @@ -112,6 +114,18 @@ class Config(): self.config_uploading = data.config_uploading self.config_anonbrowse = data.config_anonbrowse self.config_public_reg = data.config_public_reg + if self.config_calibre_dir is not None and (self.db_configured is None or self.db_configured is True): + self.db_configured=True + else: + self.db_configured = False + + @property + def get_main_dir(self): + return self.config_main_dir + + @property + def is_Calibre_Configured(self): + return self.db_configured class User(UserBase,Base): @@ -135,7 +149,7 @@ class User(UserBase,Base): class Anonymous(AnonymousUserMixin,UserBase): - anon_browse = None + # anon_browse = None def __init__(self): self.loadSettings() diff --git a/cps/web.py b/cps/web.py index 3261b6e4..dd88e66b 100755 --- a/cps/web.py +++ b/cps/web.py @@ -46,7 +46,6 @@ except ImportError, e: from cgi import escape ########################################## Global variables ######################################################## -global global_task global_task = None @@ -124,6 +123,10 @@ babel = Babel(app) import uploader +# establish connection to calibre-db +config=ub.Config() +db.setup_db(config) + lm = LoginManager(app) lm.init_app(app) lm.login_view = 'login' @@ -131,9 +134,6 @@ lm.anonymous_user = ub.Anonymous app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' -# establish connection to calibre-db -config=ub.Config() -db.setup_db(config) @babel.localeselector @@ -293,7 +293,6 @@ def admin_required(f): if current_user.role_admin(): return f(*args, **kwargs) abort(403) - return inner def unconfigured(f): @@ -302,10 +301,9 @@ def unconfigured(f): """ @wraps(f) def inner(*args, **kwargs): - if not config.config_calibre_dir: + if config.is_Calibre_Configured: return f(*args, **kwargs) abort(403) - return inner @@ -315,7 +313,6 @@ def download_required(f): if current_user.role_download() or current_user.role_admin(): return f(*args, **kwargs) abort(403) - return inner @@ -325,7 +322,6 @@ def upload_required(f): if current_user.role_upload() or current_user.role_admin(): return f(*args, **kwargs) abort(403) - return inner @@ -335,7 +331,6 @@ def edit_required(f): if current_user.role_edit() or current_user.role_admin(): return f(*args, **kwargs) abort(403) - return inner @@ -410,6 +405,10 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session # add element to book db_book_object.append(new_element) +def render_title_template(*args, **kwargs): + return render_template(instance=config.config_calibre_web_title, *args, **kwargs) + + @app.before_request def before_request(): @@ -418,9 +417,7 @@ def before_request(): g.allow_registration = config.config_public_reg g.allow_upload = config.config_uploading -'''################################################################################################################# ########################################## Routing functions ####################################################### -#################################################################################################################''' @app.route("/opds") @requires_basic_auth_if_no_ano @@ -755,8 +752,10 @@ def get_matching_tags(): @app.route('/page/') @login_required_if_no_ano def index(page): + if config.is_Calibre_Configured == False: + return redirect(url_for('basic_configuration')) entries, random, pagination = fill_indexpage(page, db.Books, True, db.Books.timestamp.desc()) - return render_template('index.html', random=random, entries=entries, pagination=pagination, + return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Latest Books")) @@ -769,7 +768,7 @@ def hot_books(page): else: filter = True if current_user.show_random_books(): - random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.RANDOM_BOOKS) + random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_random_books) else: random = false off = int(int(config.config_books_per_page) * (page - 1)) @@ -781,7 +780,7 @@ def hot_books(page): entries.append(db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first()) numBooks = entries.__len__() pagination = Pagination(page, config.config_books_per_page, numBooks) - return render_template('index.html', random=random, entries=entries, pagination=pagination, + return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Hot Books (most downloaded)")) @@ -790,7 +789,7 @@ def hot_books(page): @login_required_if_no_ano def discover(page): entries, random, pagination = fill_indexpage(page, db.Books, func.randomblob(2), db.Books.timestamp.desc()) - return render_template('discover.html', entries=entries, pagination=pagination, title=_(u"Random Books")) + return render_title_template('discover.html', entries=entries, pagination=pagination, instance=config.config_calibre_web_title, title=_(u"Random Books")) @app.route("/author") @@ -803,7 +802,7 @@ def author_list(): entries = db.session.query(db.Authors, func.count('books_authors_link.book').label('count')).join( db.books_authors_link).join(db.Books).filter( filter).group_by('books_authors_link.author').order_by(db.Authors.sort).all() - return render_template('list.html', entries=entries, folder='author', title=_(u"Author list")) + return render_title_template('list.html', entries=entries, folder='author', title=_(u"Author list")) @app.route("/author/") @@ -814,13 +813,13 @@ def author(name): else: filter = True if current_user.show_random_books(): - random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.RANDOM_BOOKS) + random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_random_books) else: random = false entries = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.name.like("%" + name + "%"))).filter( filter).all() - return render_template('index.html', random=random, entries=entries, title=_(u"Author: %(nam)s", nam=name)) + return render_title_template('index.html', random=random, entries=entries,title=_(u"Author: %(nam)s", nam=name)) @app.route("/series") @@ -833,7 +832,7 @@ def series_list(): entries = db.session.query(db.Series, func.count('books_series_link.book').label('count')).join( db.books_series_link).join(db.Books).filter( filter).group_by('books_series_link.series').order_by(db.Series.sort).all() - return render_template('list.html', entries=entries, folder='series', title=_(u"Series list")) + return render_title_template('list.html', entries=entries, folder='series', title=_(u"Series list")) @app.route("/series//", defaults={'page': 1}) @@ -843,7 +842,7 @@ def series(name, page): entries, random, pagination = fill_indexpage(page, db.Books, db.Books.series.any(db.Series.name == name), db.Books.series_index) if entries: - return render_template('index.html', random=random, pagination=pagination, entries=entries, + return render_title_template('index.html', random=random, pagination=pagination, entries=entries, title=_(u"Series: %(serie)s", serie=name)) else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") @@ -876,7 +875,7 @@ def language_overview(): lang_counter = db.session.query(db.books_languages_link, func.count('books_languages_link.book').label('bookcount')).group_by( 'books_languages_link.lang_code').all() - return render_template('languages.html', languages=languages, lang_counter=lang_counter, + return render_title_template('languages.html', languages=languages, lang_counter=lang_counter, title=_(u"Available languages")) @@ -891,7 +890,7 @@ def language(name, page): name = cur_l.get_language_name(get_locale()) except: name = _(isoLanguages.get(part3=name).name) - return render_template('index.html', random=random, entries=entries, pagination=pagination, + return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Language: %(name)s", name=name)) @@ -905,7 +904,7 @@ def category_list(): entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count')).join( db.books_tags_link).join(db.Books).filter( filter).group_by('books_tags_link.tag').all() - return render_template('list.html', entries=entries, folder='category', title=_(u"Category list")) + return render_title_template('list.html', entries=entries, folder='category', title=_(u"Category list")) @app.route("/category/", defaults={'page': 1}) @@ -914,7 +913,7 @@ def category_list(): def category(name, page): entries, random, pagination = fill_indexpage(page, db.Books, db.Books.tags.any(db.Tags.name == name), db.Books.timestamp.desc()) - return render_template('index.html', random=random, entries=entries, pagination=pagination, + return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Category: %(name)s", name=name)) @@ -940,7 +939,8 @@ def show_book(id): for entry in shelfs: book_in_shelfs.append(entry.shelf) - return render_template('detail.html', entry=entries, cc=cc, title=entries.title, books_shelfs=book_in_shelfs) + return render_title_template('detail.html', entry=entries, cc=cc, + title=entries.title, books_shelfs=book_in_shelfs) else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") return redirect(url_for("index")) @@ -959,10 +959,11 @@ def stats(): counter = len(db.session.query(db.Books).all()) authors = len(db.session.query(db.Authors).all()) Versions=uploader.book_formats.get_versions() + vendorpath = os.path.join(config.get_main_dir + "vendor" + os.sep) if sys.platform == "win32": - kindlegen = os.path.join(config.MAIN_DIR, "vendor", u"kindlegen.exe") + kindlegen = os.path.join(vendorpath, u"kindlegen.exe") else: - kindlegen = os.path.join(config.MAIN_DIR, "vendor", u"kindlegen") + kindlegen = os.path.join(vendorpath, u"kindlegen") kindlegen_version=_('not installed') if os.path.exists(kindlegen): p = subprocess.Popen(kindlegen, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) @@ -971,7 +972,8 @@ def stats(): if re.search('Amazon kindlegen\(', lines): Versions['KindlegenVersion'] = lines Versions['PythonVersion']=sys.version - return render_template('stats.html', bookcounter=counter, authorcounter=authors, Versions=Versions, title=_(u"Statistics")) + return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, Versions=Versions, + title=_(u"Statistics")) @app.route("/shutdown") @@ -1011,9 +1013,9 @@ def search(): db.Books.series.any(db.Series.name.like("%" + term + "%")), db.Books.authors.any(db.Authors.name.like("%" + term + "%")), db.Books.title.like("%" + term + "%"))).filter(filter).all() - return render_template('search.html', searchterm=term, entries=entries) + return render_title_template('search.html', searchterm=term, entries=entries) else: - return render_template('search.html', searchterm="") + return render_title_template('search.html', searchterm="") @app.route("/advanced_search", methods=["GET"]) @@ -1068,7 +1070,7 @@ def advanced_search(): for language in exclude_languages_inputs: q = q.filter(not_(db.Books.series.any(db.Languages.id == language))) q = q.all() - return render_template('search.html', searchterm=searchterm, entries=q) + return render_title_template('search.html', searchterm=searchterm, entries=q, title=_(u"search")) tags = db.session.query(db.Tags).order_by(db.Tags.name).all() series = db.session.query(db.Series).order_by(db.Series.name).all() if current_user.filter_language() == u"all": @@ -1081,13 +1083,13 @@ def advanced_search(): lang.name = _(isoLanguages.get(part3=lang.lang_code).name) else: languages=None - return render_template('search_form.html', tags=tags, languages=languages, series=series) + return render_title_template('search_form.html', tags=tags, languages=languages, series=series, title=_(u"search")) @app.route("/cover/") @login_required_if_no_ano def get_cover(cover_path): - return send_from_directory(os.path.join(config.DB_ROOT, cover_path), "cover.jpg") + return send_from_directory(os.path.join(config.config_calibre_dir, cover_path), "cover.jpg") @app.route("/opds/thumb_240_240/") @app.route("/opds/cover_240_240/") @@ -1096,7 +1098,7 @@ def get_cover(cover_path): @requires_basic_auth_if_no_ano def feed_get_cover(book_id): book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - return send_from_directory(os.path.join(config.DB_ROOT, book.path), "cover.jpg") + return send_from_directory(os.path.join(config.config_calibre_dir, book.path), "cover.jpg") @app.route("/read//") @@ -1104,14 +1106,14 @@ def feed_get_cover(book_id): def read_book(book_id, format): book = db.session.query(db.Books).filter(db.Books.id == book_id).first() if book: - book_dir = os.path.join(config.MAIN_DIR, "cps", "static", str(book_id)) + book_dir = os.path.join(config.get_main_dir, "cps", "static", str(book_id)) if not os.path.exists(book_dir): os.mkdir(book_dir) if format.lower() == "epub": # check if mimetype file is exists mime_file = str(book_id) + "/mimetype" if not os.path.exists(mime_file): - epub_file = os.path.join(config.DB_ROOT, book.path, book.data[0].name) + ".epub" + epub_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".epub" if not os.path.isfile(epub_file): raise ValueError('Error opening eBook. File does not exist: ', epub_file) zfile = zipfile.ZipFile(epub_file) @@ -1131,28 +1133,28 @@ def read_book(book_id, format): fd.write(zfile.read(name)) fd.close() zfile.close() - return render_template('read.html', bookid=book_id, title=_(u"Read a Book")) + return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book")) elif format.lower() == "pdf": all_name = str(book_id) + "/" + urllib.quote(book.data[0].name) + ".pdf" tmp_file = os.path.join(book_dir, urllib.quote(book.data[0].name)) + ".pdf" if not os.path.exists(tmp_file): - pdf_file = os.path.join(config.DB_ROOT, book.path, book.data[0].name) + ".pdf" + pdf_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".pdf" copyfile(pdf_file, tmp_file) - return render_template('readpdf.html', pdffile=all_name, title=_(u"Read a Book")) + return render_title_template('readpdf.html', pdffile=all_name, title=_(u"Read a Book")) elif format.lower() == "txt": all_name = str(book_id) + "/" + urllib.quote(book.data[0].name) + ".txt" tmp_file = os.path.join(book_dir, urllib.quote(book.data[0].name)) + ".txt" if not os.path.exists(all_name): - txt_file = os.path.join(config.DB_ROOT, book.path, book.data[0].name) + ".txt" + txt_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".txt" copyfile(txt_file, tmp_file) - return render_template('readtxt.html', txtfile=all_name, title=_(u"Read a Book")) + return render_title_template('readtxt.html', txtfile=all_name, title=_(u"Read a Book")) elif format.lower() == "cbr": all_name = str(book_id) + "/" + urllib.quote(book.data[0].name) + ".cbr" tmp_file = os.path.join(book_dir, urllib.quote(book.data[0].name)) + ".cbr" if not os.path.exists(all_name): - cbr_file = os.path.join(config.DB_ROOT, book.path, book.data[0].name) + ".cbr" + cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".cbr" copyfile(cbr_file, tmp_file) - return render_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book")) + return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book")) else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") @@ -1174,7 +1176,7 @@ def get_download_link(book_id, format): if len(author) > 0: file_name = author + '-' + file_name file_name = helper.get_valid_filename(file_name) - response = make_response(send_from_directory(os.path.join(config.DB_ROOT, book.path), data.name + "." + format)) + response = make_response(send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + format)) try: response.headers["Content-Type"]=mimetypes.types_map['.'+format] except: @@ -1193,7 +1195,7 @@ def get_download_link(book_id, format): @app.route('/register', methods=['GET', 'POST']) def register(): error = None - if not config.PUBLIC_REG: + if not config.config_public_reg: abort(404) if current_user is not None and current_user.is_authenticated: return redirect(url_for('index')) @@ -1202,7 +1204,7 @@ def register(): to_save = request.form.to_dict() if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: flash(_(u"Please fill out all fields!"), category="error") - return render_template('register.html', title="register") + return render_title_template('register.html', title=_(u"register")) existing_user = ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).first() existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"]).first() @@ -1218,26 +1220,23 @@ def register(): except: ub.session.rollback() flash(_(u"An unknown error occured. Please try again later."), category="error") - return render_template('register.html', title="register") + return render_title_template('register.html', title=_(u"register")) flash("Your account has been created. Please login.", category="success") return redirect(url_for('login')) else: flash(_(u"This username or email address is already in use."), category="error") - return render_template('register.html', title="register") + return render_title_template('register.html', title=_(u"register")) - return render_template('register.html', title=_(u"register")) + return render_title_template('register.html', title=_(u"register")) @app.route('/login', methods=['GET', 'POST']) def login(): error = None - - if config.config_calibre_dir == None: + if config.is_Calibre_Configured == False: return redirect(url_for('basic_configuration')) - if current_user is not None and current_user.is_authenticated: return redirect(url_for('index')) - if request.method == "POST": form = request.form.to_dict() user = ub.session.query(ub.User).filter(ub.User.nickname == form['username'].strip()).first() @@ -1250,7 +1249,7 @@ def login(): else: flash(_(u"Wrong Username or Password"), category="error") - return render_template('login.html', title=_(u"login")) + return render_title_template('login.html', title=_(u"login")) @app.route('/logout') @@ -1336,7 +1335,7 @@ def create_shelf(): existing_shelf = ub.session.query(ub.Shelf).filter(or_((ub.Shelf.name == to_save["title"])&( ub.Shelf.is_public == 1), (ub.Shelf.name == to_save["title"])& (ub.Shelf.user_id == int(current_user.id)))).first() if existing_shelf: - flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") + flash(_(u"A shelf with the name '%(title)s' already exists.",title=to_save["title"]), category="error") else: try: ub.session.add(shelf) @@ -1344,9 +1343,9 @@ def create_shelf(): flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success") except: flash(_(u"There was an error"), category="error") - return render_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf")) + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf")) else: - return render_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf")) + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf")) @app.route("/shelf/edit/", methods=["GET", "POST"]) @login_required @@ -1369,9 +1368,9 @@ def edit_shelf(shelf_id): flash(_(u"Shelf %(title)s changed",title=to_save["title"]), category="success") except: flash(_(u"There was an error"), category="error") - return render_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf")) + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf")) else: - return render_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf")) + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf")) @@ -1413,7 +1412,7 @@ def show_shelf(shelf_id): cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() result.append(cur_book) - return render_template('shelf.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), shelf=shelf) + return render_title_template('shelf.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), shelf=shelf) @app.route("/shelf/order/", methods=["GET", "POST"]) @@ -1437,11 +1436,13 @@ def order_shelf(shelf_id): ub.Shelf.id == shelf_id))).first() result = list() if shelf: - books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all() + books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\ + .order_by(ub.BookShelf.order.asc()).all() for book in books_in_shelf2: cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() result.append(cur_book) - return render_template('shelf_order.html', entries=result, title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), shelf=shelf) + return render_title_template('shelf_order.html', entries=result, + title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), shelf=shelf) @app.route("/me", methods=["GET", "POST"]) @@ -1502,11 +1503,11 @@ def profile(): except IntegrityError: ub.session.rollback() flash(_(u"Found an existing account for this email address."), category="error") - return render_template("user_edit.html", content=content, downloads=downloads, + return render_title_template("user_edit.html", content=content, downloads=downloads, title=_(u"%(name)s's profile", name=current_user.nickname)) flash(_(u"Profile updated"), category="success") - return render_template("user_edit.html", translations=translations, profile=1, languages=languages, content=content, - downloads=downloads, title=_(u"%(name)s's profile", name=current_user.nickname)) + return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages, content=content, + downloads=downloads,title=_(u"%(name)s's profile", name=current_user.nickname)) @app.route("/admin/view") @@ -1515,33 +1516,42 @@ def profile(): def admin(): content = ub.session.query(ub.User).all() settings = ub.session.query(ub.Settings).first() - return render_template("admin.html", content=content, email=settings, config=config, title=_(u"Admin page")) + return render_title_template("admin.html", content=content, email=settings, config=config, title=_(u"Admin page")) -@app.route("/admin/config") +@app.route("/admin/config", methods=["GET", "POST"]) @login_required @admin_required def configuration(): - return render_template("config_edit.html", content=config, title=_(u"Basic Configuration")) + return configuration_helper() @app.route("/config", methods=["GET", "POST"] ) @unconfigured def basic_configuration(): + return configuration_helper() + +def configuration_helper(): global global_task + reboot_required= False if request.method == "POST": to_save = request.form.to_dict() - content = ub.session.query(ub.Settings).first() + content = ub.session.query(ub.Settings).first() # ToDo replace content with config ? if "config_calibre_dir" in to_save: - content.config_calibre_dir = to_save["config_calibre_dir"] + if content.config_calibre_dir != to_save["config_calibre_dir"]: + content.config_calibre_dir = to_save["config_calibre_dir"] + reboot_required = True if "config_port" in to_save: - content.config_port = to_save["config_port"] - if "config_calibre_web_title" in to_save: - content.config_calibre_web_title = to_save["config_calibre_web_title"] + if content.config_port != int(to_save["config_port"]): + content.config_port = int(to_save["config_port"]) + reboot_required = True if "config_calibre_web_title" in to_save: content.config_calibre_web_title = to_save["config_calibre_web_title"] if "config_title_regex" in to_save: - content.config_title_regex = to_save["config_title_regex"] + if content.config_title_regex != to_save["config_title_regex"]: + content.config_title_regex = to_save["config_title_regex"] + reboot_required = True if "config_log_level" in to_save: content.config_log_level = to_save["config_log_level"] + # ToDo check reboot required if "config_random_books" in to_save: content.config_random_books = int(to_save["config_random_books"]) if "config_books_per_page" in to_save: @@ -1561,16 +1571,19 @@ def basic_configuration(): config.loadSettings() except e: flash(e, category="error") - return render_template("config_edit.html", content=config, title=_(u"Basic Configuration")) - - ub.session.close() - ub.engine.dispose() - # stop tornado server - server = IOLoop.instance() - server.add_callback(server.stop) - global_task = 0 + return render_title_template("config_edit.html", content=config, title=_(u"Basic Configuration")) - return render_template("config_edit.html", content=config, title=_(u"Basic Configuration")) + if reboot_required: + if config.is_Calibre_Configured: + db.session.close() + # db.engine.dispose() # ToDo verify correct + ub.session.close() + ub.engine.dispose() + # stop tornado server + server = IOLoop.instance() + server.add_callback(server.stop) + global_task = 0 + return render_title_template("config_edit.html", content=config, title=_(u"Basic Configuration")) @app.route("/admin/user/new", methods=["GET", "POST"]) @@ -1590,7 +1603,7 @@ def new_user(): to_save = request.form.to_dict() if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: flash(_(u"Please fill out all fields!"), category="error") - return render_template("user_edit.html", new_user=1, content=content, title=_(u"Add new user")) + return render_title_template("user_edit.html", new_user=1, content=content, title=_(u"Add new user")) content.password = generate_password_hash(to_save["password"]) content.nickname = to_save["nickname"] content.email = to_save["email"] @@ -1629,7 +1642,7 @@ def new_user(): except IntegrityError: ub.session.rollback() flash(_(u"Found an existing account for this email address or nickname."), category="error") - return render_template("user_edit.html", new_user=1, content=content, translations=translations, + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, languages=languages, title=_(u"Add new user")) @@ -1658,7 +1671,7 @@ def edit_mailsettings(): category="success") else: flash(_(u"There was an error sending the Test E-Mail: %(res)s", res=result), category="error") - return render_template("email_edit.html", content=content, title=_(u"Edit mail settings")) + return render_title_template("email_edit.html", content=content, title=_(u"Edit mail settings")) @app.route("/admin/user/", methods=["GET", "POST"]) @@ -1745,7 +1758,7 @@ def edit_user(user_id): except IntegrityError: ub.session.rollback() flash(_(u"An unknown error occured."), category="error") - return render_template("user_edit.html", translations=translations, languages=languages, new_user=0, + return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, content=content, downloads=downloads, title=_(u"Edit User %(nick)s", nick=content.nickname)) @@ -1787,7 +1800,7 @@ def edit_book(book_id): if to_save["cover_url"] and os.path.splitext(to_save["cover_url"])[1].lower() == ".jpg": img = requests.get(to_save["cover_url"]) - f = open(os.path.join(config.DB_ROOT, book.path, "cover.jpg"), "wb") + f = open(os.path.join(config.config_calibre_dir, book.path, "cover.jpg"), "wb") f.write(img.content) f.close() @@ -1950,9 +1963,9 @@ def edit_book(book_id): if "detail_view" in to_save: return redirect(url_for('show_book', id=book.id)) else: - return render_template('book_edit.html', book=book, authors=author_names, cc=cc) + return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, title=_(u"edit metadata")) else: - return render_template('book_edit.html', book=book, authors=author_names, cc=cc) + return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, title=_(u"edit metadata")) else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") return redirect(url_for("index")) @@ -1962,7 +1975,7 @@ def edit_book(book_id): @login_required_if_no_ano @upload_required def upload(): - if not config.UPLOADING: + if not config.config_uploading: abort(404) # create the function for sorting... db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) @@ -1977,7 +1990,7 @@ def upload(): title_dir = helper.get_valid_filename(title.decode('utf-8'), False) author_dir = helper.get_valid_filename(author.decode('utf-8'), False) data_name = title_dir - filepath = config.DB_ROOT + os.sep + author_dir + os.sep + title_dir + filepath = config.config_calibre_dir + os.sep + author_dir + os.sep + title_dir saved_filename = filepath + os.sep + data_name + meta.extension if not os.path.exists(filepath): @@ -2025,6 +2038,6 @@ def upload(): author_names.append(author.name) cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() if current_user.role_edit() or current_user.role_admin(): - return render_template('book_edit.html', book=db_book, authors=author_names, cc=cc) + return render_title_template('book_edit.html', book=db_book, authors=author_names, cc=cc, title=_(u"edit metadata")) book_in_shelfs = [] - return render_template('detail.html', entry=db_book, cc=cc, title=db_book.title, books_shelfs=book_in_shelfs) + return render_title_template('detail.html', entry=db_book, cc=cc,title=db_book.title, books_shelfs=book_in_shelfs, ) From 2c615fdf050e5d360cd20b70c96b18ba842bb8aa Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Sat, 28 Jan 2017 20:16:40 +0100 Subject: [PATCH 04/19] Finalize graphical setup for calibre-web --- cps.py | 18 +- cps/db.py | 41 +++- cps/fb2.py | 2 +- cps/helper.py | 43 ++-- cps/templates/admin.html | 2 +- cps/templates/config_edit.html | 22 +- cps/templates/index.html | 2 +- cps/templates/stats.html | 8 +- cps/templates/user_edit.html | 18 +- cps/ub.py | 244 ++++++++++++------- cps/web.py | 429 +++++++++++++++++++-------------- 11 files changed, 506 insertions(+), 323 deletions(-) diff --git a/cps.py b/cps.py index c2913dc3..b8f51e02 100755 --- a/cps.py +++ b/cps.py @@ -3,27 +3,25 @@ import sys import time base_path = os.path.dirname(os.path.abspath(__file__)) - # Insert local directories into path sys.path.insert(0, os.path.join(base_path, 'vendor')) from cps import web -# from cps import config from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop if __name__ == '__main__': - '''if config.DEVELOPMENT: - web.app.run(host="0.0.0.0", port=web.config.config_port, debug=True) - else:''' - http_server = HTTPServer(WSGIContainer(web.app)) - http_server.listen(web.config.config_port) - IOLoop.instance().start() + if web.ub.DEVELOPMENT: + web.app.run(host="0.0.0.0", port=web.ub.config.config_port, debug=True) + else: + http_server = HTTPServer(WSGIContainer(web.app)) + http_server.listen(web.ub.config.config_port) + IOLoop.instance().start() if web.global_task == 0: print("Performing restart of Calibre-web") - os.execl(sys.executable,sys.executable, *sys.argv) + os.execl(sys.executable, sys.executable, *sys.argv) else: print("Performing shutdown of Calibre-web") - os._exit(0) + sys.exit(0) diff --git a/cps/db.py b/cps/db.py index a16672ae..d918a032 100755 --- a/cps/db.py +++ b/cps/db.py @@ -7,12 +7,21 @@ from sqlalchemy.orm import * import os import re import ast -from ub import Config +from ub import config +import ub + +session = None +cc_exceptions = None +cc_classes = None +cc_ids = None +books_custom_column_links = None +engine = None + # user defined sort function for calibre databases (Series, etc.) def title_sort(title): # calibre sort stuff - config=Config() + # config=Config() title_pat = re.compile(config.config_title_regex, re.IGNORECASE) match = title_pat.search(title) if match: @@ -216,9 +225,10 @@ class Books(Base): series = relationship('Series', secondary=books_series_link, backref='books') ratings = relationship('Ratings', secondary=books_ratings_link, backref='books') languages = relationship('Languages', secondary=books_languages_link, backref='books') - identifiers=relationship('Identifiers', backref='books') + identifiers = relationship('Identifiers', backref='books') - def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover, authors, tags): + def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover, + authors, tags): # ToDO check Authors and tags necessary self.title = title self.sort = sort self.author_sort = author_sort @@ -253,19 +263,33 @@ class Custom_Columns(Base): return display_dict -def setup_db(config): +def setup_db(): global session global cc_exceptions global cc_classes global cc_ids global books_custom_column_links + global engine - if config.config_calibre_dir is None: - return + if config.config_calibre_dir is None or config.config_calibre_dir == u'': + return False dbpath = os.path.join(config.config_calibre_dir, "metadata.db") engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False) - conn = engine.connect() + try: + conn = engine.connect() + + except: + content = ub.session.query(ub.Settings).first() + content.config_calibre_dir = None + content.db_configured = False + ub.session.commit() + config.loadSettings() + return False + content = ub.session.query(ub.Settings).first() + content.db_configured = True + ub.session.commit() + config.loadSettings() conn.connection.create_function('title_sort', 1, title_sort) cc = conn.execute("SELECT id, datatype FROM custom_columns") @@ -310,3 +334,4 @@ def setup_db(config): Session = sessionmaker() Session.configure(bind=engine) session = Session() + return True \ No newline at end of file diff --git a/cps/fb2.py b/cps/fb2.py index 93e3dcc2..ccc85207 100644 --- a/cps/fb2.py +++ b/cps/fb2.py @@ -3,7 +3,7 @@ from lxml import etree import os import uploader - +# ToDo: Check usage of original_file_name def get_fb2_info(tmp_file_path, original_file_name, original_file_extension): ns = { diff --git a/cps/helper.py b/cps/helper.py index 7812131c..d861af38 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import db, ub -# import config +import db +import ub from flask import current_app as app import logging import smtplib @@ -33,8 +33,9 @@ def update_download(book_id, user_id): ub.session.commit() -def make_mobi(book_id,calibrepath): - vendorpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + "../vendor" + os.sep)) +def make_mobi(book_id, calibrepath): + vendorpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + + os.sep + "../vendor" + os.sep)) if sys.platform == "win32": kindlegen = os.path.join(vendorpath, u"kindlegen.exe") else: @@ -80,18 +81,20 @@ def make_mobi(book_id,calibrepath): class StderrLogger(object): - buffer='' + buffer = '' + def __init__(self): self.logger = logging.getLogger('cps.web') def write(self, message): - if message=='\n': + if message == '\n': self.logger.debug(self.buffer) - self.buffer='' + self.buffer = '' else: - self.buffer=self.buffer+message + self.buffer += message + -def send_raw_email(kindle_mail,msg): +def send_raw_email(kindle_mail, msg): settings = ub.get_mail_settings() msg['From'] = settings["mail_from"] @@ -107,7 +110,7 @@ def send_raw_email(kindle_mail,msg): # send email try: - timeout=600 # set timeout to 5mins + timeout = 600 # set timeout to 5mins org_stderr = smtplib.stderr smtplib.stderr = StderrLogger() @@ -140,18 +143,11 @@ def send_test_mail(kindle_mail): msg['Subject'] = _(u'Calibre-web test email') text = _(u'This email has been sent via calibre web.') msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')) - return send_raw_email(kindle_mail,msg) + return send_raw_email(kindle_mail, msg) -def send_mail(book_id, kindle_mail,calibrepath): +def send_mail(book_id, kindle_mail, calibrepath): """Send email with attachments""" - is_mobi = False - is_azw = False - is_azw3 = False - is_epub = False - is_pdf = False - file_path = None - settings = ub.get_mail_settings() # create MIME message msg = MIMEMultipart() msg['Subject'] = _(u'Send to Kindle') @@ -177,7 +173,7 @@ def send_mail(book_id, kindle_mail,calibrepath): if 'mobi' in formats: msg.attach(get_attachment(formats['mobi'])) elif 'epub' in formats: - filepath = make_mobi(book.id,calibrepath) + filepath = make_mobi(book.id, calibrepath) if filepath is not None: msg.attach(get_attachment(filepath)) elif filepath is None: @@ -207,8 +203,7 @@ def get_attachment(file_path): return attachment except IOError: traceback.print_exc() - message = (_('The requested file could not be read. Maybe wrong '\ - 'permissions?')) + message = (_('The requested file could not be read. Maybe wrong permissions?')) # ToDo: What is this? return None @@ -218,7 +213,7 @@ def get_valid_filename(value, replace_whitespace=True): filename. Limits num characters to 128 max. """ value = value[:128] - re_slugify = re.compile('[^\w\s-]', re.UNICODE) + # re_slugify = re.compile('[^\w\s-]', re.UNICODE) value = unicodedata.normalize('NFKD', value) re_slugify = re.compile('[^\w\s-]', re.UNICODE) value = unicode(re_slugify.sub('', value).strip()) @@ -238,7 +233,7 @@ def get_normalized_author(value): return value -def update_dir_stucture(book_id,calibrepath): +def update_dir_stucture(book_id, calibrepath): db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) book = db.session.query(db.Books).filter(db.Books.id == book_id).first() path = os.path.join(calibrepath, book.path) diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 69cadaee..9ac1c858 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -75,7 +75,7 @@

{{_('Administration')}}

- {% if not config.DEVELOPMENT %} + {% if not development %}
{{_('Restart Calibre-web')}}
{{_('Stop Calibre-web')}}
{% endif %} diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 80c5d7f7..2dac08f6 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -9,7 +9,7 @@
- +
@@ -17,11 +17,11 @@
- +
- +
@@ -31,10 +31,10 @@
@@ -48,8 +48,14 @@
-
+
+ {% if not origin %} + {{_('Back')}} + {% endif %} + {% if success %} + {{_('Login')}} + {% endif %}
{% endblock %} diff --git a/cps/templates/index.html b/cps/templates/index.html index 50755bbf..9abbaff1 100755 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -1,6 +1,6 @@ {% extends "layout.html" %} {% block body %} -{% if g.user.show_random_books() %} +{% if g.user.show_detail_random() %}

{{_('Discover (Random Books)')}}

diff --git a/cps/templates/stats.html b/cps/templates/stats.html index 998ed65e..49c13fc4 100644 --- a/cps/templates/stats.html +++ b/cps/templates/stats.html @@ -12,19 +12,19 @@ Python - {{Versions['PythonVersion']}} + {{versions['PythonVersion']}} Kindlegen - {{Versions['KindlegenVersion']}} + {{versions['KindlegenVersion']}} ImageMagick - {{Versions['ImageVersion']}} + {{versions['ImageVersion']}} PyPDF2 - {{Versions['PyPdfVersion']}} + {{versions['PyPdfVersion']}} diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index d9b6f466..674ca2aa 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -41,25 +41,33 @@
- +
- +
- +
- +
- +
+
+ + +
+
+ + +
{% if g.user and g.user.role_admin() and not profile %} {% if not content.role_anonymous() %} diff --git a/cps/ub.py b/cps/ub.py index 0268751b..d5612fdb 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -8,24 +8,41 @@ from sqlalchemy.orm import * from flask_login import AnonymousUserMixin import os import traceback +import logging from werkzeug.security import generate_password_hash from flask_babel import gettext as _ -dbpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__))+os.sep+".."+os.sep), "app.db") +dbpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep), "app.db") engine = create_engine('sqlite:///{0}'.format(dbpath), echo=False) Base = declarative_base() ROLE_USER = 0 ROLE_ADMIN = 1 ROLE_DOWNLOAD = 2 -ROLE_UPLOAD = 4 +ROLE_UPLOAD = 4 ROLE_EDIT = 8 ROLE_PASSWD = 16 ROLE_ANONYMOUS = 32 + +DETAIL_RANDOM = 1 +SIDEBAR_LANGUAGE = 2 +SIDEBAR_SERIES = 4 +SIDEBAR_CATEGORY = 8 +SIDEBAR_HOT = 16 +SIDEBAR_RANDOM = 32 +SIDEBAR_AUTHOR = 64 + DEFAULT_PASS = "admin123" -class UserBase(): + +DEVELOPMENT = False + + + + +class UserBase: + @staticmethod def is_authenticated(self): return True @@ -78,57 +95,55 @@ class UserBase(): return self.default_language def show_random_books(self): - return self.random_books + if self.sidebar_view is not None: + return True if self.sidebar_view & SIDEBAR_RANDOM == SIDEBAR_RANDOM else False + else: + return False def show_language(self): - return self.language_books + if self.sidebar_view is not None: + return True if self.sidebar_view & SIDEBAR_LANGUAGE == SIDEBAR_LANGUAGE else False + else: + return False def show_hot_books(self): - return self.hot_books + if self.sidebar_view is not None: + return True if self.sidebar_view & SIDEBAR_HOT == SIDEBAR_HOT else False + else: + return False def show_series(self): - return self.series_books + if self.sidebar_view is not None: + return True if self.sidebar_view & SIDEBAR_SERIES == SIDEBAR_SERIES else False + else: + return False def show_category(self): - return self.category_books - - def __repr__(self): - return '' % self.nickname + if self.sidebar_view is not None: + return True if self.sidebar_view & SIDEBAR_CATEGORY == SIDEBAR_CATEGORY else False + else: + return False -class Config(): - def __init__(self): - self.config_main_dir=os.path.join(os.path.normpath(os.path.dirname( - os.path.realpath(__file__)) + os.sep + ".." + os.sep)) - self.db_configured=None - self.loadSettings() + def show_author(self): + if self.sidebar_view is not None: + return True if self.sidebar_view & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR else False + else: + return False - def loadSettings(self): - data=session.query(Settings).first() - self.config_calibre_dir = data.config_calibre_dir - self.config_port = data.config_port - self.config_calibre_web_title = data.config_calibre_web_title - self.config_books_per_page = data.config_books_per_page - self.config_random_books = data.config_random_books - self.config_title_regex = data.config_title_regex - self.config_log_level = data.config_log_level - self.config_uploading = data.config_uploading - self.config_anonbrowse = data.config_anonbrowse - self.config_public_reg = data.config_public_reg - if self.config_calibre_dir is not None and (self.db_configured is None or self.db_configured is True): - self.db_configured=True + def show_detail_random(self): + if self.sidebar_view is not None: + return True if self.sidebar_view & DETAIL_RANDOM == DETAIL_RANDOM else False else: - self.db_configured = False + return False - @property - def get_main_dir(self): - return self.config_main_dir - @property - def is_Calibre_Configured(self): - return self.db_configured + def __repr__(self): + return '' % self.nickname -class User(UserBase,Base): +# Baseclass for Users in Calibre-web, settings which are depending on certain users are stored here. It is derived from +# User Base (all access methods are declared there) +class User(UserBase, Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) @@ -140,31 +155,31 @@ class User(UserBase,Base): shelf = relationship('Shelf', backref='user', lazy='dynamic') downloads = relationship('Downloads', backref='user', lazy='dynamic') locale = Column(String(2), default="en") - random_books = Column(Integer, default=1) - language_books = Column(Integer, default=1) - series_books = Column(Integer, default=1) - category_books = Column(Integer, default=1) - hot_books = Column(Integer, default=1) + sidebar_view = Column(Integer, default=1) + #language_books = Column(Integer, default=1) + #series_books = Column(Integer, default=1) + #category_books = Column(Integer, default=1) + #hot_books = Column(Integer, default=1) default_language = Column(String(3), default="all") -class Anonymous(AnonymousUserMixin,UserBase): - # anon_browse = None - +# Class for anonymous user is derived from User base and complets overrides methods and properties for the +# anonymous user +class Anonymous(AnonymousUserMixin, UserBase): def __init__(self): self.loadSettings() def loadSettings(self): - data=session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() - settings=session.query(Settings).first() + data = session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() + settings = session.query(Settings).first() self.nickname = data.nickname self.role = data.role - self.random_books = data.random_books + self.sidebar_view = data.sidebar_view self.default_language = data.default_language - self.language_books = data.language_books - self.series_books = data.series_books - self.category_books = data.category_books - self.hot_books = data.hot_books + #self.language_books = data.language_books + #self.series_books = data.series_books + #self.category_books = data.category_books + #self.hot_books = data.hot_books self.default_language = data.default_language self.locale = data.locale self.anon_browse = settings.config_anonbrowse @@ -179,6 +194,7 @@ class Anonymous(AnonymousUserMixin,UserBase): return self.anon_browse +# Baseclass representing Shelfs in calibre-web inapp.db class Shelf(Base): __tablename__ = 'shelf' @@ -190,6 +206,8 @@ class Shelf(Base): def __repr__(self): return '' % self.name + +# Baseclass representing Relationship between books and Shelfs in Calibre-web in app.db (N:M) class BookShelf(Base): __tablename__ = 'book_shelf_link' @@ -202,6 +220,7 @@ class BookShelf(Base): return '' % self.id +# Baseclass representing Downloads from calibre-web in app.db class Downloads(Base): __tablename__ = 'downloads' @@ -212,54 +231,79 @@ class Downloads(Base): def __repr__(self): return '' % (self.mail_server) pass +# Class holds all application specific settings in calibre-web +class Config: + def __init__(self): + self.config_main_dir = os.path.join(os.path.normpath(os.path.dirname( + os.path.realpath(__file__)) + os.sep + ".." + os.sep)) + self.db_configured = None + self.loadSettings() + + def loadSettings(self): + data = session.query(Settings).first() + self.config_calibre_dir = data.config_calibre_dir + self.config_port = data.config_port + self.config_calibre_web_title = data.config_calibre_web_title + self.config_books_per_page = data.config_books_per_page + self.config_random_books = data.config_random_books + self.config_title_regex = data.config_title_regex + self.config_log_level = data.config_log_level + self.config_uploading = data.config_uploading + self.config_anonbrowse = data.config_anonbrowse + self.config_public_reg = data.config_public_reg + if self.config_calibre_dir is not None: # and (self.db_configured is None or self.db_configured is True): + self.db_configured = True + else: + self.db_configured = False + + @property + def get_main_dir(self): + return self.config_main_dir + + #def is_Calibre_Configured(self): + # return self.db_configured + + +# Migrate database to current version, has to be updated after every database change. Currently migration from +# everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding +# rows with SQL commands def migrate_Database(): - if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None: - create_anonymous_user() try: - session.query(exists().where(User.random_books)).scalar() + session.query(exists().where(User.locale)).scalar() session.commit() except exc.OperationalError: # Database is not compatible, some rows are missing conn = engine.connect() - conn.execute("ALTER TABLE user ADD column random_books INTEGER DEFAULT 1") conn.execute("ALTER TABLE user ADD column locale String(2) DEFAULT 'en'") conn.execute("ALTER TABLE user ADD column default_language String(3) DEFAULT 'all'") session.commit() - try: - session.query(exists().where(User.language_books)).scalar() - session.commit() - except exc.OperationalError: # Database is not compatible, some rows are missing - conn = engine.connect() - conn.execute("ALTER TABLE user ADD column language_books INTEGER DEFAULT 1") - conn.execute("ALTER TABLE user ADD column series_books INTEGER DEFAULT 1") - conn.execute("ALTER TABLE user ADD column category_books INTEGER DEFAULT 1") - conn.execute("ALTER TABLE user ADD column hot_books INTEGER DEFAULT 1") - session.commit() try: session.query(exists().where(Settings.config_calibre_dir)).scalar() session.commit() @@ -270,12 +314,34 @@ def migrate_Database(): conn.execute("ALTER TABLE Settings ADD column `config_calibre_web_title` String DEFAULT 'Calibre-web'") conn.execute("ALTER TABLE Settings ADD column `config_books_per_page` INTEGER DEFAULT 60") conn.execute("ALTER TABLE Settings ADD column `config_random_books` INTEGER DEFAULT 4") - conn.execute("ALTER TABLE Settings ADD column `config_title_regex` String DEFAULT '^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+'") - conn.execute("ALTER TABLE Settings ADD column `config_log_level` String DEFAULT 'INFO'") + conn.execute("ALTER TABLE Settings ADD column `config_title_regex` String DEFAULT " + "'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+'") + conn.execute("ALTER TABLE Settings ADD column `config_log_level` SmallInteger DEFAULT '" + logging.INFO + "'") conn.execute("ALTER TABLE Settings ADD column `config_uploading` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0") session.commit() + try: + create = False + session.query(exists().where(User.sidebar_view)).scalar() + session.commit() + except exc.OperationalError: # Database is not compatible, some rows are missing + conn = engine.connect() + conn.execute("ALTER TABLE user ADD column `sidebar_view` Integer DEFAULT 1") + session.commit() + create=True + try: + if create: + conn.execute("SELET language_books FROM user") + session.commit() + except exc.OperationalError: + conn = engine.connect() + conn.execute("UPDATE user SET 'sidebar_view' = (random_books*"+str(SIDEBAR_RANDOM)+"+ language_books *"+ + str(SIDEBAR_LANGUAGE)+"+ series_books *"+str(SIDEBAR_SERIES)+"+ category_books *"+str(SIDEBAR_CATEGORY)+ + "+ hot_books *"+str(SIDEBAR_HOT)+"+"+str(SIDEBAR_AUTHOR)+"+"+str(DETAIL_RANDOM)+")") + session.commit() + if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None: + create_anonymous_user() def create_default_config(): settings = Settings() @@ -307,10 +373,12 @@ def get_mail_settings(): return data + +# Generate user Guest (translated text), as anoymous user, no rights def create_anonymous_user(): user = User() user.nickname = _("Guest") - user.email='no@email' + user.email = 'no@email' user.role = ROLE_ANONYMOUS user.password = generate_password_hash('1') @@ -322,10 +390,14 @@ def create_anonymous_user(): pass +# Generate User admin with admin123 password, and access to everything def create_admin_user(): user = User() user.nickname = "admin" user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_PASSWD + user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \ + SIDEBAR_RANDOM + SIDEBAR_AUTHOR + user.password = generate_password_hash(DEFAULT_PASS) session.add(user) @@ -335,10 +407,13 @@ def create_admin_user(): session.rollback() pass + +# Open session for database connection Session = sessionmaker() Session.configure(bind=engine) session = Session() +# generate database and admin and guest user, if no database is existing if not os.path.exists(dbpath): try: Base.metadata.create_all(engine) @@ -349,3 +424,6 @@ if not os.path.exists(dbpath): pass else: migrate_Database() + +# Generate global Settings Object accecable from every file +config = Config() diff --git a/cps/web.py b/cps/web.py index dd88e66b..035e3631 100755 --- a/cps/web.py +++ b/cps/web.py @@ -1,13 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - import mimetypes import logging from logging.handlers import RotatingFileHandler import textwrap from flask import Flask, render_template, session, request, Response, redirect, url_for, send_from_directory, \ make_response, g, flash, abort -import ub, helper +import ub +from ub import config +import helper import os import errno from sqlalchemy.sql.expression import func @@ -18,7 +19,8 @@ from flask_login import LoginManager, login_user, logout_user, login_required, c from flask_principal import Principal, Identity, AnonymousIdentity, identity_changed from flask_babel import Babel from flask_babel import gettext as _ -import requests, zipfile +import requests +import zipfile from werkzeug.security import generate_password_hash, check_password_hash from babel import Locale as LC from babel import negotiate_locale @@ -40,16 +42,17 @@ from tornado.ioloop import IOLoop try: from wand.image import Image + use_generic_pdf_cover = False except ImportError, e: use_generic_pdf_cover = True from cgi import escape -########################################## Global variables ######################################################## +# Global variables global_task = None -########################################## Proxy Helper class ###################################################### +# Proxy Helper class class ReverseProxied(object): """Wrap the application in this middleware and configure the front-end server to add these headers, to let you quietly bind @@ -68,8 +71,8 @@ class ReverseProxied(object): } """ - def __init__(self, app): - self.app = app + def __init__(self, application): + self.app = application def __call__(self, environ, start_response): script_name = environ.get('HTTP_X_SCRIPT_NAME', '') @@ -87,7 +90,8 @@ class ReverseProxied(object): environ['HTTP_HOST'] = server return self.app(environ, start_response) -########################################## Main code ############################################################## + +# Main code mimetypes.init() mimetypes.add_type('application/xhtml+xml', '.xhtml') mimetypes.add_type('application/epub+zip', '.epub') @@ -99,23 +103,19 @@ mimetypes.add_type('application/x-cbz', '.cbz') mimetypes.add_type('application/x-cbt', '.cbt') mimetypes.add_type('image/vnd.djvu', '.djvu') - app = (Flask(__name__)) app.wsgi_app = ReverseProxied(app.wsgi_app) '''formatter = logging.Formatter( "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s") -file_handler = RotatingFileHandler(os.path.join(config.LOG_DIR, "calibre-web.log"), maxBytes=50000, backupCount=1) +file_handler = RotatingFileHandler(os.path.join(config.get_main_dir, "calibre-web.log"), maxBytes=50000, backupCount=1) file_handler.setFormatter(formatter) app.logger.addHandler(file_handler) -if config.DEVELOPMENT: - app.logger.setLevel(logging.DEBUG) -else: - app.logger.setLevel(logging.INFO) +app.logger.setLevel(config.config_log_level) app.logger.info('Starting Calibre Web...') logging.getLogger("book_formats").addHandler(file_handler) -logging.getLogger("book_formats").setLevel(logging.INFO)''' +logging.getLogger("book_formats").setLevel(config.config_log_level)''' Principal(app) @@ -123,10 +123,6 @@ babel = Babel(app) import uploader -# establish connection to calibre-db -config=ub.Config() -db.setup_db(config) - lm = LoginManager(app) lm.init_app(app) lm.login_view = 'login' @@ -134,7 +130,7 @@ lm.anonymous_user = ub.Anonymous app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' - +db.setup_db() @babel.localeselector def get_locale(): @@ -142,7 +138,7 @@ def get_locale(): user = getattr(g, 'user', None) if user is not None and hasattr(user, "locale"): return user.locale - translations=[item.language for item in babel.list_translations()]+ ['en'] + translations = [item.language for item in babel.list_translations()] + ['en'] preferred = [x.replace('-', '_') for x in request.accept_languages.values()] return negotiate_locale(preferred, translations) @@ -155,14 +151,15 @@ def get_timezone(): @lm.user_loader -def load_user(id): - return ub.session.query(ub.User).filter(ub.User.id == int(id)).first() +def load_user(user_id): + return ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @lm.header_loader def load_user_from_header(header_val): if header_val.startswith('Basic '): header_val = header_val.replace('Basic ', '', 1) + basic_username = basic_password = '' try: header_val = base64.b64decode(header_val) basic_username = header_val.split(':')[0] @@ -215,7 +212,7 @@ class Pagination(object): @property def previous_offset(self): - return int((self.page-2) * self.per_page) + return int((self.page - 2) * self.per_page) @property def last_offset(self): @@ -239,11 +236,9 @@ class Pagination(object): def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2): last = 0 - for num in xrange(1, self.pages + 1): - if num <= left_edge or \ - (num > self.page - left_current - 1 and \ - num < self.page + right_current) or \ - num > self.pages - right_edge: + for num in xrange(1, self.pages + 1): # ToDo: can be simplified + if num <= left_edge or (num > self.page - left_current - 1 and num < self.page + right_current) \ + or num > self.pages - right_edge: if last + 1 != num: yield None yield num @@ -275,12 +270,13 @@ def shortentitle_filter(s): s = textwrap.wrap(s, 60, break_long_words=False)[0] + ' [...]' return s + @app.template_filter('mimetype') def mimetype_filter(val): try: - s = mimetypes.types_map['.'+val] + s = mimetypes.types_map['.' + val] except: - s= 'application/octet-stream' + s = 'application/octet-stream' return s @@ -288,22 +284,27 @@ def admin_required(f): """ Checks if current_user.role == 1 """ + @wraps(f) def inner(*args, **kwargs): if current_user.role_admin(): return f(*args, **kwargs) abort(403) + return inner + def unconfigured(f): """ Checks if current_user.role == 1 """ + @wraps(f) def inner(*args, **kwargs): - if config.is_Calibre_Configured: + if not config.db_configured: return f(*args, **kwargs) abort(403) + return inner @@ -313,6 +314,7 @@ def download_required(f): if current_user.role_download() or current_user.role_admin(): return f(*args, **kwargs) abort(403) + return inner @@ -322,6 +324,7 @@ def upload_required(f): if current_user.role_upload() or current_user.role_admin(): return f(*args, **kwargs) abort(403) + return inner @@ -331,6 +334,7 @@ def edit_required(f): if current_user.role_edit() or current_user.role_admin(): return f(*args, **kwargs) abort(403) + return inner @@ -340,7 +344,7 @@ def fill_indexpage(page, database, db_filter, order): filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) else: filter = True - if current_user.show_random_books(): + if current_user.show_detail_random(): random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_random_books) else: random = false @@ -405,19 +409,23 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session # add element to book db_book_object.append(new_element) + def render_title_template(*args, **kwargs): return render_template(instance=config.config_calibre_web_title, *args, **kwargs) - @app.before_request def before_request(): + if ub.DEVELOPMENT: + reload(ub) g.user = current_user - g.public_shelfes = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1).all() g.allow_registration = config.config_public_reg g.allow_upload = config.config_uploading + if not config.db_configured and request.endpoint not in ('basic_configuration', 'login') and '/static/' not in request.path: + return redirect(url_for('basic_configuration')) + -########################################## Routing functions ####################################################### +# Routing functions @app.route("/opds") @requires_basic_auth_if_no_ano @@ -440,6 +448,7 @@ def feed_osd(): response.headers["Content-Type"] = "application/xml" return response + @app.route("/opds/search/") @requires_basic_auth_if_no_ano def feed_cc_search(query): @@ -462,7 +471,7 @@ def feed_search(term): db.Books.authors.any(db.Authors.name.like("%" + term + "%")), db.Books.title.like("%" + term + "%"))).filter(filter).all() entriescount = len(entries) if len(entries) > 0 else 1 - pagination = Pagination( 1,entriescount,entriescount) + pagination = Pagination(1, entriescount, entriescount) xml = render_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) else: xml = render_template('feed.xml', searchterm="") @@ -483,7 +492,7 @@ def feed_new(): off = 0 entries = db.session.query(db.Books).filter(filter).order_by(db.Books.timestamp.desc()).offset(off).limit( config.config_books_per_page) - pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Books).filter(filter).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) @@ -502,7 +511,7 @@ def feed_discover(): # if not off: # off = 0 entries = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_books_per_page) - pagination = Pagination(1, config.config_books_per_page,int(config.config_books_per_page)) + pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page)) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -521,8 +530,9 @@ def feed_hot(): off = 0 entries = db.session.query(db.Books).filter(filter).filter(db.Books.ratings.any(db.Ratings.rating > 9)).offset( off).limit(config.config_books_per_page) - pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, - len(db.session.query(db.Books).filter(filter).filter(db.Books.ratings.any(db.Ratings.rating > 9)).all())) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Books).filter(filter).filter( + db.Books.ratings.any(db.Ratings.rating > 9)).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -541,7 +551,7 @@ def feed_authorindex(): if not off: off = 0 authors = db.session.query(db.Authors).order_by(db.Authors.sort).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Authors).all())) xml = render_template('feed.xml', authors=authors, pagination=pagination) response = make_response(xml) @@ -559,10 +569,11 @@ def feed_author(id): filter = True if not off: off = 0 - entries = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id )).filter( + entries = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id)).filter( filter).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, - len(db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id )).filter(filter).all())) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id)).filter( + filter).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -576,7 +587,7 @@ def feed_categoryindex(): if not off: off = 0 entries = db.session.query(db.Tags).order_by(db.Tags.name).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Tags).all())) xml = render_template('feed.xml', categorys=entries, pagination=pagination) response = make_response(xml) @@ -594,10 +605,11 @@ def feed_category(id): filter = True if not off: off = 0 - entries = db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id==id)).order_by( + entries = db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id == id)).order_by( db.Books.timestamp.desc()).filter(filter).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, - len(db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id==id)).filter(filter).all())) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id == id)).filter( + filter).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -615,7 +627,7 @@ def feed_seriesindex(): if not off: off = 0 entries = db.session.query(db.Series).order_by(db.Series.name).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Series).all())) xml = render_template('feed.xml', series=entries, pagination=pagination) response = make_response(xml) @@ -635,8 +647,9 @@ def feed_series(id): off = 0 entries = db.session.query(db.Books).filter(db.Books.series.any(db.Series.id == id)).order_by( db.Books.timestamp.desc()).filter(filter).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off)/(int(config.config_books_per_page))+1), config.config_books_per_page, - len(db.session.query(db.Books).filter(db.Books.series.any(db.Series.id == id)).filter(filter).all())) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Books).filter(db.Books.series.any(db.Series.id == id)).filter( + filter).all())) xml = render_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -661,18 +674,20 @@ def get_opds_download_link(book_id, format): response.headers["Content-Disposition"] = "attachment; filename=\"%s.%s\"" % (data.name, format) return response + @app.route("/ajax/book/") @requires_basic_auth_if_no_ano def get_metadata_calibre_companion(uuid): - entry = db.session.query(db.Books).filter(db.Books.uuid.like("%"+uuid+"%")).first() - if entry is not None : - js = render_template('json.txt',entry=entry) + entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first() + if entry is not None: + js = render_template('json.txt', entry=entry) response = make_response(js) response.headers["Content-Type"] = "application/json; charset=utf-8" return response else: return "" + @app.route("/get_authors_json", methods=['GET', 'POST']) @login_required_if_no_ano def get_authors_json(): @@ -752,11 +767,11 @@ def get_matching_tags(): @app.route('/page/') @login_required_if_no_ano def index(page): - if config.is_Calibre_Configured == False: - return redirect(url_for('basic_configuration')) + #if not config.db_configured: + # return redirect(url_for('basic_configuration')) entries, random, pagination = fill_indexpage(page, db.Books, True, db.Books.timestamp.desc()) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Latest Books")) + title=_(u"Latest Books")) @app.route("/hot", defaults={'page': 1}) @@ -781,15 +796,16 @@ def hot_books(page): numBooks = entries.__len__() pagination = Pagination(page, config.config_books_per_page, numBooks) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Hot Books (most downloaded)")) + title=_(u"Hot Books (most downloaded)")) @app.route("/discover", defaults={'page': 1}) @app.route('/discover/page/') @login_required_if_no_ano def discover(page): - entries, random, pagination = fill_indexpage(page, db.Books, func.randomblob(2), db.Books.timestamp.desc()) - return render_title_template('discover.html', entries=entries, pagination=pagination, instance=config.config_calibre_web_title, title=_(u"Random Books")) + entries, random, pagination = fill_indexpage(page, db.Books, True, func.randomblob(2)) + pagination = Pagination(1, config.config_books_per_page,config.config_books_per_page) + return render_title_template('discover.html', entries=entries, pagination=pagination, title=_(u"Random Books")) @app.route("/author") @@ -819,7 +835,7 @@ def author(name): entries = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.name.like("%" + name + "%"))).filter( filter).all() - return render_title_template('index.html', random=random, entries=entries,title=_(u"Author: %(nam)s", nam=name)) + return render_title_template('index.html', random=random, entries=entries, title=_(u"Author: %(nam)s", nam=name)) @app.route("/series") @@ -843,7 +859,7 @@ def series(name, page): db.Books.series_index) if entries: return render_title_template('index.html', random=random, pagination=pagination, entries=entries, - title=_(u"Series: %(serie)s", serie=name)) + title=_(u"Series: %(serie)s", serie=name)) else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") return redirect(url_for("index")) @@ -876,7 +892,7 @@ def language_overview(): func.count('books_languages_link.book').label('bookcount')).group_by( 'books_languages_link.lang_code').all() return render_title_template('languages.html', languages=languages, lang_counter=lang_counter, - title=_(u"Available languages")) + title=_(u"Available languages")) @app.route("/language/", defaults={'page': 1}) @@ -891,7 +907,7 @@ def language(name, page): except: name = _(isoLanguages.get(part3=name).name) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Language: %(name)s", name=name)) + title=_(u"Language: %(name)s", name=name)) @app.route("/category") @@ -914,7 +930,7 @@ def category(name, page): entries, random, pagination = fill_indexpage(page, db.Books, db.Books.tags.any(db.Tags.name == name), db.Books.timestamp.desc()) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Category: %(name)s", name=name)) + title=_(u"Category: %(name)s", name=name)) @app.route("/book/") @@ -949,7 +965,6 @@ def show_book(id): @app.route("/admin") @login_required def admin_forbidden(): - return "Admin ONLY!" abort(403) @@ -958,22 +973,23 @@ def admin_forbidden(): def stats(): counter = len(db.session.query(db.Books).all()) authors = len(db.session.query(db.Authors).all()) - Versions=uploader.book_formats.get_versions() + versions = uploader.book_formats.get_versions() vendorpath = os.path.join(config.get_main_dir + "vendor" + os.sep) if sys.platform == "win32": kindlegen = os.path.join(vendorpath, u"kindlegen.exe") else: kindlegen = os.path.join(vendorpath, u"kindlegen") - kindlegen_version=_('not installed') + versions['KindlegenVersion'] = _('not installed') if os.path.exists(kindlegen): - p = subprocess.Popen(kindlegen, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) - check = p.wait() + p = subprocess.Popen(kindlegen, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + stdin=subprocess.PIPE) + p.wait() for lines in p.stdout.readlines(): if re.search('Amazon kindlegen\(', lines): - Versions['KindlegenVersion'] = lines - Versions['PythonVersion']=sys.version - return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, Versions=Versions, - title=_(u"Statistics")) + versions['KindlegenVersion'] = lines + versions['PythonVersion'] = sys.version + return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions, + title=_(u"Statistics")) @app.route("/shutdown") @@ -983,23 +999,25 @@ def shutdown(): global global_task task = int(request.args.get("parameter").strip()) global_task = task - if task == 1 or task == 0: # valid commandos received + if task == 1 or task == 0: # valid commandos received # close all database connections db.session.close() db.engine.dispose() ub.session.close() ub.engine.dispose() # stop tornado server - server=IOLoop.instance() + server = IOLoop.instance() server.add_callback(server.stop) + showtext = {} if task == 0: - text['text']=_(u'Performing Restart, please reload page') + showtext['text'] = _(u'Performing Restart, please reload page') else: - text['text']= _(u'Performing shutdown of server, please close window') - return json.dumps(text) + showtext['text'] = _(u'Performing shutdown of server, please close window') + return json.dumps(showtext) else: abort(404) + @app.route("/search", methods=["GET"]) @login_required_if_no_ano def search(): @@ -1082,7 +1100,7 @@ def advanced_search(): except: lang.name = _(isoLanguages.get(part3=lang.lang_code).name) else: - languages=None + languages = None return render_title_template('search_form.html', tags=tags, languages=languages, series=series, title=_(u"search")) @@ -1091,6 +1109,7 @@ def advanced_search(): def get_cover(cover_path): return send_from_directory(os.path.join(config.config_calibre_dir, cover_path), "cover.jpg") + @app.route("/opds/thumb_240_240/") @app.route("/opds/cover_240_240/") @app.route("/opds/cover_90_90/") @@ -1169,16 +1188,18 @@ def get_download_link(book_id, format): book = db.session.query(db.Books).filter(db.Books.id == book_id).first() data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == format.upper()).first() if data: - if current_user.is_authenticated: # collect downloaded books only for registered user and not for anonymous user + # collect downloaded books only for registered user and not for anonymous user + if current_user.is_authenticated: helper.update_download(book_id, int(current_user.id)) author = helper.get_normalized_author(book.author_sort) file_name = book.title if len(author) > 0: file_name = author + '-' + file_name file_name = helper.get_valid_filename(file_name) - response = make_response(send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + format)) + response = make_response( + send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + format)) try: - response.headers["Content-Type"]=mimetypes.types_map['.'+format] + response.headers["Content-Type"] = mimetypes.types_map['.' + format] except: pass response.headers["Content-Disposition"] = \ @@ -1192,9 +1213,9 @@ def get_download_link(book_id, format): else: abort(404) + @app.route('/register', methods=['GET', 'POST']) def register(): - error = None if not config.config_public_reg: abort(404) if current_user is not None and current_user.is_authenticated: @@ -1232,8 +1253,7 @@ def register(): @app.route('/login', methods=['GET', 'POST']) def login(): - error = None - if config.is_Calibre_Configured == False: + if not config.db_configured: return redirect(url_for('basic_configuration')) if current_user is not None and current_user.is_authenticated: return redirect(url_for('index')) @@ -1268,7 +1288,7 @@ def send_to_kindle(book_id): if settings.get("mail_server", "mail.example.com") == "mail.example.com": flash(_(u"Please configure the SMTP mail settings first..."), category="error") elif current_user.kindle_mail: - result = helper.send_mail(book_id, current_user.kindle_mail,config.config_calibre_dir) + result = helper.send_mail(book_id, current_user.kindle_mail, config.config_calibre_dir) if result is None: flash(_(u"Book successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail), category="success") @@ -1287,12 +1307,12 @@ def add_to_shelf(shelf_id, book_id): if not shelf.is_public and not shelf.user_id == int(current_user.id): flash("Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name) return redirect(url_for('index')) - maxO = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() - if maxO[0] is None: + maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() + if maxOrder[0] is None: maxOrder = 0 else: - maxOrder = maxO[0] - ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder+1) + maxOrder = maxOrder[0] + ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1) ub.session.add(ins) ub.session.commit() @@ -1332,10 +1352,11 @@ def create_shelf(): shelf.is_public = 1 shelf.name = to_save["title"] shelf.user_id = int(current_user.id) - existing_shelf = ub.session.query(ub.Shelf).filter(or_((ub.Shelf.name == to_save["title"])&( ub.Shelf.is_public == 1), - (ub.Shelf.name == to_save["title"])& (ub.Shelf.user_id == int(current_user.id)))).first() + existing_shelf = ub.session.query(ub.Shelf).filter( + or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1), + (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).first() if existing_shelf: - flash(_(u"A shelf with the name '%(title)s' already exists.",title=to_save["title"]), category="error") + flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") else: try: ub.session.add(shelf) @@ -1347,16 +1368,19 @@ def create_shelf(): else: return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf")) + @app.route("/shelf/edit/", methods=["GET", "POST"]) @login_required def edit_shelf(shelf_id): shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() if request.method == "POST": to_save = request.form.to_dict() - existing_shelf = ub.session.query(ub.Shelf).filter(or_((ub.Shelf.name == to_save["title"])&( ub.Shelf.is_public == 1), - (ub.Shelf.name == to_save["title"])& (ub.Shelf.user_id == int(current_user.id)))).filter(ub.Shelf.id!=shelf_id).first() + existing_shelf = ub.session.query(ub.Shelf).filter( + or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1), + (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).filter( + ub.Shelf.id != shelf_id).first() if existing_shelf: - flash(_(u"A shelf with the name '%(title)s' already exists.",title=to_save["title"]), category="error") + flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") else: shelf.name = to_save["title"] if "is_public" in to_save: @@ -1365,7 +1389,7 @@ def edit_shelf(shelf_id): shelf.is_public = 0 try: ub.session.commit() - flash(_(u"Shelf %(title)s changed",title=to_save["title"]), category="success") + flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success") except: flash(_(u"There was an error"), category="error") return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf")) @@ -1373,15 +1397,12 @@ def edit_shelf(shelf_id): return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf")) - @app.route("/shelf/delete/") @login_required def delete_shelf(shelf_id): cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() - deleted = 0 if current_user.role == ub.ROLE_ADMIN: deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete() - else: deleted = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), ub.Shelf.id == shelf_id), @@ -1407,12 +1428,14 @@ def show_shelf(shelf_id): ub.Shelf.id == shelf_id))).first() result = list() if shelf: - books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all() + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( + ub.BookShelf.order.asc()).all() for book in books_in_shelf: cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() result.append(cur_book) - return render_title_template('shelf.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), shelf=shelf) + return render_title_template('shelf.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), + shelf=shelf) @app.route("/shelf/order/", methods=["GET", "POST"]) @@ -1422,10 +1445,10 @@ def order_shelf(shelf_id): to_save = request.form.to_dict() books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( ub.BookShelf.order.asc()).all() - counter=0 + counter = 0 for book in books_in_shelf: setattr(book, 'order', to_save[str(book.book_id)]) - counter+=1 + counter += 1 ub.session.commit() if current_user.is_anonymous(): shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() @@ -1436,7 +1459,7 @@ def order_shelf(shelf_id): ub.Shelf.id == shelf_id))).first() result = list() if shelf: - books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\ + books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ .order_by(ub.BookShelf.order.asc()).all() for book in books_in_shelf2: cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() @@ -1459,7 +1482,7 @@ def profile(): lang.name = _(isoLanguages.get(part3=lang.lang_code).name) translations = babel.list_translations() + [LC('en')] for book in content.downloads: - downloadBook=db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() if downloadBook: downloads.append(db.session.query(db.Books).filter(db.Books.id == book.book_id).first()) else: @@ -1481,21 +1504,21 @@ def profile(): content.default_language = to_save["default_language"] if to_save["locale"]: content.locale = to_save["locale"] - content.random_books = 0 - content.language_books = 0 - content.series_books = 0 - content.category_books = 0 - content.hot_books = 0 - if "show_random" in to_save and to_save["show_random"] == "on": - content.random_books = 1 - if "show_language" in to_save and to_save["show_language"] == "on": - content.language_books = 1 - if "show_series" in to_save and to_save["show_series"] == "on": - content.series_books = 1 - if "show_category" in to_save and to_save["show_category"] == "on": - content.category_books = 1 - if "show_hot" in to_save and to_save["show_hot"] == "on": - content.hot_books = 1 + content.sidebar_view = 0 + if "show_random" in to_save: + content.sidebar_view += ub.SIDEBAR_RANDOM + if "show_language" in to_save: + content.sidebar_view += ub.SIDEBAR_LANGUAGE + if "show_series" in to_save: + content.sidebar_view += ub.SIDEBAR_SERIES + if "show_category" in to_save: + content.sidebar_view += ub.SIDEBAR_CATEGORY + if "show_hot" in to_save: + content.sidebar_view += ub.SIDEBAR_HOT + if "show_author" in to_save: + content.sidebar_view += ub.SIDEBAR_AUTHOR + if "show_detail_random" in to_save: + content.sidebar_view += ub.DETAIL_RANDOM if "default_language" in to_save: content.default_language = to_save["default_language"] try: @@ -1504,10 +1527,11 @@ def profile(): ub.session.rollback() flash(_(u"Found an existing account for this email address."), category="error") return render_title_template("user_edit.html", content=content, downloads=downloads, - title=_(u"%(name)s's profile", name=current_user.nickname)) + title=_(u"%(name)s's profile", name=current_user.nickname)) flash(_(u"Profile updated"), category="success") - return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages, content=content, - downloads=downloads,title=_(u"%(name)s's profile", name=current_user.nickname)) + return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages, + content=content, + downloads=downloads, title=_(u"%(name)s's profile", name=current_user.nickname)) @app.route("/admin/view") @@ -1516,29 +1540,36 @@ def profile(): def admin(): content = ub.session.query(ub.User).all() settings = ub.session.query(ub.Settings).first() - return render_title_template("admin.html", content=content, email=settings, config=config, title=_(u"Admin page")) + return render_title_template("admin.html", content=content, email=settings, config=config, + development=ub.DEVELOPMENT, title=_(u"Admin page")) + @app.route("/admin/config", methods=["GET", "POST"]) @login_required @admin_required def configuration(): - return configuration_helper() + return configuration_helper(0) + -@app.route("/config", methods=["GET", "POST"] ) +@app.route("/config", methods=["GET", "POST"]) @unconfigured def basic_configuration(): - return configuration_helper() + return configuration_helper(1) + -def configuration_helper(): +def configuration_helper(origin): global global_task - reboot_required= False + reboot_required = False + db_change = False + success = False if request.method == "POST": to_save = request.form.to_dict() - content = ub.session.query(ub.Settings).first() # ToDo replace content with config ? + content = ub.session.query(ub.Settings).first() + # ToDo: check lib vaild, and change without restart if "config_calibre_dir" in to_save: if content.config_calibre_dir != to_save["config_calibre_dir"]: content.config_calibre_dir = to_save["config_calibre_dir"] - reboot_required = True + db_change = True if "config_port" in to_save: if content.config_port != int(to_save["config_port"]): content.config_port = int(to_save["config_port"]) @@ -1550,8 +1581,7 @@ def configuration_helper(): content.config_title_regex = to_save["config_title_regex"] reboot_required = True if "config_log_level" in to_save: - content.config_log_level = to_save["config_log_level"] - # ToDo check reboot required + content.config_log_level = int(to_save["config_log_level"]) if "config_random_books" in to_save: content.config_random_books = int(to_save["config_random_books"]) if "config_books_per_page" in to_save: @@ -1560,22 +1590,32 @@ def configuration_helper(): content.config_anonbrowse = 0 content.config_public_reg = 0 if "config_uploading" in to_save and to_save["config_uploading"] == "on": - content.config_uploading = 1 + content.config_uploading = 1 if "config_anonbrowse" in to_save and to_save["config_anonbrowse"] == "on": - content.config_anonbrowse = 1 + content.config_anonbrowse = 1 if "config_public_reg" in to_save and to_save["config_public_reg"] == "on": - content.config_public_reg = 1 + content.config_public_reg = 1 try: + if db_change: + if config.db_configured: + db.session.close() + db.engine.dispose() ub.session.commit() flash(_(u"Calibre-web configuration updated"), category="success") config.loadSettings() + app.logger.setLevel(config.config_log_level) + logging.getLogger("book_formats").setLevel(config.config_log_level) except e: flash(e, category="error") - return render_title_template("config_edit.html", content=config, title=_(u"Basic Configuration")) - + return render_title_template("config_edit.html", content=config, origin=origin, + title=_(u"Basic Configuration")) + if db_change: + reload(db) + if not db.setup_db(): + flash(_(u'DB location is not valid, please enter correct path'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + title=_(u"Basic Configuration")) if reboot_required: - if config.is_Calibre_Configured: - db.session.close() # db.engine.dispose() # ToDo verify correct ub.session.close() ub.engine.dispose() @@ -1583,7 +1623,11 @@ def configuration_helper(): server = IOLoop.instance() server.add_callback(server.stop) global_task = 0 - return render_title_template("config_edit.html", content=config, title=_(u"Basic Configuration")) + app.logger.info('Reboot required, restarting') + if origin: + success = True + return render_title_template("config_edit.html", origin=origin, success=success, content=config, + title=_(u"Basic Configuration")) @app.route("/admin/user/new", methods=["GET", "POST"]) @@ -1603,26 +1647,29 @@ def new_user(): to_save = request.form.to_dict() if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: flash(_(u"Please fill out all fields!"), category="error") - return render_title_template("user_edit.html", new_user=1, content=content, title=_(u"Add new user")) + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + title=_(u"Add new user")) content.password = generate_password_hash(to_save["password"]) content.nickname = to_save["nickname"] content.email = to_save["email"] content.default_language = to_save["default_language"] if "locale" in to_save: content.locale = to_save["locale"] - content.random_books = 0 - content.language_books = 0 - content.series_books = 0 - content.category_books = 0 - content.hot_books = 0 + content.sidebar_view = 0 + if "show_random" in to_save: + content.sidebar_view += ub.SIDEBAR_RANDOM if "show_language" in to_save: - content.language_books = to_save["show_language"] + content.sidebar_view += ub.SIDEBAR_LANGUAGE if "show_series" in to_save: - content.series_books = to_save["show_series"] + content.sidebar_view += ub.SIDEBAR_SERIES if "show_category" in to_save: - content.category_books = to_save["show_category"] + content.sidebar_view += ub.SIDEBAR_CATEGORY if "show_hot" in to_save: - content.hot_books = to_save["show_hot"] + content.sidebar_view += ub.SIDEBAR_HOT + if "show_author" in to_save: + content.sidebar_view += ub.SIDEBAR_AUTHOR + if "show_detail_random" in to_save: + content.sidebar_view += ub.DETAIL_RANDOM content.role = 0 if "admin_role" in to_save: content.role = content.role + ub.ROLE_ADMIN @@ -1643,7 +1690,7 @@ def new_user(): ub.session.rollback() flash(_(u"Found an existing account for this email address or nickname."), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - languages=languages, title=_(u"Add new user")) + languages=languages, title=_(u"Add new user")) @app.route("/admin/mailsettings", methods=["GET", "POST"]) @@ -1665,7 +1712,7 @@ def edit_mailsettings(): except e: flash(e, category="error") if "test" in to_save and to_save["test"]: - result=helper.send_test_mail(current_user.kindle_mail) + result = helper.send_test_mail(current_user.kindle_mail) if result is None: flash(_(u"Test E-Mail successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail), category="success") @@ -1689,7 +1736,7 @@ def edit_user(user_id): lang.name = _(isoLanguages.get(part3=lang.lang_code).name) translations = babel.list_translations() + [LC('en')] for book in content.downloads: - downloadBook=db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() if downloadBook: downloads.append(db.session.query(db.Books).filter(db.Books.id == book.book_id).first()) else: @@ -1729,21 +1776,42 @@ def edit_user(user_id): content.role = content.role + ub.ROLE_PASSWD elif "passwd_role" not in to_save and content.role_passwd(): content.role = content.role - ub.ROLE_PASSWD - content.random_books = 0 - content.language_books = 0 - content.series_books = 0 - content.category_books = 0 - content.hot_books = 0 - if "show_random" in to_save and to_save["show_random"] == "on": - content.random_books = 1 - if "show_language" in to_save and to_save["show_language"] == "on": - content.language_books = 1 - if "show_series" in to_save and to_save["show_series"] == "on": - content.series_books = 1 - if "show_category" in to_save and to_save["show_category"] == "on": - content.category_books = 1 - if "show_hot" in to_save and to_save["show_hot"] == "on": - content.hot_books = 1 + + if "show_random" in to_save and not content.show_random_books(): + content.sidebar_view += ub.SIDEBAR_RANDOM + elif "show_random" not in to_save and content.show_random_books(): + content.sidebar_view -= ub.SIDEBAR_RANDOM + + if "show_language" in to_save and not content.show_language(): + content.sidebar_view += ub.SIDEBAR_LANGUAGE + elif "show_language" not in to_save and content.show_language(): + content.sidebar_view -= ub.SIDEBAR_LANGUAGE + + if "show_series" in to_save and not content.show_series(): + content.sidebar_view += ub.SIDEBAR_SERIES + elif "show_series" not in to_save and content.show_series(): + content.sidebar_view -= ub.SIDEBAR_SERIES + + if "show_category" in to_save and not content.show_category(): + content.sidebar_view += ub.SIDEBAR_CATEGORY + elif "show_category" not in to_save and content.show_category(): + content.sidebar_view -= ub.SIDEBAR_CATEGORY + + if "show_hot" in to_save and not content.show_hot_books(): + content.sidebar_view += ub.SIDEBAR_HOT + elif "show_hot" not in to_save and content.show_hot_books(): + content.sidebar_view -= ub.SIDEBAR_HOT + + if "show_author" in to_save and not content.show_author(): + content.sidebar_view += ub.SIDEBAR_AUTHOR + elif "show_author" not in to_save and content.show_author(): + content.sidebar_view -= ub.SIDEBAR_AUTHOR + + if "show_detail_random" in to_save and not content.show_detail_random(): + content.sidebar_view += ub.DETAIL_RANDOM + elif "show_detail_random" not in to_save and content.show_detail_random(): + content.sidebar_view -= ub.DETAIL_RANDOM + if "default_language" in to_save: content.default_language = to_save["default_language"] if "locale" in to_save and to_save["locale"]: @@ -1759,7 +1827,8 @@ def edit_user(user_id): ub.session.rollback() flash(_(u"An unknown error occured."), category="error") return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, - content=content, downloads=downloads, title=_(u"Edit User %(nick)s", nick=content.nickname)) + content=content, downloads=downloads, + title=_(u"Edit User %(nick)s", nick=content.nickname)) @app.route("/admin/book/", methods=['GET', 'POST']) @@ -1959,13 +2028,15 @@ def edit_book(book_id): for author in book.authors: author_names.append(author.name) for b in edited_books_id: - helper.update_dir_stucture(b,config.config_calibre_dir) + helper.update_dir_stucture(b, config.config_calibre_dir) if "detail_view" in to_save: return redirect(url_for('show_book', id=book.id)) else: - return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, title=_(u"edit metadata")) + return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, + title=_(u"edit metadata")) else: - return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, title=_(u"edit metadata")) + return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, + title=_(u"edit metadata")) else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") return redirect(url_for("index")) @@ -2038,6 +2109,8 @@ def upload(): author_names.append(author.name) cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() if current_user.role_edit() or current_user.role_admin(): - return render_title_template('book_edit.html', book=db_book, authors=author_names, cc=cc, title=_(u"edit metadata")) + return render_title_template('book_edit.html', book=db_book, authors=author_names, cc=cc, + title=_(u"edit metadata")) book_in_shelfs = [] - return render_title_template('detail.html', entry=db_book, cc=cc,title=db_book.title, books_shelfs=book_in_shelfs, ) + return render_title_template('detail.html', entry=db_book, cc=cc, title=db_book.title, + books_shelfs=book_in_shelfs, ) From 73d049051b843eb862afd11ef200f718ea10e3c2 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Sat, 28 Jan 2017 20:29:04 +0100 Subject: [PATCH 05/19] - Fully graphical setup (can be changed as admin under configuration) - Title of calibre-web can be changed, to distinguish between different installations - View of "random books" can be changed independent of discover books - Author view in sidebar can be switched of ###################################### !!!!This is a big change, I recommend everybody updating from an earlier version to do a backup before upgrading!!!! ###################################### --- config.ini.example | 13 ------------- readme.md | 24 +++++++++++++++--------- 2 files changed, 15 insertions(+), 22 deletions(-) delete mode 100644 config.ini.example diff --git a/config.ini.example b/config.ini.example deleted file mode 100644 index 02a57541..00000000 --- a/config.ini.example +++ /dev/null @@ -1,13 +0,0 @@ -[General] -DB_ROOT = -APP_DB_ROOT = -MAIN_DIR = -LOG_DIR = -PORT = 8083 -NEWEST_BOOKS = 60 -[Advanced] -TITLE_REGEX = ^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+ -DEVELOPMENT = 0 -PUBLIC_REG = 0 -UPLOADING = 0 -ANON_BROWSE = 0 diff --git a/readme.md b/readme.md index de49ee80..75d50672 100755 --- a/readme.md +++ b/readme.md @@ -8,6 +8,7 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d ##Features - Bootstrap 3 HTML5 interface +- full graphical setup - User management - Admin interface - User Interface in english, french, german, simplified chinese, spanish @@ -26,9 +27,10 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d ## Quick start -1. Rename `config.ini.example` to `config.ini` and set `DB_ROOT` to the path of the folder where your Calibre library (metadata.db) lives -2. Execute the command: `python cps.py` -3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog +1. Execute the command: `python cps.py` +2. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog +3. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button +4. Go to Login page **Default admin login:** *Username:* admin @@ -36,12 +38,16 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d ## Runtime Configuration Options -`PUBLIC_REG` -Set to 1 to enable public user registration. -`ANON_BROWSE` -Set to 1 to allow not logged in users to browse the catalog. -`UPLOADING` -Set to 1 to enable PDF uploading. This requires the imagemagick library to be installed. +The configuration can be changed as admin in the admin panel under "Configuration" + +Server Port: +Changes the port calibre-web is listening, changes take effect after pressing submit button +Enable public registration: +Tick to enable public user registration. +Enable anonymous browsing: +Tick to allow not logged in users to browse the catalog, anonymous user permissions can be set as admin ("Guest" user) +Enable uploading: +Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick library to be installed. ## Requirements From e00a6741c7b52441751c464117fa04ba86bb0ff9 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Sat, 28 Jan 2017 20:39:33 +0100 Subject: [PATCH 06/19] Updated language files --- cps/translations/de/LC_MESSAGES/messages.mo | Bin 294924 -> 296356 bytes cps/translations/de/LC_MESSAGES/messages.po | 362 +++++++++++------- cps/translations/es/LC_MESSAGES/messages.po | 356 ++++++++++------- cps/translations/fr/LC_MESSAGES/messages.po | 352 ++++++++++------- .../zh_Hans_CN/LC_MESSAGES/messages.po | 354 ++++++++++------- messages.pot | 342 ++++++++++------- 6 files changed, 1078 insertions(+), 688 deletions(-) diff --git a/cps/translations/de/LC_MESSAGES/messages.mo b/cps/translations/de/LC_MESSAGES/messages.mo index 7d5b53037a2dc85135d07b94ddc3f8a4ae985a2e..7723d8b78aecc5bcd1a770aa9a86e7347d68f2c3 100644 GIT binary patch delta 128938 zcmYJ+cl?&)|NrrKDMg~B?3Pp%l957IDcL0=A|XnWq_Qq0B%_F|6iJE3hse$-GBYX} z*^*gCMn-;*$8|oxx8LWV`+2O_@j8z4IM3^Pze}I%uh@L*b(@zy*}TfF75r~a{fdh1 z@RmO;{r~@MvZhkSh7@bYEpT(n+hTRx6*s~*SQQUK`**@>cobH_<6}>>-)UI0qM}rB z78koyaUnWz8g7iU(1A~+{#kV3!uT52p!_yE(F(NxSMgi4|7tYuTD0HaDR1zT3n*1o zY|Mo#sDqo~_Hh?9;QsMoG+?KAByK>tTiQ>EC&%7s|Fh7E&rSV>DG$dDnZII08b+t# zhLmqbxA;zUrT5{cI4AY<(Fqo%{>_wEpez0&R%r@sjw?R8L zMgug%O>oz^FFIl8v>%1;KzDQpPfU9s^cJ2IFGLeBA@{uEMs&W3KUXT115TyFt$z@m z_^J2;n$RmLzljc9icY)&P3SYU-`BVWevihf_@&svYG{JBu)NUN_?J={Cq6=JyoQYCJwY8NhYEe-SJ>|{f9_WMz#Ew{x z^5Iw+Pe*UTndk)Pper7P?#!?_B91}(jZgV5tm5-Ol?%6SMjGa#9iKrbScoR_3c8hx z>ySW31})zZn+>sA~mopAI{uzBwAGRqESd1IqiM zJ9jcVegHat2-^P=G~tnGV%MPk#-;ssEV=T@TsUwB+HnqgYM($Kr^V>=`(^wE?O*Bl zB49PNycv3L>!P=62XuTpbe^Np9X%19=ak>`{GUaIkIT8}<1q?7?YE!-C!?9qLRa({ zI^k1jV$a97(D9$d)o8!qB(0-?- zy)T;B`DmPBX#dONEolFT(0Gra2|bw>kh2O6i;kBjmO&;Y~!uK`Wus zsec|#?4^_!r@RC`tSiu4^doNV^Z##gQL*K^;(NgEXd->ko#}^8I3!+-CR{=%x&~dz zxHuv06XR5L8s{!0*w7enR{GjrQB%uOfk( zXnP&>O;{gYNQ;zPqqp(^bo}91e*U|q;zV?!-YK7j209O2;U#Fm5$HstQ$HU4N_Gc2 zekMA>!)bpKo%lKQ7QUYPx3Fa3WvTcG4e)u&U&kNNM1DaN_z&&3;on7KHPAy<8{O(1 z;_m2o$^+1Qeloga=b{N-^mm^B%TqBbUXO15E$Bpdr#uBs;30Ge9zg>=8(&KMo9JO% zid*4IbcfcXiPZk5u+cv}|5ogtii6Ns>#=BnLFmdyrhP1W>L;Tg%d^p^Xt@;{wU+~;V(b#deM#jAT; zwEymC`++GRi{66Muz3E{;e2dK!w7UGv(cS+4ZVlop<7t(zv9+xgKqr}DYrlq-81za z&|7sx%H3j5bme`J#7Y%|xUl0$^qyav^3CxMG}C*~(>pEY2hp?fKXfM^Llb%vjkhf2 z73lb{;&*7ApKv3e|Fz`{4hOnb)%kj1psmq_8l}858n`t&aod#Jr`!=8*99Hd6+Ih0 z(X(;^8vkna@ZN@-Fn`4qF3fNa8sIVXae6ksg--Z6n!wj+{~zP8=(u(0gq12)E`NM) zg-*OHy5$F>-<*y?`**{VfqQbHr=b&^iLT&WG}9sIy}u%kkJFHMc*T?G;a!3TULAi& z6IzcR#=4bRF*d>SQ&+iisXXyPR5;$)p#%OyC#b$b(SK94e?7FmA=JQ{F0I^ieimVSqh z{|kL8Dpx6P?KZIinrIXBY?OB4!j&9^20R=M&=oxsC#8I9+WV$_9(pS-LK7H;CN>UT z@pN>eN74AtqVX4@aTgbsDwc5JR=l4MpQOApexLfEQ~nbTydF)oTGe9wX0a}M3%5ll zYLNOSDesIP>fNxW&wo2ETtVkF9GCK`u|JyNP;}zUQob5p@%1U+7iXX=orB(zdFWw$ zIrYoYo%;mK&;Jixcv#k<1FCOWl(&k_&;fg+E9-#egwfM}T<+T=l@AuxRuk;dp8@+_-S;&8)$%KaYg(Ro$x!f-x_q>pXh=%s#c8K3d^?wou>)f zzhyO^f46q8H0+B`bVxdMNqx826HTxWI_~^<5xOIn#?k1Zy9Mn(DfJJf{5YD(!fJW` z-$=t!H1OwWBCFzRG~?gWzCKo|Ues@l_OBDSK@(_%-m(^GBD-jc&>grBjWY}FHy7>q43?}|z=eU|KnE;OhtK0T zaSht<_mnGbQY2PA)F+YOD=3mwi_w)WMHBcKP5i60 zuSxxHDgT3xue@2|#%Lm&qvN;2^6&o|lrMPv(y&u(g$`&F_eTRBf+li!>W_;jVR>g_ z-*_H+&xfEpGZKw+4OaL0AIpUUZ;zAF0n^YG&PC70eDpirD{21>eZl;Q23&_tuu`q! z&^1LDuvg0aqPMCGx`30g{PTYwF6?+t9EfH*B;||I39g8vvHV>x<>_d^IVnGZF6enQ z&Z5-6fu512=pkHLi|5~qU#T#GO0|nX)zEUSSTAme25y}4PI0%mSK9YQCq5Y6!A|He zG`pt#?9>meo#+4JG>kyEel)t`TT_2G8fYq-*qrzn+W%>^|0}6~8{Luj(*7xWtA0Qi z@F&{;zfvx$ZC(W449&c5%G;nF8>YTF8h977MM zn!pk?Zt25Ze1T5`U-v-eR`Im^ZkhB zpZ|a7!hscA6jntu+!*ax3*D-%Vh$fVuHWbS2NBTe}$Tw*oy}U#7eU9rrsL@1IzuZc)Dpn!x5*@?E?o z7Y5ok9U8~xXvbYs-UGdtZDSYomi0{gnP{B;DPMplHVo}I3O$TBqB}daF3-OMW>MkF z9z`cufWBas#_!PqmFpENsDTYA*G3Q9?&vLOk0yLrJPM89EuIv6$FuA4{JT{HsW8wG z^xNr3bi!%rI0x-F51rr{bZcKo`Bij70Uf`=mPKOKOI$c$Gqht} zbmHw(-xR$?EzyCk(0+Tx{n7p%(|%OyyQRJ-8vhJ*C;Fv4DD9=8To~|*bQm4SrhWn% zU{ahKA58oI&;*`9CwKvU54;lJM{nsjaUJ^fY`j%@L8XcYTzK!AU^CneTi^-U9Ir$J z&qNQ;T=Y496@8<9p7P)58LG2&u~Us>GxXNBOt~$(p!V3r@BbaRFynL3j>FLjuZ}mM zJ9G=W(upbGA7`Npcr4}T(ebaux6sG+J@hgED*le;=fCIb5S@gj7`u0R(s0bRhPI2}vdQ}I7847eEGfo1WF)UQE*YxO&N z7}ueR{EMz^gKdj(wb8`u#~sjq&CvvQMf>la_72zASfe?WI;EqaOZS0quVan$Vr- z8JLbvG%xipqW#`N<19lD>4&Lbi6&V3o(m`ZB^7_6E2^}8QC}Mkur)fMK6(##M0a8j zG=UE2gx%11C&u1rqUWT15jyU2WL&9YEEf)(gl_%RlxL%X9z`>M22Eg5>KCJly@!tf zDD9u8{0(~VSEK#@Nc}&ta(&0~{8#6~2{uIsY+Y{POC~z7Il6*fVmovNozmU|z2~Q( z<9nm=`k}wN8Itm7bOGbhgzr>8|L;k~G<3x?(TN{LCwday^4HLYpiZ+Nfx+hfdH4z3023 zJ8>|YP-k=@UD1Sk#?wn&IPp2?Rt-w|GW1Ygg$B454LBYRa0i;mJ?Qv(=mhiAz6jl! z#c5xX`j5~!U!eU<-*92(Kga*j32QbkI&6tf&=9R}o^tDy+o6Fvpr7kUqEE}|=vf+w zj=Krn$-B`0(~yOfD&}xuM)T0m^JmaA@gDjckgrq!7rM1u>`+Y93=Oy+IzcCNXO2y| zN7_$EcX9x_kU?nt;bnRLuI0i&cc7U~MFY-42Rt61i;K{S-a`Ao7e7T8@=e-TqZ9px z#;Mw*=wB1v@p`zi&wrCN?1p}b?1Lt73_9>6^gVET>Ib6%E<-20F7-E|XW}k2v4>Ot z6dGqCy3jY`axD3rf5Aoh45Z;d^e}F`V=J`7E)S3Do>H!|h%Dc_GC z-g#);S5yC9i3@jN6}nZwrbFfCMM9gQ^;@Rg6y5S&<38xUJ{aBF!_hNwe99-I6ZS=K z@rCI4k?4X;*K^@%z9rs;zBs0&!*n#@ocI{}2a!)>`5O|t;vdi*`4`=h&37tnf{x!C zO>jSSCps0DDo*0UOa`DGhoA{uf+luF%Gbo}(SA3j{kAwM-j7Z+6OH>g8t*xDrxv0+ z@MgK4=WluWfj*Tax z{d-&X`5(ZATR0>gu0{jjhz7Vd<@?aU52A0h|Dm_wQFLW*pj*EzqKwobo5} z542ycU5gjb&aopJ=d=~ao`wC;2@l8eAY~SGX|H;`kvN=xa3a4``rY(Zv2qeZ?L{ziQ|X)|}5 z;=R#$1JTcli}v98cgwD$!p!bQCzz6s)6hg_r92ny{}j5FZ^Wf>1sdn8l)p{+XLJXC zM-!;nvpDS4_T>3@f-R|V;_cC`ZiEJE75731wu=X$<2t8&6gs|p$|uIt&_w&9J2ViD zdvWSZB`zFz4SEZ%L$~@i^fTcebj1&+yZ{Zb6dm_L${(W%et{8ue;T+@AW$sh^IT&0Qxjsh2@|B$8zC- z+tE`xDdidHy`F>Kub~ONi;nvM4ftu=zfJqk=)`}Sv`k-5TcFO0X6AnU8^KkSKUV}arx2OKW)IWjV!dFxO0eXv8q5apQ<2P(u^xLv6 z&%ayTEEW5qD?S44*b^OiZpxRVD<6v{HUYhtlhBXbr_%m5x}fFg&aFuOH|T=bpbP!2 z#D#&@r9-85MThF>z1lS8rf9&HXac*ZexKC0M-x9Jc1?S4^w#u^gV6Dp#L`GE%<$S& z+=^y65uI>K>Zhaka(2p(V*|?bQ~m@Uw=#Yoe@5f{k@7z&SKhbWpTGaXg&Eb1TNDiy z+oA&-rQ9^-mT`CVi^ksQ_`_0vOv>HSgicQV>8bCFF62C{;phKQF6?+EI^nhPM)YYI zk4`Wt-XCY8J2w|SWY3}f-#~B0^3;Eg9?D>sgu>AS|8W*0%x6zJE)8XTkzluM` z-_b<>LnqjH|H>7Wa5MC2sf|7jJD`cTjR&9;cZ^5u&-3pWhHg|i@wsS*L(r96mhv^| zmX1XyoP=)aedrmOi>~Vwb(x}*KhMkgAW^3aqoPxvl>p?W&yf_~n z_;P#$9rtd^%j3uB4t|Ly_9HsaS~T9D=(tkF0mW8UMFZDGw{Bas!%pacUC~>xckCFC zL;LqZ@BJWj+?8koqtWpIeD^)zeg@3p-8~w@TVf1%2ucNQvuhEJBKo8x9 z2No05MklTpo20%qI$@i5KDW|1vJj=niy?rl61CG<3jxbVYBYJG2a4 z*+;2gneumOUyH{13*GwlX|H-v5oa^>tZjjZG~uFA8qP!~=oimV{YB%CZC|J4+DB7k z$nj_jccN>ZiY{R$`o;S(^jN-uM*KAGD^p&LE@>?qcO81#svKM_y%rj`Ir^NnI+zF5 zn{Xf%PINRHxJT@T20Q~z@EkPo`RHC;lKK+5r&pu#Zb$cGdVCa(^8&iSm(h4{A6%+j zu?rW=snDv26q)XUEh!(4CNvBka5)<2syGInXe_$YiSYq+L64$|KAZ9j=*nM1-%uZx zxNw4B<2rQUf9O_K?@(-ct+)dkaBnn`c4+?tV&~K!iza+L8vm4(`^0`|BBeoG7+?hY zX)y+!_yP1~`#GPgg#Vy{H|SWj*Fh8B3f=nc(0)ye_ENChUTcyBad``97%UC;@;qJd7p{joRhj+4>E-a#k$5d9AJIl6$~(O-d7Ikb2Y?0~iW zpl;7a`3XkvW$$tWpO0w35_)e(qZ8kPCO8QlH#N>fSN;n673Tfa|A{^|8+R(s*7oQ^ z_fqDs*q;lxs3V&BVJRPjuJ|}Ku@mE2Xaa-Lal=!-4DEkq9F4vLZ$Kv;7jH+$-Gk-- zP-iL^2AqLD=X1~rUq)B7B=ujS3I2r!tkk&}w-Gw=X6R0Bf%a>Np7LhsJNH0zo@3K~ zLT8?TJNBZ&0B5E{KXmKQPyO)JUyf!z3Qb@%+HV{h=Z<(!oSydC@o{vX=hOaDXP$op zy+MU9mZfNhpQ87AB|2bD+SjK1A39-`!;1bj(0;YiIQ7uDjnIBOrQ8aQzfWvm%0&ls zWnIvTk3zSs2YL(6K?4m(SA0dh7Txlj(F7);6WkYP#5wUX^e{e+J{_gSY4|b~-=eS3 z_2^1!b}3e}6&ko9I#JWu0!^${+S{NB?w|6ZDIb9isqcbxSi9DI|LM;EF{tg%3`}fd*AEI0O8M>9L z&@KB3O>BMK;E1AMb@X0thQ6|!pbKh;o{b}L2ke2~`%BRm)L1Mza1Iwvun?X2U3B0F zXkshTt^5}4_bYlRD;-%_8+~syLi_E5F63Zzr#h#6Oxll6{b@(?{JSM*Q{lve({OPd ziSED{G~oDnN7^T&(xPEkeJ0eTK&Q6HRRWk$L_%JgS&zQ#9a~Xovc- zDLQc1l=n<|KXgY9j>n+md!ajcX3B%mof(GS!jb6CUQ*CGna7W6M&_wP}c~;sV zjZdOGG(YW&Qok5YcuD*Ky=9-qHR!m~e_S|0^`nacwa~|}9=fH?&`evS{q{x!w?|jh z0bTJ?v3oo@_Ca^3U&_PK@uk92#Z_E5(RFA>6VQ7#x#&>wIQqroS@gy94jS+?bVaM< zdbHoB#}xOxF?!nfMW2eJ(1o3WCNQKd&;KP{IPnPdo{d3QJON$tooIqn(3Q?YSN?pDeY&Wi48<29*!nfO8Y2u{0;Gzt~~#4{asWzU@E%x52idD%UhlDd^Etq)W4qk zW$21OieIB~e@4gug^sJzt=O>|=&h*JEzf_WH0%_2k8RNv9E2XWL*w!BEHv?p;-%;< z8Hr9f1|5G>$`jE3ccTlvuf&BJPDi)$VKnn+(T*?2H{!c!|Mye=0-fL+^h4^$lz&1K z`7Qn%s~%Uxtr<&oars1gA9laGNr~OQHMFX(>Sf+eA8hBL7*P#>Ng#PB?{f59JI-&`8N&PYLgxCwq z&;OZRIN-cEI30$eTUSCSx&{q64&8wX=wZA!^$%hB5TkFzXX9ct&Zp>mWfi)Be~;(+ zH$asWiVmBihpry_d^Sa2OnagU9)#}9q3A?iQhz)euxHArqlxxK;}3{K;|O#C*PX!g z?{94;P~i$6K_8>1(H|D)r~YF!;MeF%en11SMFXuz6WFLn(XSR7uO6CsgV+RpdUirT zCl(28jGpGE=!euHsXrM_Y#2K3GIZjr&|5qfIkcsU$y|6? zW}_26hYoxtE1p0zdLAA4YU~S~`>q-^r(<6AwxG5;UQk&;;&8 zU&WKrglD1&J&Fc=I`uE3{THJlufl z6OKnSzAN>Qq6xl@j{hKj8dstTevjVTUsC=p?f+mkfB#qc)FQyf=zv;r%hcCLKhK+> z3G9>dvFM>X6HVYEG@(mVz6ynfZN5!=)}!a zZi)VE*Bb5LG4);0TW}0I@rmfT-YK6M2gD)0OGU<)QQ?H6&;Vo5&;Rl07C(aC!&lS3 zB(6yPN_701)c=N#TaWH&-P4QlJD@wX3!33wLxhvNURe5TMHcm|#4`S>#0zw|~f-a#jN4;}a^dYHaW`5$zm4f+%VYs6Y; z|GMY|+r`G{nQD$MY#(&OPUwytft(%wqAnK>?3)e);st2tLsPy44R{6m!WoC&lDp6y zxgR}rPonYOiObOhKStjJU!eZ`}i z&^YzbINQcX=mMLa#q)1~-O|uD4F{y$DISU5^KS9f*dINNL(zaE(FKe_pPun(qBGL| zKXj*_Kofhp#D$r?j&^(}4ez4?KTiG1lz)h8Q~yuO8}uzEu7Qr*B5sFH)C8SpCp3ZG z&;^zD=fWL049&b78sOx32KxM-iw3v^%l8uPKRVuoj=K}>HzoBCps(tO<3hCG^1@QZ zr(76l6?({iLMN;^yLg3ehJKM~n({vA<92lF`=ovd`utyyPB0Z+*i7`W%|qiXLg)Da zH}v!WTQ1D_r*Z}F0d(NM=nJUYIYq!4=*sKFZP3IUq2qT>eXG>BN%_FEcS`w4G{NI6 z`~08Gg%kEcS3Ur}N0*`#jEUpXffG}n8fT&7=AkQl3BBjb(1brs`SX;&M&ta5<-e%& z8yBA9ztKcC>{oQCg$CFvEOo`3Jz zC@P$2V!RJc=t1<}J%(odG8$-cT#ki?c)w;yqyY56|K_H2Ayz! zbR~zOD>w&T`G`0M?Kd7xWOB+6#(C%tyolbiw^ILE{2}#!;^sd88=Y6YP_{!4Nh>tK zA?Q6l2E7%%QXYV==o0j-j6hdt--jmpFuL+*(!Ky|`24?_h7ZudpQZdY z8sJCt9{!#78Uu@dwb8&^qJbNuw{EAj@1AlSG_eEFL_4E9*cHpqe=jZ^ct$)IP2|Fq zFGd55jMt$P-h#f0??U^}L3iXaG~QFGe=+s1r2Hnjuq6ZY^Z(~*Sc7Kx8+uCrLj%=5 zzX-fFI#GQzfhK5si?~PH_f7dAH1WgG#Ja{4(0NWiKcD|+QDLC-<1qA|UlGTm6Hh`j zpNgK5M^b(cJv(orx9YRh|Ah8ikH)VtsF=78x`6FVxo8qwpc%JDGu$`z9aDbqd~qq#7^jkp6&Kp(rOQvYu1KTQ34bfS6}6p3t) zPS`j$M;EXwx_~xl|ASKBIUbA5%lH4{qT+P4W50A5l=_QPKN6kj+O&^N{q5)$Pe%WO z-%Rw@EkbYAd+2w}uh50AL*rB$?9;>ZSB(oFht1K3`sfxnNqHwUvE5SM8=YXkczD`R zioMZ^&W`7!w_q6hxL%6JzXmt*`M;hEC!UZF6XSGrD<46(@@X`Ym*N{}B1_N-zd*lq zeuwTz#f616(fV!B@vUMTEdTz$Jr^d>37zoBbT}T(^puoOM|bKRwEvKJN!mxCD;=Hk z^=Q2DaU!~q`_Y8wT*&ippvS4uXVDB7peuU~eVjf(6ZsY$xEk&EJNlmZ7v0*;h7=QQ zgSIz8$F)G?w?Y%zJ0373KmT`5hofV6G(gXEJU#XOQ$Hy6!%`lBCN>(~fpIC{5+|Yw zPDK-$gC_D=DHZe42^XRrUq@f9@1a}vU0jRyTaTXZsuvXj>!1O*Mt7_+`a)`r?odZ` z+zGK4dMit3a$!b;;?Q_myb=w3ZOS*J3Ehrv?G$w7)6og$p#A4z1AGqMkx$Y7U!vo` zLFO%0{E~)0(R=ZV~EG(f|2XqNij&=u{4CUhve#fPI?dtBOkpesEGjWZOT=<;}V>Tg7E!L3-b<76%j zJS`m_ijTx6(TSc%w|pV`+pQ(&xbM(c?3!3*cro$T=pk!_CbkPYeqVF}U54}gThT2Y zPe%iuhi>&?G{NEV3UuH#Xuomjo9 z2Vq0J8vPkt-!m>?G#Ym-y6{^|To`x~+VMg3UH>FH;5Br>J8?PM z?_+cYKcJuQYtz2r6-B&F&BCzXck1kCaR8xNw4lQ_&e+$ua0er=SDQMR#H_ z+W&I2-zao~o6wcth4#A#UD!itB9F)C(SENY{Yn*!)9?Wr=nHg$AJB<^NqJrT54~ko zM-+iJikqV2>Y#D9M*Ho6#%YG`z^-v`EdKsK7jEIvu{WCGU^L(`G~ngvN=BoBZc6(d zsh^Ba^gx`0?$A6m{sMGIm&LEJmf!z>O+}TF#bZ?mov0BSaA$PF-O+^FqG#j)G|^+y z-aVdz_B%7>{wWVe`wvh3i=Xuv;G{wL+iqlyHp#hPfpI&mvBq3zS&6g`A3(L~y&{vb4=Qm1q{ zDiuA`;q;Wxjswvxy9j-yUV{d_E$x%hM5dt$Je2lF<1^@4T8O?EUO}FQQpM7A_!8av z@6kX%qZ$65`hU^D8(dY?Z-V8Oqla~?*cgq|5>2=@`q&+UerR=#ryXQciSbO)Y5$3LI)8)*NfXo4T5{23Z=75YB-5zC+d|D~bY)kVOX zu`U{T`?wPta4$5`c4z_}&{b8sho z5)JSrdhfnPC;Tz~hK~C;%xcY})Tg{p6IVp>by74)_EbZw31G{5v#GmvMpg%zkM#qm%`;F*?6VNj?Iqi>N`Op8J;KGTXiwkX_{Azp~4Y(Xll2cDSv)8aYk z)?a|G=n8bp$D@I#raS{3|5WN}j6aep*{4(LuCh9=q_{Y$ES(D7HGJ8|{(dH%JDucLW;0`#7F|Uj#i<(H|W+DCHsO$}T}ydIfr@u16=l8%_BBI1}wZ7v1tF;*03S zZ=gH03{CvAaXkM9T1$l!{DTgxep4}Eb2Q+#=tK?C{>{+DcSCQ{0qCte3SC*>H~<|# z7)@YU9Dy!$Oer01L<5aac?ue6Iy&*Jv_Bdbr2gIb0eToeOZi7EFASadA2eRon~QP{ zG_leasn{Bws4=?5JH>YB**F{>a3Xpyd!Yd@M8{o>E?@+D$i|@wOiBBLXd(}%{V8O} zN)>N#VaJtdpdaH}bb`OrUTJ)Bi#9?Ns*ASo5O+f3>=xU^17asMv7^yMdza<;Kc5S? zu7uvx>rXP}AALs#%DI?>CiU!3}7XhNT$_x3CFOnrk+xEB2?w!tk$oNclE5A`?U z!jI8Cupu6bX4*IP!&84f`X`*T(9`=0daG8%A5*^`O?2~Hi^IGvn(&V3LU)Pnu>AeM z6BlNF40`&HM+cmSZuQxzzX<)E@D=D8n27G!Bj`$BOnGtq1f6ho{5w{kQ0z$E2|WM4 ziJDMh$F}iM^l|EjCU7pg(u>gvuS@-1Xd*LGeh%I0H_>=2(Ei_I`Ipq_0&CxvTW}lC zzxTEY6?SNW4mc#`Zs?zM^hf)RL=(Ce-QwHOow_IO51>!UoRlApPow=_L=$)o?YFeV zg@sv^W6%KO(ZF}6JQbbrA#~isDL_=wy0r_@0k5Jv@J{>)onR$8;Sce*xE>u>?apG{W@y~4(fS7HxMo=X{BPw26>ZYc z9zBGe&@DS5o`XL3m!S7KaFnj!j#`d7xWQ&OFl#Ue~;yVsQVW#+~P`i z6@hD_6W2!*X@%aB17kO||5@l`dI8%1x|Hum6Pbsu_&Ib37NI-&X3ER&;`uj|kEw9L zS7@d`ro%62fInl!#3E32blj#X*F|?`TXg(xY2P#L`=ap=N_`h}Cyt$%@Bb%L;fhX2 z1DuCWJQy8#Ny?+piLXz2e7p+{cwfr1<2>}R%}00O_4rQOmzQ$!X&P3g{3H4%`wb0H zOZ3Im8ok$t#S`M0aWHy|MxgQTL>DvzU0~@EF3kKXG{cwB313H7`hLov zru+kX4}VSh-&kc*QC|a1tS%b2B|2dnH2#6;_(O|wsp1$eoalJ;RG$*hLpzqxTXSu^ z70vu!Y=AS+30_4Le>Z-L-pU`){y(FKc^!I~H=1lcpa0vHFZc{VGi;f1Yc#QT=t>Sk z0~~=)baFfgP4J?$k3_fnW;C&T6uw2sD9f(ShU8Psa&q zpN+0~9=g&O(SGm5X`pes2j z^@pbZNc7AckFKmQn%E_&zXnbG_LTpJ-uoBOr()^7rJ~|TDm+XTQ;I+h(T~|Z(S-Iv zS9nm$ozN{jCgmPz!o5>}Hu^?9A3ZB0Qyz=Pxi?NfjSE*c3!U(hl;@!nJdLhw0lH<& z(Euys_vkJ785?88eMP(-(03MWK4qd{DX+JIHvr-;_ZpGk~ zFU9iGQXY%m%UjVE-Gj!v|GvEBXQpCqd&_$;nU`|6Z`N5}sg zH<((Su8q+8I&tf%+++haphBCXr)C%QG_*%g!?EZd_d}2AFmxp&(SX;W<8MG;1h=5$ zrlQZ?v*-feMf-i2^5-Qk4Dc-)cn!Lh>r!9&{vx63aceZt&gjmxM*HuL#yKE%L?=2t zc18Q0h$enI8mH7Z9R{Z1BDCWrXuwfv9}{m%`|WAJH}%uwZ1h{|V`+aqzLWOl=)|8C z^`(lhxiF(2ii(P#(Exv-TUT>h@smYkw7xz1DRNxO{n0Zq0(~5AL~qp;wBP^YB6R#G z=!3r&H}fUG$@F62cIbdsXva=y$5YUE<`6WYacRFF-QuUw)BPg4V{f8oUr!FBtiF=tQ@q{=WDyx^vH=XX7pO zvHlXhmA}v6`S*FR@nDhpcId=AqA%%vmr6F86!OiH$+${7zZ+#P8*$?PMm1Y%*ZjKEpH$hj}5&i3Jr=W>ng6_nP z=(tI#pM&n$iz&Z@>~yK(D=yrDztGgDf(}O zCRRIcg~n@u9_}5{!`uqXf5^LEI&?q>c8SNLTh;>|&=*Z`Fq+tvX#eZb1aC&4rrXd7 z??My4H$I5=pBtaT@?Y|NfeQn^l7_eA^7sk*RIEe;{+;?N|0^c0flgQpouD52p4dL^ z%~Ibo?h)JlFQ5MhQDNqt(Fu+~Pic2_<>#V-hoKXdQoahE=z8=pPCyUsY;>memndsyB1e(x0=(sOSx%dtp_;dUV&2)oDibQIn1GYl@H9%L;98Gwa)OSD=J~HKQ z=(v;71pA-~^+(5-hNi=%@k+Gg7&Oq$Xvf>res{`K(FA9telEJgr&7NJ9se=96JMf< ztU`Bob7Od22MW`e>l0soy2GPJ5ep0NSrpJPJ*yd)j+q z`T6g|g&AE?uHe^PDPNBUxGhdX1Kf{(C_RL}ke)yn@H(3CGIZPr=+1qHj$4V|y6@42 ztW`h%|4BuaM~i`*pb6E%@`_{AxNF=S4ctCXI z;k~;L4e$&acmaCaUrl*=+CN60ny=8ESc^_j>9Jykwb1cf#|CKJ9aC-@TR)Z`^7f&^ ztvVzfJEz0Z=mg!-tvem<*FT<*1|EW*>PzEjG=W>t{&%ANCZ~QHx-&B$$98DP zgVWF{|i8FIC*Yg@Nu&!vjS_#iJ>| zfWEumM4yT;<7#x=I&`9HPZVx}CeR3N-zDyg#yJd)b1Igf|9)IJV0ap?LI;jZ{UmgP z2hkNijs|!IUC|Qs7JQ8E#24{r^j1|oSy(M@ipHsnTl@Ss<-$*|1JSMSfqqEzM>}4I zeuuj;^$%eARf~Q{T!MZDTZMj=tNK*Y-VkkXi^e%J^}XUyEdRf;jZ23qXrQ_1R=tCM zNnDloDo+<5Hg(a!EzytL1JMM#D_n&HXl z$7-MP_ugHn{1;)aO3m2xn1+U)g{Ck!<)`BdXrx8x<}XJ3eVX=F=;`?}^?#sS@L$SR zo-N9oJj)(AQEe&=uq9e<5O+++7AdzvkL%v(F+C*pN1-?4xRg&xxeprmTy)%p=(rK_ z`e%7Z+TpfzoQ$q)IvQ|x>K~8KrF{{a=;F9E^&h7EdCIHM=WTWB|4Du2`Na-wRLaGs z=)`r9>H^+8pLc5_m(k>o?uILE#{GWvG%=u{I!_dSpPyI+V?wGQ?qsFGS_XhgL_$KxLpl^=NpD&)XX6X3#=)@h-9qxkVcjR$gxPtT0C;3ve zM0P>{qRXDRBOZ??dMP^oYBZr6Q-5ciijJTC0?)r0Jw}BI%tt3$fNtrl z>G*N{3O!8U$3M|eld3NkZi$ZH2~A+P*d`u;PTUEd@2D3`#ei;9IPgR?p;Oc0Y;;8f z(moUoa9PTuQXYc_yeZ`gXq>yz9h!#j&@1RXtI;@XOR4x9&A8HnBG5)?$4${dTc88B zk4<8Ww6{k4?~BGcDD{V-ZiU5x`Gxdw~B3Idvv88W4F}zM#r6v z-tz%y+~KJ&mALTGj6xH+4NYJMI$#z$;7RlrJcnk!DCM`~`{>uy&(IzEEA>@hDH5oO z#@QSlzZH7ROO3cN@b2iqwrIx==;`hfPeccvo$?@b>#smpI41RD(X%iKjeCEbg--Yw zy0Dj!c}f-UaAAPu=)e{6OZ2h(4xQ)^G=WO57J)a8o1^2lLHjj|Ez-VM+#j8|BijF1 zEdN8eJ<_2!n#eij20o6_jzi<+=!&jF``wW8&FD_uiGCZNf!>OzQeKKCz7mbM8r|XF z)Sr?5PDQ2HiiT?FfSTxldg#R4r+!CtrMsqnAGF_rXy8t%KN3A#-O*cnHk$ZgH2yFw zfBzqmhU?J4%~@X78{~lygQax8attf>ZsTYecsPQ`(K&*o6&@)pew&0JzKM1&*%Ts zRQUM4fDT-O?!*`93V%Q+{w1zUd&L{Yi)Q247@eRUy5d97ah*~=3XRt__9&%8FEpbw zQ@#M*k>TiotI#dJ4&9;gXyDt?i6*E1zSKW}_MemX$I&zJG#dA1biUHNY4{XO@=)|?rovMc>);#UIqC3_a-GTkk@g30#k3bXZfyOxv8DFY6v$&|}k9Hh{ zCUO~;SDg0o@ecGBO-2LFj&soopFnryIduF2G||`NJLtIOSbqLjq+umG;QN$+ML$H= zp%d0#T%7)f=tO&>f%lCEp@BQ4d_?RTd&FMx3@rZ{X+JK!7lW<9E7IXwbR}caM8>0E z!5%^rT8O@ImY}!ftJMDyQCMH4sy9ncfq zq0`aFsb9(i&~Zc1i7rMb8lCzZ(edL`eHXoW7MJ#uSH6&<*6z|m*|JyJdu?Ra+T&qF5|g1+-F zL$~+_^o&f3GtfAX#;4I+_hQO#p=aVfEdM{&f5wFge1%TQ<8DJg zJtw97AbJQNPWd%7-aBag6)Bg#NX3unWAbYnDwY?|YYjB;*65b*7(Tg|yk8`~C0616Q?Y$28l!=lqZ775?|GY)4?+WVMt9_R^wyk??o3~FN6t_ABJ|#u z(0F6hJ^`I)B9{Ll{Ha_x(Hu13lW{&eZ~>b6Yv@GF&=r1y_FsdZ>a}QM8+=ea6`P^u zI_Qt{4bWS>FPcDSEVbg|7%uj}5opK9&_5e_2c2LwI`OY)LjS}KJ}eU77;WDI?Y}+R zuW{;kO1U+ss^V}B=)F0jQ^W){{8|`{@#gkK>o$?Fl1aG4& zd@tos&;-9iclt*(!9PCa`S%UBo(cnR{!#HYOB;P${It4t+yf1CBs%f&=$ROZ?$mYYgp<((rp1})EqMg( z_X3*0GIS?DPWewXe#Ix{IDG#rE-GrF0k=d~zCF4#&C$TS#Qo3!hok+EL;IhU_A}8_ zej&Qz5%C7J-yP_7DInf_=Z!_fpvDPN1m z8H>ifE#*n*PE5n{@Bbg>!T?X8naxj!MJd0F9-5Eh_h`VsW983^Q@;^9Vg1+?Jqx>{ zJ9jYpt+yBY^ql`0&%b{tdnFY1e;}&`;O<(D+ZG3B83)@XOCj#piVOFNy*6(a-I@u`wQjW;`(MBhW3s0}VJG8{#wQ zL?5FQuSLgi@@27r-Qr>BPWC}}aBztWxBg;ui>^Q)yNUS!Si0|kEvG+@;}4~xM3c~< zR47D73Rx*5DneOhWLKz+er_|$NJb$^Wko`gl2M3IC}n3yC?Z0{@BO~7&tI?Wtnc}r zbDitD@8_xT75ogk!o}zWtI!>({7q5c2<_JnO}r<13(t##&|7jv9EQ$!3woAHcW_}Q zccTOEM-Rtj^e|0FJHCkadj-7}Z=!*hp#hhp{eDEptw#H=OZx`Pi$h)&?N=8WU#e)p zg%fWZcSKM3?r6uo(1|+4gX0nCQ*#VDQ6DtUS!o{_FHQSZXrecyd<&MJ|2tE0PdZE} zH*jy!l}tgO?-}U5dl@~Xi_rwX#-_LeO`yiN#jUD`%_z4*6YPfeKR%v><MM9<7L^wzzF_In>4|9M;Fq&593<&aol)Ck#HmQv2BH(k^NGCI2!*1^l9q5g6H3rT}p)m zhR3n!z{k-EpF;<}fv#{ddJ8^AZ`qe9xbY9gxMPrcOBKg+;ntpt{>*nay0Y`oOb5nG z(5)_|{l?Va8povmK6F8o;$vxlIzETSpIa{T{r@#C47?Cs@ki(me2#Wp8Gk_&T^s*H z`&aw1NVFz8aXoZg!?+zfu0`Axov)o`=C3$}3j=kH$Dk|kg$C+_Zta;V4@9?gFgota zI1EkTrg#S$=iZbjp`R6xp@)7xmj5CAhg_K9muP?=&=<lrKORaB(aR&Be87pqtRlM#a10cyxk?&@(Xw?f)dYbI+g)n1k-% zn`puxrTu#}k>BEaq(8s^`MG!(Z;rmhTc9tZ4k>p>2lhh~7!U`eD;b*dwP@la;;7W$ zmGXUPq7R}8K8fXj2>%ine)U?2uIPKT<4@>>f1v~ZOS$IiqP{LV;a2D^*%tlIw+kAl zJDT`$Xq;2g{%5BBe9J!n7jofBE=4E43O(&Np@Hs31KfvxZa;tqo{0vWi}st3o{5F% zt@sQbw*rm-Q(TLV`xndK|2O)j=vX7xj$5J~8>PH`+$r^K&_wr6`4DuXBhZPDNqaAJ z2m7Ik4o084%YWhd_u_gg9C$nWxjYt~cv|Xbp#kTn{Ce8oiSMHmEJIhm679b><$utf zt@3MeC~KkfZTlTgK> zNc7g-iQcOFO1XF<&OslWMd-)qmnr{-enF|crWja1wupP96L&!;J_^f+FYTwIKbQ({37kYqT~KTC zu>3#5*oF%SHb?JKt9UTFr6;0^To4DL{fD9xT#W`Ck@h>$3GYSw-H-NrDCMc>;hT=- ze2e!RhG0rRW4dq6w`=6ZkX!oBGOs6baTq`_+$)(8skoI&OD#2iwJtX#7L} z;Q6=15$V_+JuJtk+$SCTrT)Cs4@&uRH1M@(0ym;te>?hP`xrF-v^W!;_(gP{S4v#C z#S72?@2C7($}7+bR-*&|Nc}%(fXaUs?~6^*ehtw$%~Ea^cSjT1Cw4^pl{)33YZ`i@ zflfpxI4$+(p#v^V{bgw4*Q9(iI&L&N(Kz&;PeRAdj<2BO-a_J+DwcBLz|YXFTaE_! z9({g)Mpynf+OOiTqFfaXTmwzCZfuB-Z;qa=R_JNp5AAR&;Og#a7P@A4xAVtiBr+%c{=(r zy%2p|ze)Q#bcGdv7dy2H`jxH@8n0#C8C_7@zj^-M(hgLZ;UTe0Ivk0v=vef{(JP*f zuHX_h&@l9@+=5PcPkaJR=v6F#{f?iZ^ZoQU&%fXOex2Bz~ z?2Nu(PEGsy=t2gi{t9%bZbVo90Q!o46ixh@_)3Wj11&=DtBLmC8q23W?uQ=EqtkvGI{spGo=aotN-oU&`c&MS@)$IM`_X|9p@}?(Zr#(V ze>Ogk2A+$Kn~(N?JMHhM{*(A6(!W&kT{`@PCbA|S{y_t5_-_$-6STb+I>A=xy={{A z9nnPFpb74o_I*-+FnSBRr2d$4ndk3hE)0A&x`GSirRbr#61``)q7&SQCN>G}|5%)c zCN>N0_Y&IgRrHtK3(&Lk1A5jr{Ez-V|FyWV;}+;4Ym9z)>=BPf59zsRzw6P7N22$9 zbm|{LpModRpA()(&&Ince~IqQ?`VR5Vab*K%Z1jU*}z-HZP6`jiLPvSbYMF)k^RvA zozNXT61^oo(|#&C{;afLgpMB?ud1k2JpaR}aNtNZ@ZIRZ325Lc=xKfiUGaQ0p^wo7 zzKGwY{wH*z-_Y@Yp>a3dpb}U7{X%rUnj4gghApWuP!n`O3v@?zMYnW6G{9kL?~VrO zo$?uJzaaI4(Rf#)iC&-b&GC-Zk1cWGVVDq~L??PF9bQM@XbaI5eU z=&Se|^r^TvK8fZ35$>y8xFv6)nZ1XuWEr}Wuh7G|BIPw`|G&_QHmF>T+XU@b7j18h z?p%wwN9qqk`yGzupZ|M$LB&bwa5}nW=cWFVSc=2YiEmDMG&G)~vp1MPo2`q|M3-NB2|PuUyF^?d%`$AvGHr{a7x z(C6sNzCkDYG38&-g#JV)sMxsZw=vppbF{sFY=mxob2M%nbf@>U?B{<+E_T5~&I0;-E< zwjH{%JiCY37z0^^cGA< z1J8;tq5bEf{T8Nv33>}YPW!jAYKms0TtI>F-jF&gNr_Qbys#Pi&fXJ!XCH?4KNbCv zY9QKwB$~hkbb?3F!}d7(1?5%rFfNO$(Znj%%JaW*tzv6xp@FtWSGFD6aVPZO@wP!% z*aO|_lhJQN=cjx%`bVsz&~a1Ic+=x-bo|Tc4lOL@VkvrWmc{STfxnc36>O7w89!Sa72X_LCez}j(3bizilIhxSU=+?GFCpZY5@UWDRMi+7% z+V8})pNYmhFJ6p3O+&F{Cbx0nV>KE5w0jOc#jm3Szd&vNsW>H`i3UC&UEz?_|1afhQyvjVp{M*VbO-K5w|)W|=TUUx z>G65A->da_{@vO)sBpj%H1lQXPJDwK;4f)kllFhm0F~+&U#EUW%DP+;r$(R8*XZ2I`mc05ss`XrOE2&8Z)g@_2M-CZiKQfySAR?%WILieE(&Sd8U= z|NlN0zHmN8@A*&YfWONfINe(o_0`b8_0Wm7Li;sA6KjD!O>NMLI;XuSdTUNX$DNHX zsK5ICe+U<5T0$qdA>NEma2uNO-RQt^X@3w6_$YeIo z3mfqKTk%0EK1E-x-=Zs6hbHhZntA1|iu%pb3F@NljnG$dv)C4$@L=?o9**`q9=$~; zr+v^?JpXnanhw{X6AwpMcr%*do#;yMLHkdOv*H}I-|Og#7og*prhQr3ze)Lfbe`2E zF3kM5G*sTY$h0~-Q7!bK(5Y#^6g?v)G~v<+ zE}Up2x}~Ghf%l_Z_6Ry)D%x)rI&cm;(LA)@+bJ(e`?A!3g^pW+-rAqhah0|yTdJtR zg#qiut|1i)6ns=Q~xsBe?A)j-PA8d59KGe`~0uq!hx%;z(3-D z=mb?87WJFPdgz&Hh>mL=+oBV-N8@x%xigyZk!ZZ0@kA{D`F~$79C%I|`lA^RidUl( zjZFJ!^w8al?$jgbgwxQo@*LWK9y;*?bmFCGzfaQsbwi$iD}G3aU*n%>;D6JxTB9Q1 zX6R#69}T<}8n|iP0gbmy+V?`^bVL(6EOtZ39p8xOzx+a=!psMuE4va6bQ3!84s^g+ zbf+ey{UJ1wscC;U^)IA64^7~$l$WCMKSASsS4zW5bfRC;52L@($EAAXVuHqKLM_pN zyP^s06Fa886S|PDsXqa|m8YQN&O;L(oN}qeg@K31QE9jvO=uFD>7%KCBK5P-adXl> zAN|x@nEFr91iwb(e2>OijVAbe(a!mAQgo<-X0|CBs6Lu`v)Bd=xE~tu;CMJ1xI3EA zap(l6qceqoNDUxUy}<1)kSxpF*>j%`tIHpJ)~XG?}VqOeK0!i3UsH2qZ5se z6H`AGjXOK#S5sbu<$wSGF&D1*3oK`j9+s8pz_n;X|E0a^cEt*6pyO(z{TrmdDY}z8 zpyOMk{q~IervBjV^8NovD%{$h@kI2SOJ8)W2ckPrLK7T~_8%K3pc6cd?#R@XXQ5Bg z9CX|gG@*~sKVki9yHe4xJ`Ec-Edp0ZS6mAX+yEWeB=tL>3A8~2?}hg3fc85GP2`Bw zABV;}8QrNf(eVRH=`a|*hyRNs(0e%s-GNEyz^Bj&X2)0K!uUQK`180No%lyI(KV_6 z6HU1EFBb;hs9DirGjxKw=)i_?do-ce=mhQ1e*4GHsqYq#Lnk;1o%nP#-q|S+EXt*d z!D+Yx9WV@?;O5ldj!rZ-?GMDqurc-1(4AY1P4RPdL6x>I;%$n?s~fjUeUoyT=dT49 zX4o1XxEp%!I;4DP+Pk5*q9>Z@>F6On7u}i5&{y{`bftHp6W<>nLdQ)>`_t<4KRX>> zK@)icP2}y=f0*)TXn+;*XEf2Z=(r7<7due}-LX1oBHP4f==fGC?~cX)BZUhyJqR7x z1r6Lio)phQzj$1PCU{M}867_cop2ob{6CoTWOU-EQl5$SpVOS@-;39%aN-5&xHNv6 zj$flI{66(Rp>M+9(f3027DfAZXuK9Fw?^acmU4UaR&`AM5iNNBtvEIneb7wLi38D< zUV^UZ>eLU5H=~K&5${g@eeppwp($}1x`7Gt-XaYZ_{#P{d zzt9ObYFPxXjs~ui@|NfVnxOr6L=WknX)kr=!mT_S{Xyf*I0Q}LhBy*UU^F`6J?KjA zM<2gO;xlLhFQXH`j-I7O==e|3@n0bQ_d&I{{a01j^9(>a_36rzwo?gi3`8u9f$q_!vHkn+t7fI zr~NH7(QnX|twC2>WtYOO&=u{B-m-(x#7{u`pN%eXXzFi4jeHzrK9IVQNz}G)CWOyTlIY zz%D5tj|Mm+4nohub#XMh0}rDsehf|QDRdz-Q$H8$`~1J2iqFuMe1i`79$mrewEvU( z4R1f5_R`ZRolO|fG4Vth07u(sZv=f5Qv2U6jHbK`~R1eeAV z4(Ipq*QI^4J&OMI(H&@rCbkQDhW19s9~e8M3+jsQK+o8F51xNJo=Sx)JQMvJ3KygG zPotk+v(SOBqKUqd_IKm^=&kw`ooEHxe|7vFjk69-bfZ0sel<(EsE;-@LOZrV@7ZqX z;o2V!cyv4#O|Vxy6&-(O+WW_g1T^u7(bGN^jWaFn&!M+w4w~?S_z9N({ofBW2TV4fS*%s&oP0$Hi zq`VV)*mg^O2XuT#bOD{vgnOdn2cq#VLC0N%De({*GAw?|<#hh4;2II>E8=3^b8J=<|9F z`k`|dx{@hq0?(xUJi3s1Xo3sl2WY=9(F9kZ{Z_T(`M2VCDqQ)xSg}vhp$a;%X3F(a zZix18mhz4%w@G;~G~xYXkF=kKPJBVigZJV2cZEZ#@NkSkAGf>FL-S~yfll~xoR7Y8 z-$vhj%h6k~0-azbn)q+%PW>G#w=c?@qW$WZQqcuf96Yn%H0&_IWx6Ldur>5lH) z@$sbC7frk$8s{Q(qW__Bu8TLt((POr=pHoicyz#n=n5ZC`>Uyc0}Zq&^&etW%FECl z+@M1-eseV5mT3P*=seq_JF+7(4}bqF746ZC4@L)eP5Br!k(1HKX%KoyZ$%H|{b>K` z=tR$+Mknfxo{j$K4qb`9 z>25&B-Hs-9H@Xv((Qz|VUV!%d1ReLKWk3H{a^cqe8vj7=;Xml%thQf~V10B2jnVdf z&=q$|`;lm3J<)h4qYF3<-NC_V|C`YT-+|@d|BXw-Bs8-}(Lm3jFN_z^MBYpLmuLbj z(G~rP2K*NtS9SklqRr59eKes)sc(uN_8s@<`FG3qO2eV(fUYU`L?<{2{m?lLy|-7Q zPs0c_@Ga<;-=6x3secGf?6H)mr#uV2l`rpKD(=~PRCv$7#+JAq55wjiiv$LuJ2M2G z@alLyn($3n-cmH-adA@GC&#DIot%ZndA*c|1?a#f@#EBgfv$K3I>E2#z`xOiDh?(XaDn3uc*YW%KGaB&sl-I?I1B*nepb6AL`)!3z&=`G6 zwntBW$9NoWPx%7$R^Nde`}|Mk!ay_80572{o`+`scFOOg6MdfYH|V%k=!$+r`&S%P zSPhM{Ir$ zCVC^f!u!yPrlScgNc-1loVADW{ClXXbt<;L9vXOS+zne{Gdu-7WY?lU6^}$$_Bgu5 zucNo(-IPB-7xV>sOID}-&y@d*RS)I)Z%l{H4lO3$1)ZoP8u)ND@G-GB`uz4q&&q%} z7(E+9(HGTK=*0J-;~zp_SdXLqo=M!^4XNYofQfAv%6XEcxwq*Hr9|4s4h5f#?LCS%!#1-h% za}OHuIdpOohIPuH=<;cq8@irhaMaKS}v3 zH1G;E(O**kSNspXg&Q4N#NQZ=Uo+*pB`$pY8lWp^hOS_@)bAI&pes2B4bTUjxL?Wx z&=n6(`KEXqy3)JQLp~loj8CM#G=~c(ejS})DSGd|LI?bk@&;Xt`pwb)jnS30M&IRa z(8ueb*c;u^i{d5me`rEi7nUl9r{Pv~3-65M(7+F(2|bJ6yVuYF@5T?}XXyClXunnH z_&?C`8y{7S+Y(KzF_!=SZ!<33%2w#s?on>wHeCnrI_Va%x7iK;$F0_I2QZ(>aXd)}(&uGGH)Ba!F=;)%pI@-T(+!{?_ zJM@;dLgVj><$wRPFBjg^BhZ1p(1iM+37nn!{;3}vuRynU7~1d7I2PUd3Grz(;dyBP zx6uTa9i8X@2P&L+O&Zpr_o!;OqP|9~g9h3vHbp0Hg|2Y7)OSD^bO?Hgd!+s}wEubN z0x#^w^WTh%%cwB3@#wvqiYD+Jx{{aBcln}}m!pCHi0jeExKj6GWi`>qacgv8ZO{bw zPWeDIfi5L3Tv0bPfnF(}jV5qm%0tizuS5q9OZgV`vAYB9KLwreDfClvCOYmdbYY9I zy!Ge~m43*@pXh)pJqjD3D{qCad_OdBcQk<$&=vQI=b;k~K^OEtwEr-)|IJu_3ebMz zi~3Tas@w(q6zJva;KELq6r?01~?f#T&Ji0Vsyg)rTqpp&MoLx-x(*NxB4mDeg5Zg z;k|p^3S1E1Lj!({PPjb&h~C3r(Ek6%jgBq))rhsxxLcwVHjJ&%xb3n0-~a5yg%k9M zC!kw(3Odo*=ouJ{et3*Pw|XqPBa_haQ_vlmmiCv^{wA8pduYE8(Kw$S%k%Hn{7i+X zb3K~b#yyJ;b>sGE{hsK=2cUtvpgV9>>U*LqJsCZ`=c5ys;bUC{nr(FBh{cepotOZ%a>s&p|I2D%o#=Qp7PM#pjJfCtc9 z^%UCgCG=;yH`4wEI>9P5-e1@ZH#okyCC$-<_DH!sl0d2AFfLp{FLdB(XvcHoKy+n8 zQXYzK_0?$qo6$Gmohd(##(O5^7tsa1hQ?W#`o&oO^S_U|aD_jjEBq53Q00Uo(57fY z_2V{iJG5WRly`}H#P;YRJOG`z2b%ct=+2#n)qVaiOT#cU;HY>P`br*;u5?=JpF>wT z7ftLj0qyrl z>K{-2(fWS7o&+@j`kalb^ZDOb}ro7N6-wPLRU5m{gj%IzFIv$HA)GPMUG0$wV!Di)O!VjkCSsjXo8;(x%xZQ9D8t#SoGMet|X@3Wu z_I-5UzKq|Y{Z^qrg#V7lseMWjcZ*Z<5H_O1j?K`CTcLsWKnJvs9npS=#3RuSJvQy9 zr2cd?&Ut8@LFhs*PxrSe28)ZuvAUCywsu z8|W|P-a`{!i7w!`xE>q&5LP*@=+F%9&-oYnEAu#fT?jhI`FypGP*Nwru;tI|FifF8s~?Ue?b@Y z2O6j1^ulWBxXrQrAzYse2R1~vwi)_ix(hn+Fm#38(R+9bx`HW(sI%s`kv|lT9 z!rjn??Unii(F8l6Q7R@pDiz0}Ps7P*BK^?-L(l+2(H*!RUGd#$|0mI{o{Nrq6Yakk zJ)Fx@zX~1yJDPaa(wRl3_0cWeCgtX6pjK$0wrIxtr@j-KSXXqVJ<@(c%BQCN%y@3< z2gJd#G?WV`xEfu-&1o2oPH-=}r4!;*bj7pNz5u=Fi_q~)(RiPskLUL(|AQ`|YQG}k znuYvNm{PG7y5c72#4XXSYmFYFebGd^pn-a#{Z2x6t}mLvc_|MMuhRzY-mH zJ#OZg@KI@)kcLN6o`$YyR_b3)c|lx?CjME30` za$&|@&^J^!EI;RHz=3GL%h6xHUyt^mgx>4N&>fqBe#f1M?!-sv_|MS^zejiA*Z9}j zJpTr&IH$O`)zET1^iVZK18j?~bSE^xuIS^p7uvromS4qa|I^X_7o`0nbll}=VppRH z-Ea=izXL{5;Y!A$<%iPYNpyl)sh^wjTPeSX23m%`F~3E3V1sju|B6sOG~S_T{BCIf z6VZguC~;v%=b@kP7ozuUBpPUZ>Yqj*r+3jmuJ{@a_$N9+rSpn_o1*2qXnP}cCwIW| z(}bRZebHN1>dJ*3PeL<03k@(3UHRqlns_6c`R!=`d*UQ?Ayd*m4V`cn8s`nP|048O zeT>Xos#uYRHRw-f|DXxfKff5*1kJc5THg*0a1c6Sx6~hp-hw{pL_<@59h$&MbfIJ7 zM6BuO|KnU3@cA^niJtbQDSw&rN_0hkroP$*MM8DZzzxxHEn{1Bi#w)#6xy$M%4e$2 z|Ak!mX1fB-^hWd^-;Vyw`Uv_sz8K#{&&F41zqKh>?_ZP~$F}HG(gp2*D!M}#qdPhh zOK$o7T$tH&=`asn=@;n4tI+@(4k*g?(H~1%qV*lol^&DwX=q|YQoadY$o**C8L5A5 z0MEYzKc>QozE6k0(VeJ1u&_?t8a)F|V{7yPu-NMQ==e5hB732a@j}0G@)x!e?z<_?RUhn=&7HG?!;rMpMj2lA@wh#JGcxE?yLIl8s2(TUrm3+a^lBhYxgVqbJ8&MC_LBRnn)cttvl zL<5gO1K*eO6f}{i(Kp&m^yh=;(ZoMSxAyCl*P<)^4^3?2i;H>c#_h4T&;OoWc+a|^ znV%F-LlZa~op=bAUo7Yfu0ywQ6gu(!SWXb_HxpghTr|$&)PI4EDSvO-=YQitl`3|` z+SnEMMIV!!(5<@{-H``Uehht_o<=8p15M~{bfS;Z_sNQse^0s6;G*B=XoA~d$1ljv8t zH_?9Iqw%X>#`Eu+tj%S`J?w&>g%i;g4?v&m;b@@y&_gv7-NKjSTj-%&g7*6mJxjmF zKhe1VqA$AImlwCRSt%Dgp@DZp?|BC_;KArU?wtA~s$ z>epcTAL0MQg)fK=|5pUu6b;k>4cr(F)ErH0m(=f$_S**?-w}N*4@b{bAN1^8n)X}J zICrCGYa*8a5#DSr%(RispaE`=cgOK)0uQ1)^ej5=H8k+T_;LI$?Z2TD{T(Y^S&XZW z-jdp-RBRKQqW5M;G@*Ua9oawao#Rnx;GQX;jP^StswA0A2aJX#Zts`*Jk!52^np^?#vp{zYHum9Hw7`Ac}k zMMVoVf!0_)o#-p`h}55&`uP2~9#5@(6mCo@UzYkG(VhDz^);?3ZqZg~{}$-@c4)uDu;d%6 zS1S6WD=wiOZ$Sr+Px&cyRO(E z1MW(N?iKe#zc?I%4mb_np|jI|LFxygfi6cUD4`RMNc(8?*4!H(Lg#rhK6@?Cr2}3} z#hd7ai_i%_Nd2eiz5E(o`ATe#zomZb>x#h5(D6H^+y;%ack1^|`Jj|Lm$)#auJPD3 zoQMwWn{vOD&yN?Pp9MqE@gq_{3Qb^iyf^g|(S8r33z>>8y!0#=4tNEf@QwI3x~1=- z13!z)<4QEZ8uUHzAKJghu;L7Dfz~%i58LkXpx6WXs#dBvjf)m^xCT8$kD-Td4tnnv zqX~W*zltm3Pw_Xj|2lNy%GVbQs)eq&K6>jKqYG+*<- zy3*6pas6=vyckV z4aFCXEz!qsSM=xnBhgR0)6oR3L3i#hG|~G~o|N)rEdTy*Di_|%=g`OLwRCtBUBNr4 ze;*C_G5YoT3-p`L-)O%&!;1xMg_gHNC*C3Do#P&8oDReD`F~(Kc1nlC(H-fYa_^M; zq65#31JMbFq&zfUjqczLXd<_v3%eWL;c+QHIGpF-(>XaEXQ5j?2MzoVI$#O<3jQ?h zKg7RMUu{IuULPIT3{7AMbZ2)@{r>2}4n`MpWQhy^aH$9S>C_Xq#|zPWco&+`XhMt81$>eE)o8-M$Mxuh8{Jr(k(y|{?a{MSYRiQkx}jUt3q8drqnQstCmf1y zz$L-MpJD~x0MH6j{ zK2H0h6Ldx=I0{|)3247Q=nBt4-;~#*iQbd;`^#mXzsX!U(GzH9)6p%Ri{6q&Xy8@o z*8LtU-&`b82MydPHbWEG5lw7&EGLZaXlFF>W3Zmje{U}QrZW)D^g48+yU@fQMgvVj z2hPAf@rC#swxGPzEybT9_dpZ65$!(;jdNEVhc0v?mj5H%PjF#|&&7G@mcNUxWC?os zmZ3l0{)A3YZDe6hbcgDqakfEsutnSpjdv)T$l++*qet@mdvQV<`lg{DI>7*R1%uJk z{=ax5I__?4iW5_Q4gJyT9drj*pij{sX|Hi>kw85({#Lj0{5wGtD#{tB!(M2{9nh`s zi1s@y?cLJe6J2>PG+^I&R_ZT6S3DSvb2)a!>u@hzQsTlbt1+s$CtIMWwi$XR4#4Jk zEc&aNq3FuSqX|z<`NfnMqVblcydwUIE~wgV#p7KgmKt$kpk2{>+c6Ej&<~aK&_u35 zS2`w+M*}{L?#yE;PeWHc13hH3(NjMk?Y9IS_X)CN{3ARr9I!n8fW8rbMko9=u0scI zcze;m3cBUh(L`&Z6ShPbv`6X>K_@;DjdvP4?p&znAt8(L?!J>c307Z^k{)Thcl8C#8HE z`czzkF64T2A-AD%??LCeKjjC}#2&ko=fAu%D$H0y3%6n$G(c0dLrXN!&gcr-r`!qc*8{!x$D{pEMC0^D zS9ms>;6-TvtK;=(A~zx59ZMB=6c-g!&@UdZq7%Q5{;u|G^aZrhUB%CY4bbvl=wUk? zo%lrbF+L4VtUsE_MOgmqNc)X(3^wrjpTvb7UqCZ`9nEk7y0Rtc+4wm1U!yznBRcWg z)US^l-CgwG6dhkbZiB|%4js26mj5T5ZBns+?1X;3J`$VYS!kf)Xn>J%OdO8}d>Bo1 zYU-a&{YxpohQ@gt-Qguz{!92@rr{@a3x7icR@_tUOjUGeYQ_3!`_^&0)bEh;&S;{0 zpcC$kJ{<>P`Rt&H9)Azdzk&Ls;Vg8(`SBt&p-ba+=)h6v1oxo*C!%NJ5%ku~!t#p= z?KdCo_bz%U-$&Mi2QJ=u_4o-LcYDTsYu1G~hjGph+n|fo47v{g{0Xy_d^UUWF!Fd0Y`^8+77k z=#K1yCU69rKo4}jlaRAesyL&#s5l25*gxgV(19g1pp6B0Bsfnq0FdZL_)8cbz;Fr;X^HW}!_V-f$1WoA6xGL>`p+CxPaDTDDhUmBH zX7`th%nnV%QRqZHQ$7(5cv|ZF$3f^VyFBG#@g}tYZRk$khwj*eXxzzY`xEGn%r0@^ zie8EH(Qh~l)8Q*L&=2t!wEv%JU!VGoCKUZ@ptqt9I>9#R#LduzcT9N?v|p(`7fx_6 zI`QG?V{~fjFGDB13VnQrp^x38secv?^nA+m(23qe59`v@e~r$w0`31Z@@6bm{F#QT z6N^l1#=7W$t>U(^B^qECG_k$ozG*)YU3nL@UpI6IdZCG(iXPr`%k@0}gShY=eL1?a z5pfI}=mGQfCJEPwysH5GfI zTh;-c=&*P!I^n4)pM$<~2cZexj6OwoViUXzUHL3@r<-h;CoC`ay zLCrpep*|)YL!Eb44A~Z~_{5 z5V~cTqX}Ju-qV}Wvoa1{(G+yT7tj^GmhxLEFF_~%D1L#){|=42s+5X9&;aYvi8p+x z=&%X;IMzic-Wd(BFPgwXXadKe<4!^+JOhn)UdjW}ahIU?{)&`K!@2NujzkCCj%IdO zI!ug@qHX3hf${(W%e3|lhNSso|PpSAL9sZ4#A1(rJf=*Nu zy~p*?cloyHO7=~AXY>VjRLZBL6P_Cfq6rR(mzV2#{!3h#>2+wp5h>qU?!Y%9bm0By z8F&DFOsA*(GJ0zkqjA1OC;m3BM92S%E@Um*Z@u-*U$NmM#egbkLk)C5Ep$co(0jWh zy7jxEFRJ$FM4i$8T~qF!^6}_`PDH=+^+OZ56peQ!mY@F{xNrp{(SY}%9UnjgKZXu? z0zDhgraU+0H&T8teu6IKYc$S^)c=ew;E&W-OlJKCt~$9Guo+sei@xEuL0_$H(R+U= zdhdInUq<_*2@gYW%}wY8_o4|rnEFT2op=t7GcV;ulk@riDHRU*2Ho=S)A3*2lkz5y z76T7JCq5FL_;hqf&OygrjDA)OM;9_4{W;)abV0Mx1m8o)e_G1LS8)ZpWj~>Bw%=3! zGwuJOfj4@r2(T&IzfRmLZi^<`5}kN|G?C-b_r|$s0;Qo`n9=a-_7c)XZ! z<5&|tJatlTjP`4maw|09HgPX>;(b#-06olyq6_I|z0dzCTzCu4h!>y(hoBQ&9^ExFuhtVL7@3tI!F5iGQH| z)~CGT6U9W;&;)Ctr+&+ncR?rG6CJl-JP3{3`3atX2X;-xvFM6=$8*tvm!pU5O7!sD zg5|A`6XL^Y;*X=_o2usD8 z+O$`Cs_0)GT}T~t2ewMNamvlnxH}>55&k2bsn{=eie1q_J>yC7^t7LgCU6nDfGbl! z3|;Xp=#Jc#`bXlE=nHHnZd%cs7XudtSQI~q%g_YAjz6FQe?hlygK5QSu8(f%Zs>~J zqwzY9`Lf3bb;=*G-Ka9Be*3`F0<8?&GcTV}J zc-+%@z)nfUnP`9uu)N~b4^8=6bfV#C0wd9P**J6uoQshocD}hpy}-bVp7{UpW_`@h(jJWrd}R ztGF=p8_)ow(qT;M?@RgNv`|-U%EMD0W!VqdySOmG z{ps)!I`P!hKb`tn=t}0M{3iN&xEQ^4E76HIe7107G@;GWTemfua4R%UTP%68FBcB% zlnzItJ8>+U*y(7$b5eg18t7_t+z2e+y3~(B7cf5U51{YD$IwJ(qVeB&HqZZu>G%zL z&wfO2!CxtFJhK?E1v+7K^iZ`Ji!R{YnLPgvxQGh3 z>QZcq*PKi+7`O?nf8$7`lLW(3O9S#`_svQ0Xr&9I)|oMW%IQ z6Lbe!q4%s^>JN)OQ{NX|$wg?utK)6x_=)IFJ%RRnA>{?gLP`}MbKzn60?llB{5k%G z2B%mP0=lGo%(j@4s}eq6S{yS37UzX=@5Et&ibs`?LUf+tB`!?hR&>HU<5)EC1at+Dq64OHiuPNU_HRB5+M~rS(#7fKJdP?to6P8=7zjG@*mh zIESa)4ZVfOqZ9T+6S*kumtgt%FLB`uXc*e@9yIVnXyB=6LeHQR%t8}-Db7dxElT@` z@w3!_gZBS1^}nTl-CUl38#Z{k2(Sq{aV>Ph1}Qg;+oSiiHJZrY=o@aol#fCaITr1A zB6`dEp*wb2ycX?u%gd$Wbl*vZ0VkmeO-5fp)6f7fqi19(I`F&rQ~Vtr|4-cTm7-iN z)Z-m1EEy zorLy#EIx(KQ+k#Q56O$TJuXbcn$)jD18w|ju~YTX1e&7tEzv~UrhY#(@ImOrho!!I z%EzIJoKnaiu&2YhaR3@%P#lT|x+d*6q5*DA{pi%+m-~~_iPqOgSKct?7HHs|Q{H{dsJ{GwZRpNa8S)79 zn|iNw9f0OE2%UNuI@M@&bMHgPJ&N9>ndqr_J@xORuc*(^seeTGW;L4P+Ic)+UaY6W zft6n?I&O;Pw>>&hWAxbUgiUeJwD&^C_d^rA0PR0G<*U&NN5osvIHS`(!GD2fhsjj< zig^NE(JSbR7oqkRFTcRDC z#1`m)onw1+<(<((+7sR4^U!fuqKEAIcoRC&s5mC|X)E_KS}$SssA?htI+;yQvWx)gB!kC>~t+Ofo;_<*ez4B3!2ED=qWxR^+%!; z9+Pq}^xpMJ{kiBZ9Ek3~RdHC_Z$!_)sFbI~X;}V%oympw_9b+S=c5U{kKTf>&=vfQ zCh{-(-l+UmF<~`y#haoltB1zhChgnD9pf&s?OS>N+fm`bj%c7m(G?z*`V-KJPel_t z6MZpVl=^GY&%qJs_;F~052idBjsFxHXC}JSFTTa|?@HgGLYKxb(_s}l;h*T%ZnU77 zXcKft8lr()p@DZtSKbbdcOcrYE1K|0X#We){)0-nxI7)MLRWAb`iXu|+NYoar=b(P zi0;tqX#YiM;Ez)N0$t&Xl-HmO`5R5B+QOoLX$vmw*a*#N2ejiZ=mZ_mm3Kn>9gZf_ z6HTOVJQwYEG1~9a)L)CnxfzW=22FHaQ7%(MDcj6PK}(0N`(<1JL5|0P_Q(I;r;U!ptk3!2c{_%GV8$~#4U4YXVr z?Y~v(8>6?NMe28pd!Z9|KzHyUEdL4Cp{Y13_CPxxhfZ*EJU#X2pc4*6S2Q^FCG^!i z3>|lG>L;QLnVkBk;orraXsUEbySciaR%sb*aA@ zjdOd-ca^v>z* zV`&R6JjIRBQ@kr0aQ}2T6iuWnnm~`VpBPV1`+4ZZ1JO98wBLf>y3uHyd(i|Y6qYI; z;lj+HNW(0&Nj3o)YnB{R87%wyPyl$Bjx?Dioak# zI1PuR0gpj<;50PAxhW4pGcTbD4omq)bb{N^oxBSj|7hx`p=W4j>YtCVV)?&dwSWr) zeu(bGvUKzCk4t$5n#hYO&qMn!L|<5oQ~nC=_g%_Ap%bq`7xqu;t1KzTS6{;OZ|0j* z;c0G+-osYuuooJjLp%gctSgp(a7MTGGBlwp(3M?-E@XJhqtIJ91|2sE?LT8lzW>jr z!indinJ!5AeKdhD;}2-yHSupWP^G2C%B!K}&Cm(!qEAH=G_jr0cx}<0+PB1o6CRun zUDI$(%DvG5eXuF^Px*HAYx)E<(9HN!+Fy%H(1gE=KcI>HiY8L}GZ!1YUj*6&9Z(DH z*dR7e{q`xhPI-4Uf%fPQ9+>tcQ{MxP*DK||X+In3U#jTOgc!8!ThEaKdp`;Dc!3$5KBXP2_p> z^uCVv`xu?*3-qvkhfcT#JyU<8{Wkoth*u4rrxrS{0hXWtCh4#Py7jxE6Lmya-UAJM zF8b;nfCjiK^*5ji-i+R=yW+!XpMfSc8~s6K9=gCUKIHi~@b^@h=}&3+4V_?pto%`t zKy`Gbo1=+tgZ?Y09ntYepc8dZ`NY^4o#(8S&qsISqK|m~y||nTC%guI5siprQvU!N z;Ia5*>YqvZc{G8!DZd^Upm7()57GFaqi1Gii3?w$l|C*quY+z)Lv)4B(ZFrvKCxrk zJE1E+1|4@YI$^(f0lL6J|8MKgrZ4lC45X6d|DyNl2FG^SRFN{d?WN=Xw6Q-{-M?kMD7u$9Z1Y zjNwM(T#NRf66MrPE_{XFg|2W;I4?Rpga&*JO=uCif)(Lg=(sKD1l!PfUqt;bbo`!h zU)2AO^iO5{6%}Q+B>@gcCpZ#4RJG9nP0-9+pes5h>N`cb2bxIVDEE){OQU`$n%Ib_ zAC2kfzaV|V3kW^EH=+sLflmAYy0V4gQ|Ls`p$WYd^{=6~;0<)#Ms&xvpdVUaq2vBS z7gl;Z`u)EW7p~|SbYK>G%8y3}w2ktqXd>Oiv(f(j(8qNk`t^Ghy3*^>1g4_>?nKAm z8_vh{_y2`ln7}jW3SLA5t&IB3;dV6eSLkE+E&8`S6+TVI)k7y}iY}mKlslq-hSfRB z-Oz>g{*>q64Es{yyL~X4NM3Xtk7j-iI$#RA<+p@)q6yDKCtQf$!spO9uSNZvQQnO9 z-;T!H^=W+n-xD47p#ctthkus*mD17Z%G!n<&;Xs#fZfpq&P8|Vl4u`-#v2;ttI#;( zF}<)97aoQwXn@<$fcHg*htb3MWRy$Lm90eky@4jOG3qx*d3&^ffljmsO=KVXzRB2; z%$ushg#l`#_cRO5uq!&S7dr4f^cG!)CNM7AC!&d@&_j3&`k8S*+V6#MIT~*b8vpI2 zK9%tv7p{COdKSKl4nK##qk;ZLcjBxYgU=}*jz3B$NT1EMBG~;K{d;1a^=w)=mH_&(ahvZl0fu2L3lBH38C43$2_YRuCCbZwD=+=LUKHlG= ziR?%FRoRuaADiOBEz3d=S1U9?+vspAI^axnqF!i1{lbCaW#|fX&{y|(wEqlr2k%A~ z_7FP$Ni=Tixu{rwA;vl->!}cUrMB`NpkB#~UQErOvXlrzQ*Hm;k z5B&@nfNpgWdXH{J58HicB9EZ=b}{-Ctc>y&bVa++1ooo+f5r6a|1O!Z9J>Twy(ZA+A9}PSjop3xlK`P4A&xRwHdwD9l(lgKr&O&$YlBmx|6PXZR7xmMkJQFK0f5zQhxYhH~ z)4v$KH_M~^X0&fc6a5NJbbt6Kx+7(ONFLiN=)`r=1RJCMTSWcI=nkBL>7W05M#H)2 zfD5BM7#%na{hB=n{ZzaG-GO;%|A)}C@I;hXg>QxLhaaPfeSzMxZ!u-Sz0slk-ed(; z(UsOiJGKnlp$Tb0xzIY1JaZ}N8Go$_Pz47z^{AhS2IzELC zT!P-)wdk$+IO=zyEB-agNB)?+C+eXut~OCV2kTM342^Rg`YC%Sn$YYYdH!AD{HR!n zX1FNIFGhJKy47pZ7t(rkhdxK2=O58HrG82*hu)$}=!Dh6qtH0Vq6sxfap9Jo91Y#k zKz*YA0`%|Qu0R7$iS}E<+t7sX4(|^aMEeuyxW(a;XkQlXsn??7ji^|UZq24BZ%3bo zFQfb;dMgf~flKX65;!cZ6xIl9qYG+)?nF~`2byF0`ESRCE9eppy~2J`e=!>1@-PQI zY@?%G9A1saxgop-Jwvynx9mamY`lQ(_`8_?{QratSMn7)(QY)*kC^`0MGxCyKPQi2 zU33L)(SD~zxhp#UY)n55=uQrb`eEoj&qWis7Sn%V|1U1wn!C^ebI?E!hL55XJsrM) zK3*@QiLXTydOO-bi29Gwc%Mc6H{ss!0H*&7c7Jl=gyr@p6IBXpp#LG++Gu-+uv4_3 z750hx{%As%MENrG#Wg&fg#H_Bx1mqXllyu8y*DeOVk3IkzCgEXFS=Ewe@X6Pm9QZi z;AFI4FZ5Ooj`A2Z?)7NDyP|#}`u%@d)W7!&&%YhNq{1yefNt%dnBI{CN#IK8R@OoL zH9@z$1=_EDw4a9VK=-Kc9iE5wAAr6$hM)@@lj6d|G8uiWrbc-dI^lg$ei#k#47x)v zqWxE+{We7VM`(gO(8Ruu^7rBXsQ&|vlgjut$+!|auv%CP4V)F_6VS|CN4XO^L60c+ zK@V|%bO#2a6AVT_mUGb+r_ddqiR^qTV{SA&l~iObLnnGS>bHfv(R=(GdQZ#!mfX5p z=pk#4KJQ)7#4kW6z7*|$RX8Q;@4@PR{y)Wq6TXTD+JH{96Aic@ef~3lPbNGLJ#?+m z7fLs@-(Ym2v0(~5TX&%SA3;BDmqvM$?SB9Nh6@Ayfo6Qf!DPbvXnlw9Z1gMGrC~98 znr}ycxO^0yU=8}I_Zj+~@^5rub^b`=wLuf@hAE%N0bH2jRpV-=O{XNBu!Ge&(Oa87coK&%YJbsPOSR8XZtC%8k*)T7>PQ z{WNr8h0AHuvuul*;vWv|FP)sBAVH2=oYR+SFjOX(N=T?+tJhgEgGoQKgqai zVJ&oAR(JxMXd5(<)6sc)+V1n;mkU=g5Z$W5HsIB0##5twD?0ElG{O7OgchLV7e)IE z;mc^hRcM^I(0&`EeKV&23zplsFvHKIVGm|e-iJ<5jbR3;i|#}tG?Aw0idsc`M>J07 zXg?dB@O=K)aTi5-5SrNK8JWr7|Bi@;ys$7jObo9_C%h%R9ZhIVl<;`!zs~WdX#6OiOog>K8U_rpF(fZtLR~T2VKZ^G=X2xxQEb>-%_QM za%HqVRg(+f@yDS%(Hwn@I-o1;g$6i39Ec7a66N9HC^WHgXrfo6{ia3x3^e|oXx#b8 zj`H_E$wkI9=z!2@FF|^GNicYK=h?%tzxCqy4Ul`WvErGn(iObb+%m z{r7(lapA;|qXVBq1HOdbvlZw)ejDAwEolFp(f)nZ{}GnUO#0VA`)7qM&^KtuD4&bz z|AOTpE}Uotdhd(TEt`r?d=DDvp>PqplI799HtOF;<9rtN-$(uLQ7&6HGyTP*D!Ky= z%kunt(UJ-Wc0ecU8D5AckR9z~!>iG6Fw@cT_oInFiuQjo>Q|w0-;MfD(D+}Y3*29p z=idOChb1ejjNXD;Xno!A1oYmv3r`EXqJes&-<$@ak6#hG)icmU=cE0eLqC*ONB!0m z7k-%RL7(R`RgYIJ39hwn%I*6{PF{}!F-NAy-4i271TB=v`*agIbEznW3sa8CU>{3}PUseN?m z7)@Q!P3wuS<$Sc?plH7wO))3RW23zgooZs#r=oru8t+y#g}c!=#DXJu<#_QN6%KqY zI<7-E=Y4c-+tG9XWt6{06Z$^N`@-L%{x39M>59o_mq*J-g>}%ljViK!FHVex*5N7W z1ZSeJj&5k87oj_ngKl;nx}wSGE9aJ|Ux3D4geJZO-La);+}FajsknGM+=vd?9ObR( zgrA}TzD8d`doc^kRZ5u0nuKPaiuM`UnDSlWGW3w`K*#My6WbT{e}&~LN8*_NZ+aZVg%i{dk4IlYEzlKr z56?jpxFEb7-H9>bWORkMpb6X&-WxuI#(M%?@UxhH<-F(x70b~8tD?i(=!!N*`^RX2 z9Z~)|%HN{__ec46G|oTh4jo=4u_Zdsg=m~XnEsFEW^-Z2BhWzkXvZQn&}4MLlP~(Q!%CzY^uOX#Wl2 zCUoUn!_UI6(0F^$Tlh1Y$e&@EYRR|?DK2cNf(AYs9e7;W2wib=^sse61NKB8%k#s* z=nHB@w2w!}U600{iLU(4sJ|bbH}!aQSR55g&=oBYSD^#fMfp86fh}mFpNHR~0e?cr z{}%og?PaPb^+%uyRZUD~9L0tAur~VUYJOuAQD3?y&%YU#z!lMPU6j{{ThWPkq65E=@*#AB%%hSWD39*sQE1#c zVPo`bYzuUrGth*39Tm@iUn(5fKOBT^X*Qa19-6>JH1G}KbadPuXurAPf@ps_T!QZC z3bg;5XuOT7=&%)C$>-6q3+=c!Jb+I8C#F|?bW$#l?nqVi7G|Nh;-n~_gHALUjhBP2 zd`y@sh>MBgb?AVp=zy8%#CJ#iJanZCqkb{kuLKReGU{JPzd5~&Zut&0@jYn#ACWlx zN3)}$^f5``a%jM+;W1%7^czkS^e6o@(Fwamd(SBMMJKv2>MubL^%c=RJRF7T&;M~; zcyA`51E-;{+FQf9=t>?%6I&L(h90VQ==hJ&Td)(oCBL9&s7$RS;VS6LtE2VDVfyp` z1TOsaYK0E$f^Km?JK!L6;w!=t(VmAsz7xY4=mZa41A9KX(m z8NGuJ*ov-b2Rh(8^bG7ncjzG6?{9RXvd1R%<K>WgFz$?+d z3SG&Y==1+xlsBQ{wxJV!j!v{U>VH93dNAtC)Je9!Jf?r;P>l-*92Yh~Cu)jreQR`r z)6jd?Im*4!o$HHEJS57aqkS@_w>avjqo;l@x&sTYU#Dy!p84Y|V8gLGpz=LSu z$HPTvza{8OmW8ijI&t*Yy%+VH(FC@k^Xv|PsT-gF|3t@f^^$-`q8)3ZZ@4UMi0#l{ z#9fR|REQ>cO*l2`XP}AQ6ZP}Z1Rg;b@;n-UX}weuXblw(coz-43GKK&>c2!M+Ks;R z_n`qxWhH?shsUCE8i&o%d)_w6ozXMV6WyWnQ(QRV0Cd93qC7mxW6`ZHL^Ga<2AG0Q zG&8&#P4Is7C!dGWeygMYU3C2BsNaE(PwkElKZU%E5{03%)W%!>Q;CrVE-#_pmpnpLcY^A?Q6E7UdCW z|FP&q1!zK7NBb0Xr>3JHN_XSYKK~DLVaKKD%3eb!SQqu{(XIV3>UUss%Db=~R%@6{ z+!LL+5Bii0K!1xo4!srA&{z3fY=BEJrw6$?UzUUmC;^+#-G@T=ik6nsPHkF9u2e6Gw~1_crm&I%h46CMaOMG z6WNR=vOUT>qy4+6-xvNK_5Vb{+I^nzMgqzV7Z;SFSG|>0x#DAax$~8$6sE7ux z8Rgn&qK(ja?V|lObe=BgElBm|!ig?K16~#mLkEsTJLaSPCZo^uRJ8v+=u>eYdaIs5 zpNbcwybQA_uSFC88ckq7p33|ghmwnocE=~bu^fwT<-gDg9!0nKDKw!a;c_(K>S*7H z_TL)qpP~tW73IC?4jn+_9l~RoKO?hgl6gHe^JZwER_KUMl=%b8NF@qEpasDqYbx)}`pc3Fw6XLKC<% zoQwX{`vAIgOVJf?Lf>@T&;Muo~f-BKOS&Xju#_%>Y&V6WN3(@$`q_{B9^U<&rP3X07UDR(tcW4Vba6dZkcXXn^ z(XFr0BB`%|?%1(WZW*>i7tj%nlj_EW8TUdb9uN)=hlQii0au|<$#`_9CZmbmf+lcV zl;@%=eIUw@hEJnA@qAKFWxNs%YtR92MThs%Ot+&e+!gLa6a6d9Y?&OUBhUovqvM*O zPeUto=ena$O?Ft6uIKr?nF~+jBj|va(XYoF(Zlowx|M&Thq6kmIW=H#CH2yj?{-@X&f41G{|CrXvZ$Mk49ebjW-4Hb6YtewS&_nhF+HWcPo6-$v zB0r(ONj?0eWPCle+zCzaBJ@3R6}p2rV*2<0)46bqZbu)Zr_t{PZ=jFgSLg&8ZIXb; zp!KcLe&?X=x#-s45Z;R3(!0X@(X;X}daIsj!}D(@i>Pqmi|FB4i5{l6qT?pC-&S-- zc0~QpXu#j1{1-Z|blaqVMYO#-I<6KvadR}WliKq9J8=gpv@3eL&xwxbp)0)*9WXAO zhz7nMP3$Ig{LE;d9X^02^eEc@S+xI4XdX@7?#|UueK1 z+hu07#2V;h+zq{z{je#Hom1<@Qnk~6SpFDQuHkt!hnLYDR-vKZ2;W85@&Vd!8~QQx zC3-A>MsHHJ_DR1w==dgK^RNwi{7!ApT{idgsBleJM2Au6MB~tNJqexYmZ+bL27U-l z^f7e&)96B8iuRRJ|9X@+pz$`Nd$O}VciD@ds4&o9=)|Q@PF8pXTCRppSQ`ycA5FY< z*ady?E#D-tJ_@h2BVaKTNf=<{2 z-N}B@ela@!ay0&E>wU|Qi-rm4il?B5W;&YbY)s!GG|~C!;d>nIzYIEftgj`n*a${(U} zKM(hy3;HF+g#!C=t{VJjD)uUV=jdy&MTcZm& zIWd)SW>oY*1NB8S>mLpZv(bSg(K9g)?O%lM+$3}XQ_%!&Lld5dF63!6k)`3Pv^;-r zbK#e$P2u3_;|MRXW} zZrNB&f4xK#nuR8G5BfdzL3FEML<7Hy_FIddiMOMDTeR;&cj~)vKbpv)PVp_Tbmt^c z`LJ?W6YW?#$_>J%QQs1MI@(9MD>_kcG_mueeIUAn!_hDE1a#u-&eFZ$y-||%9!h2H>JrpOR9ZyCF zo)+b9=)}FyM9+)*i_sU-kZ8|FCmxB$%R~E5j`9@rJ#agw|HaaOM~8*zz(wfFmY@@^ zjPhDE(T!35B-+0UzeC6Wj4t4JOs^bGux$4vUU@W56>Q-|wYhL$WAw+yX6QdEI2V00 zjzJTdHs_92{9&ju@1@EZ4@J|{=o~Mgp-RwxuS9t*dUxK5`t9gl{wm7*!$W9FWzI^b ztc3Qffv&0HS@D&jDHY~+5}L@_=(}kEHo{TplYBe6mdDU9)^DKWKEW2aKkDoENG52H zCf+gZiZ1BvDEI5ZE5i&gj*6k-DD)f_paW8v-n4KAx`Ml+eQx*wdORM9_GiN9(RoV3 zRcQQo{u6j>H0(gPemDB<>IXD|BYGxQ4Ua}As)Gh@g08f6lur%2qKWrG6Yh`pyBOW+ z)D>K~)uYjl*GGq&qkKD>!0d2dxBw0Gc=&A8FGb_5L=#*a^&8Oso1=a^l6Wd(S5*9n z4*VUR@E`PEm+h4dY=k~(&CwNiL?`Hujyo6KxeL+p7o$(?WlF~?|6SqtXhQp={U5C1SBA{q$pp2+ zCg>-2Tl60FK)*nrALTrB2PUD3O+laZS?I*iqluJ6`4x15tI-{LC)|kXuMC^GFw;-a z0AED;Yc$Xg=)nC^|6A1m9p%iklkrD{)zERZ!g^t2^l@#5etdL4o9Ex3v@eYgSD`DL zfNt>(=;53m?GJ>HMEjHI%AQ9PTpqp}?Q775y@S56K0tSDCwi8CI-BR;iT;j?!_G;5 z^sj-wsZK-_>VV$6ZfJn>&h%_Ch%>F3p@Ub zuHvJP++ZB-%%zXJLD)Nnnh^f zSHtyaplwm!iw5`;4P5Hnyp%`GR}$)XQKhm zLkC`j4j3Hm!=pYo%t!kdNBcEsA~!_)t?2k!XurAWt(+h2PhdsAGA!o8jw{eiUylxN zp#wHX{m1Cz_*vBNj`Ds?-#T=na(xqzM8AyHM0cby8vi78zLPQiD}yt=pyDia<$chO z7oYgwo9 z>Y(*aqud(Z;?vLo-NWAKgniLlH2@uVIT~jKI<5fSxvSBI-H9gjNI#x`13XEEJ{Jv3 z(TP@~f!Cmc*P}b|Vbp&d^*hn=-=P!!jPB$i^cI#sKN(*WZLfpIZIX(L*3qFuG;~1& zo`Yt3L6k2JFOT{w(TT={lhH(PiS}9O*_n+l=tcCoe>vJy>$otJ_2|HlqP!zI><)hn z51{=Hp*vCLf+TP`G~tS9d-bq3nqWgTfs@dFCnu&d&fvl==^h=<#q^5Nt-BZvFdThR z6`%=EL0?caG5vEa`nbIq^=qTN6^;Kb8gE~;|Akf3KQi=B0@p{krVYA+PUyg%=sUX~ z`lT~BycS)_9ccf#XkrhdD}6G`i_ycl3|-jkX#ck{{qLfGz=Z?1p&fTcho8_D|BgO} zFCqoI~i&l^u}Ws#@s6S`Og(x1m!s^hfXYNOZ;H(ZJKt zfHTlU??ET}ceFo@_J1Z(Lum=@RJOCY#9p<8mU4_2mi?J8pik{|8*b?_(8?1F{ z@-I;L!S0kNpbK1q-pW1FM`QH`|JJAI8paXtJS9B;WH8?4k3oD_4s-s(5 z2TiOMdKP-13%msFHxf;tAe@wni|f&eZVqRLcZK(&3C%}$JWKe>>VQ^%WNe{ti7{zeI;pLz0f=!>VY&TIj&KQEnXVCq}sqI^ikd8EC@Y zqP-`2cKRfyG6qJ&P&DueG@(3n3n!q5=T>xU?~nTD(Uq)4|BiD#x&!Y;{U>O`U!rI0 zo2cK9&T}X&-~UTrmQ<8SC$56&m7*)D8|6mmz!T7kTcc0MsZrk-z1IWKM24dY=SBN8 z^r@PG?(}`?&;O4_!xA*`YILHv(S+WQ@+W9wUqt=a=tMt8`5-#J^yP^a&^Xn?T4=m1 zG?5c9<<_3Ug^yP+bOl4ufn&l#H1K3}!W*MJ1HG4bqdPS}>KCHppFt;D678$QH_?S` zxSZ$Tj5bl>fbE!mv7i&~MFamD?SG;J%UqH4KO9{^W%MVVEc7w#77jpP*(1;eUX3O+ z3;pfd16QPy0V}95feq*cAEH0Ee-i$T9-<1_iASM{H9!+>itbEnG)_l!VO`LEXQK)B zL*o~sJ3TSQg_+$P74y)a;hsVVzKQ9?(1|`ocj(io|2FD>jQZcuL`n}$##KZotd7Pz z22HFk8ZUKxRJ00DMFVs}CprgxelJ2Z9)t$UMH9PqW+kMfnc2-&{<8KR^%V3s~9b ze;pUT5I#Z!e}x9xgJ!xf>i>xPjN!>&E0se7H$=xZ3r~vrQ^HPYLOs!2djT4MFsA=7 zY*jq*D=JpXRR2UK{< zKaLKcqFcW+{2rb7K=>EBQ)RDAwzdM=za|>5E}CFtEQM{*MA}7rM|6Cb6c-Nc5gpD$ z6B!U4E!qzD3_a95glI@P52me+;P#~2-DC130(MT*9zUzp6GxJ(8uy3 zG{GD+k$g1p)o7wOqB}4P{e8eQXd>^T3BQl-T%P~*Kjoq#i;5#gC4riw zfqSD{J`kO7I2w2~8gN{cC!q;mhyH+Z3%Y>0Xae)l#1};UA~b>LNAdhS@MS6-_9VJG|(MUo{diQZ#40TqW(EFk)=^yiGCWcjruL&_GnLi$%R|9I~oq4J8~#2J0`g` zmC?Yh(F9LM6Y7Gls7KVFi!S71wEq?8t-KQLSA>qc8o7n3jO)3u*nASb_bbsv*P#=>j|ToY%AcWezedM@ALac~K8T!uTE-?F%Ax}*qF+R+MSabv zKMtL^L6n=Jx8Njn;`Zo79nr+PMERU3Uw|e$7>zR=)Bj@l*r+H(16*SRPC*AuM<=)= z>Sv?3Y+kfKj*fda$|d2eXqTg8Gr81^Rhda=-Fgt0;cm&PxX*AIDXrN_if-9r_t!UqfCbk8Q zvjg3!AHsuZys}p%@s3E#^Iwe%1J^=VQWu?|Nt91Q6X+1-&S?K$XuosOz!#&3>oTWz4$@1MK5aEl)fpG7Bl1r4w|>fes?N9cq*!d+-0-=RD3D?08U z^y_)Kg5;29q2Dh~LEF#8^uIDNfD0420^PE);iRaah6cVf%J)b4aWs+V&=r@UiLXRg zx&|G$9-VMYw10+<+gZT#@4($uIN+yf_ygU_jKU;9Cfcu3SR?9dN4XIiuvvH#`i46N zJqtb2l@CA@9F6uLR~XO#cq*LW8gwhCMEN%KF}epG_yoG5Mc5EaqJCS{e~t#;jjs4d zH12QF{&&=uD@yuRPH|!2nrO#k(Fy9KiJTbq?a_dzqdV0d9oIM7`=b*NLZAOD&|6uE zj&mNz-HgV+BTU^F7Y~O|qk&!wm!knzp%cCp^&8NHH=}{KMf)yvf<5TC{ox@rp)$p> za-<)B|B+l|)QyJY!xrcSZPAH4q5(Taxo4F7gcqXy2cruZ7WKK|Rna~vydE3+`F}GP zp8luO6?}C2WE+HeXwaC)@Q3g<@qgXm5@iYD?@)V~>ge;k%fA{x@;qz|YW~ z*oh|aQ}}zBF(C=1TGAItO51TUWuKmX@aVSqw3@Fev4 zzah%g&;c`}JO}MRKU|1T{1m#vCE@aDUyUyCO|;*7^tWzXCh+|GV)!mPl$w|XJ`5dD z5gkx9%EzL&s$SGLi*matcSaNF9ri;LABZj}JL+@7F)1#bs4$!u4cCTK(UtxyyaU~# zIq10gQGO(R8clc!n#e0rejWW%yFTjQLl=_zhzlp!iDtSR4ZI(n==Z44n3N2tfF9DL z(21I&f7x^rdb;~y`uBur0=eNhbO$D&^IeU^Pi5S|g@I2%piVKkw~(ZlpS8sHT) zz$&!gx+uSkPWVC8e;nn{qPz=Dcn>;$f4a=`e=sUaPfk{RING5KI>Av^Zza`95@G^@Imy)>4oT4uSW0HW;EdE z=oWvCPJ93zSK;cUy)t_1YNGXZ(Ebh3AM;N_Z&hzhSurp=f;3#yZb43p=X)i^h5umS1N09G51^SHO=5Zq zT0an*;G`%&h>a+(Mz``CY>b)LCkda3*7rsyx)M$FYII?@p$VoI#>FdWh99E$Y%iMm z;Ws1$jzhPyEn43j4LBs~$AveA^U!|JNBIr(-fu?_}EeP4_~&rCjg7N$l0y=cFOre@}*-*Asn;Y7>Oj9-Zk zZ{hKjH)1pV4SgyaPD@^?-S7m;*=U0Q3hzWGxED=$KF(QnQki2*e*K|TZrw`V^7F3D z9Z_76U6h-jS8{R1(jQeltb5n2;kgBKn$#`bwC1q?ZqF(nH$1y2XLw1Mx~1Q!+NNvP znEYYsfmykQS$X+IS>vVIK?7X6^qI`;lMcD;KS=|URv>>O&gq)$BGP>vg|GE|y<&XPc z{YtJlx%9>5x(*$ala-y9pEqf2esN*e(1QF4g}HenGS14&wq;!L&@s8gm}f+8VNtT) z|1=ek8us z^eV{C8=gNlYiNG{=t9?0I5K}i$+c@rzgV`U)mx=MEPv6J`2}MMpLId9$p6`)f*dDi zYewW`^vfyuPq)I6#YMyOC*&EsFsERAPQm|-7&a!Kot==&gcDxcRQmEN4Mr4?$u7vs znK+IeEaWC;5hbfAw}>Sd<`)#Xs=+%;@2r&BkJ6lqXJ$?=S+%e9Ev2et7Z;7BYhlh9 zZVvX^>wk5~nowLYoLf7(sF=f1n442jRC3SoGB;Ll+cm5IxSZT!BXbH^ z8;7tct2l4OniXS;az`}J8k$>Fl#`B`HJUS+JA#2Zc_lR`mKk-}8UJ<6*v*2`9F37# zc{#=DLzt7-DdYd!!IL+R7@W7>rApSN%3OC??JnsPms6P4)dQE4XP&ui7jw;dwn=8~ zdjD0C(JO!0$T8W&|GQU7x0}kO`ZoFRg$(DhiTSg-^T6c|&CVO0K6)h_y+a-9cgdTa zJ#tJIs*nnwy~=~7%cyr6j4=&UQ(tSHF3kO{`5&tnb&N6g_!HZHlfRpx{v z)912;bJ?WykzI<5@|h&NxNrjNo71dwX8Y24*>i?=&b;Kb|96pD>El_D)jfSgv-1kM zhea8K(p$m`a{nh#$y=Q>yB$_Fee?davA$sDaCDoMwC|bu!6{YGO^z;i#*@tLo0FN! QY?oQgZq7M5mAU2r08w2>_5c6? delta 127589 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 diff --git a/cps/translations/de/LC_MESSAGES/messages.po b/cps/translations/de/LC_MESSAGES/messages.po index afa80df0..01837a24 100644 --- a/cps/translations/de/LC_MESSAGES/messages.po +++ b/cps/translations/de/LC_MESSAGES/messages.po @@ -21,7 +21,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n" -"POT-Creation-Date: 2017-01-21 11:44+0100\n" +"POT-Creation-Date: 2017-01-28 20:35+0100\n" "PO-Revision-Date: 2016-07-12 19:54+0200\n" "Last-Translator: Ozzie Isaacs\n" "Language: de\n" @@ -32,290 +32,314 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:948 +#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:982 msgid "not installed" msgstr "Nicht installiert" -#: cps/helper.py:98 +#: cps/helper.py:136 +#, python-format +msgid "Failed to send mail: %s" +msgstr "E-Mail: %s konnte nicht gesendet werden" + +#: cps/helper.py:143 msgid "Calibre-web test email" msgstr "Calibre-web Test E-Mail" -#: cps/helper.py:99 cps/helper.py:155 +#: cps/helper.py:144 cps/helper.py:154 msgid "This email has been sent via calibre web." msgstr "Die E-Mail wurde via calibre-web versendet" -#: cps/helper.py:136 cps/helper.py:225 -#, python-format -msgid "Failed to send mail: %s" -msgstr "E-Mail: %s konnte nicht gesendet werden" - -#: cps/helper.py:154 cps/templates/detail.html:127 +#: cps/helper.py:153 cps/templates/detail.html:129 msgid "Send to Kindle" msgstr "An Kindle senden" -#: cps/helper.py:177 cps/helper.py:192 +#: cps/helper.py:171 cps/helper.py:186 msgid "Could not find any formats suitable for sending by email" msgstr "" "Konnte keine Formate finden welche für das versenden per E-Mail geeignet " "sind" -#: cps/helper.py:186 +#: cps/helper.py:180 msgid "Could not convert epub to mobi" msgstr "Konnte .epub nicht nach .mobi konvertieren" -#: cps/helper.py:245 +#: cps/helper.py:206 msgid "The requested file could not be read. Maybe wrong permissions?" msgstr "Die angeforderte Datei konnte nicht gelesen werden. Falsche Dateirechte?" -#: cps/ub.py:259 +#: cps/ub.py:380 msgid "Guest" msgstr "Gast" -#: cps/web.py:742 +#: cps/web.py:774 msgid "Latest Books" msgstr "Letzte Bücher" -#: cps/web.py:767 +#: cps/web.py:799 msgid "Hot Books (most downloaded)" msgstr "Beliebte Bücher (die meisten Downloads)" -#: cps/templates/index.xml:29 cps/web.py:775 +#: cps/templates/index.xml:29 cps/web.py:808 msgid "Random Books" msgstr "Zufällige Bücher" -#: cps/web.py:788 +#: cps/web.py:821 msgid "Author list" msgstr "Autorenliste" -#: cps/web.py:805 +#: cps/web.py:838 #, python-format msgid "Author: %(nam)s" msgstr "Autor: %(nam)s" -#: cps/templates/index.xml:50 cps/web.py:818 +#: cps/templates/index.xml:50 cps/web.py:851 msgid "Series list" msgstr "Liste Serien" -#: cps/web.py:829 +#: cps/web.py:862 #, python-format msgid "Series: %(serie)s" msgstr "Serie: %(serie)s" -#: cps/web.py:831 cps/web.py:927 cps/web.py:1126 cps/web.py:1874 +#: cps/web.py:864 cps/web.py:961 cps/web.py:1179 cps/web.py:2041 msgid "Error opening eBook. File does not exist or file is not accessible:" msgstr "" "Buch öffnen fehlgeschlagen. Datei existiert nicht, oder ist nicht " "zugänglich." -#: cps/web.py:862 +#: cps/web.py:895 msgid "Available languages" msgstr "Verfügbare Sprachen" -#: cps/web.py:877 +#: cps/web.py:910 #, python-format msgid "Language: %(name)s" msgstr "Sprache: %(name)s" -#: cps/templates/index.xml:43 cps/web.py:890 +#: cps/templates/index.xml:43 cps/web.py:923 msgid "Category list" msgstr "Kategorieliste" -#: cps/web.py:900 +#: cps/web.py:933 #, python-format msgid "Category: %(name)s" msgstr "Kategorie: %(name)s" -#: cps/web.py:956 +#: cps/web.py:992 msgid "Statistics" msgstr "Statistiken" -#: cps/web.py:965 -msgid "Server restarts" -msgstr "Server startet neu" +#: cps/web.py:1013 +msgid "Performing Restart, please reload page" +msgstr "Führe Neustart durch, bitte Seite neu laden" + +#: cps/web.py:1015 +msgid "Performing shutdown of server, please close window" +msgstr "Server wird runtergefahren, bitte Fenster schließen" -#: cps/web.py:1102 cps/web.py:1109 cps/web.py:1116 cps/web.py:1123 +#: cps/web.py:1091 cps/web.py:1104 +msgid "search" +msgstr "Suche" + +#: cps/web.py:1155 cps/web.py:1162 cps/web.py:1169 cps/web.py:1176 msgid "Read a Book" msgstr "Lese ein Buch" -#: cps/web.py:1172 cps/web.py:1510 +#: cps/web.py:1227 cps/web.py:1649 msgid "Please fill out all fields!" msgstr "Bitte alle Felder ausfüllen!" -#: cps/web.py:1188 +#: cps/web.py:1228 cps/web.py:1244 cps/web.py:1249 cps/web.py:1251 +msgid "register" +msgstr "Registieren" + +#: cps/web.py:1243 msgid "An unknown error occured. Please try again later." msgstr "Es ist ein unbekannter Fehler aufgetreten. Bitte später erneut versuchen." -#: cps/web.py:1193 +#: cps/web.py:1248 msgid "This username or email address is already in use." msgstr "Der Benutzername oder die E-Mailadresse ist in bereits in Benutzung." -#: cps/web.py:1196 -msgid "register" -msgstr "Registieren" - -#: cps/web.py:1212 +#: cps/web.py:1266 #, python-format msgid "you are now logged in as: '%(nickname)s'" msgstr "Du bist nun eingeloggt als '%(nickname)s'" -#: cps/web.py:1216 +#: cps/web.py:1270 msgid "Wrong Username or Password" msgstr "Falscher Benutzername oder Passwort" -#: cps/web.py:1218 +#: cps/web.py:1272 msgid "login" msgstr "Login" -#: cps/web.py:1235 +#: cps/web.py:1289 msgid "Please configure the SMTP mail settings first..." msgstr "Bitte zuerst die SMTP Mail Einstellung konfigurieren ..." -#: cps/web.py:1239 +#: cps/web.py:1293 #, python-format msgid "Book successfully send to %(kindlemail)s" msgstr "Buch erfolgreich versandt an %(kindlemail)s" -#: cps/web.py:1243 +#: cps/web.py:1297 #, python-format msgid "There was an error sending this book: %(res)s" msgstr "Beim Senden des Buchs trat ein Fehler auf: %(res)s" -#: cps/web.py:1245 +#: cps/web.py:1299 msgid "Please configure your kindle email address first..." msgstr "Bitte die Kindle E-Mail Adresse zuuerst konfigurieren..." -#: cps/web.py:1265 +#: cps/web.py:1319 #, python-format msgid "Book has been added to shelf: %(sname)s" msgstr "Das Buch wurde dem Bücherregal: %(sname)s hinzugefügt" -#: cps/web.py:1286 +#: cps/web.py:1340 #, python-format msgid "Book has been removed from shelf: %(sname)s" msgstr "Das Buch wurde aus dem Bücherregal: %(sname)s entfernt" -#: cps/web.py:1304 cps/web.py:1325 +#: cps/web.py:1359 cps/web.py:1383 #, python-format msgid "A shelf with the name '%(title)s' already exists." msgstr "Es existiert bereits ein Bücheregal mit dem Titel '%(title)s'" -#: cps/web.py:1309 +#: cps/web.py:1364 #, python-format msgid "Shelf %(title)s created" msgstr "Bücherregal %(title)s erzeugt" -#: cps/web.py:1311 cps/web.py:1336 +#: cps/web.py:1366 cps/web.py:1394 msgid "There was an error" msgstr "Es trat ein Fehler auf" -#: cps/web.py:1312 cps/web.py:1314 +#: cps/web.py:1367 cps/web.py:1369 msgid "create a shelf" msgstr "Bücherregal erzeugen" -#: cps/web.py:1334 +#: cps/web.py:1392 #, python-format msgid "Shelf %(title)s changed" msgstr "Bücherregal %(title)s verändert" -#: cps/web.py:1337 cps/web.py:1339 +#: cps/web.py:1395 cps/web.py:1397 msgid "Edit a shelf" msgstr "Bücherregal editieren" -#: cps/web.py:1360 +#: cps/web.py:1415 #, python-format msgid "successfully deleted shelf %(name)s" msgstr "Bücherregal %(name)s erfolgreich gelöscht" -#: cps/web.py:1381 +#: cps/web.py:1437 #, python-format msgid "Shelf: '%(name)s'" msgstr "Bücherregal: '%(name)s'" -#: cps/web.py:1409 +#: cps/web.py:1468 #, python-format msgid "Change order of Shelf: '%(name)s'" msgstr "Reihenfolge in Bücherregal '%(name)s' verändern" -#: cps/web.py:1469 +#: cps/web.py:1528 msgid "Found an existing account for this email address." msgstr "Es existiert ein Benutzerkonto für diese E-Mailadresse" -#: cps/web.py:1471 cps/web.py:1474 +#: cps/web.py:1530 cps/web.py:1534 #, python-format msgid "%(name)s's profile" msgstr "%(name)s's Profil" -#: cps/web.py:1472 +#: cps/web.py:1531 msgid "Profile updated" msgstr "Profil aktualisiert" -#: cps/web.py:1483 cps/web.py:1491 +#: cps/web.py:1544 msgid "Admin page" msgstr "Admin Seite" -#: cps/templates/admin.html:33 cps/web.py:1511 +#: cps/web.py:1604 +msgid "Calibre-web configuration updated" +msgstr "Calibre-web Konfiguration wurde aktualisiert" + +#: cps/web.py:1611 cps/web.py:1617 cps/web.py:1630 +msgid "Basic Configuration" +msgstr "Basis Konfiguration" + +#: cps/web.py:1615 +msgid "DB location is not valid, please enter correct path" +msgstr "DB Speicherort ist ungültig, bitte Pfad korrigieren" + +#: cps/templates/admin.html:33 cps/web.py:1651 cps/web.py:1693 msgid "Add new user" msgstr "Neuen Benutzer hinzufügen" -#: cps/web.py:1544 +#: cps/web.py:1687 #, python-format msgid "User '%(user)s' created" msgstr "Benutzer '%(user)s' angelegt" -#: cps/web.py:1548 +#: cps/web.py:1691 msgid "Found an existing account for this email address or nickname." msgstr "" "Es existiert ein Benutzerkonto für diese Emailadresse oder den " "Benutzernamen." -#: cps/web.py:1568 +#: cps/web.py:1711 msgid "Mail settings updated" msgstr "E-Mail Einstellungen aktualisiert" -#: cps/web.py:1574 +#: cps/web.py:1717 #, python-format msgid "Test E-Mail successfully send to %(kindlemail)s" msgstr "Test E-Mail erfolgreich an %(kindlemail)s versendet" -#: cps/web.py:1577 +#: cps/web.py:1720 #, python-format msgid "There was an error sending the Test E-Mail: %(res)s" msgstr "Fehler beim versenden der Test E-Mail: %(res)s" -#: cps/web.py:1578 +#: cps/web.py:1721 msgid "Edit mail settings" msgstr "E-Mail Einstellungen editieren" -#: cps/web.py:1606 +#: cps/web.py:1749 #, python-format msgid "User '%(nick)s' deleted" msgstr "Benutzer '%(nick)s' gelöscht" -#: cps/web.py:1661 +#: cps/web.py:1825 #, python-format msgid "User '%(nick)s' updated" msgstr "Benutzer '%(nick)s' aktualisiert" -#: cps/web.py:1664 +#: cps/web.py:1828 msgid "An unknown error occured." msgstr "Es ist ein unbekanter Fehler aufgetreten" -#: cps/web.py:1666 +#: cps/web.py:1831 #, python-format msgid "Edit User %(nick)s" msgstr "Benutzer %(nick)s bearbeiten" -#: cps/web.py:1904 +#: cps/web.py:2036 cps/web.py:2039 cps/web.py:2113 +msgid "edit metadata" +msgstr "Metadaten editieren" + +#: cps/web.py:2071 #, python-format msgid "Failed to create path %s (Permission denied)." msgstr "Fehler beim Erzeugen des Pfads %s (Zugriff verweigert)" -#: cps/web.py:1909 +#: cps/web.py:2076 #, python-format msgid "Failed to store file %s (Permission denied)." msgstr "Fehler beim speichern der Datei %s (Zugriff verweigert)" -#: cps/web.py:1914 +#: cps/web.py:2081 #, python-format msgid "Failed to delete file %s (Permission denied)." msgstr "Fehler beim Löschen von Datei %s (Zugriff verweigert)" @@ -344,7 +368,7 @@ msgstr "DLS" msgid "Admin" msgstr "Admin" -#: cps/templates/admin.html:12 cps/templates/detail.html:114 +#: cps/templates/admin.html:12 cps/templates/detail.html:116 msgid "Download" msgstr "Download" @@ -392,15 +416,15 @@ msgstr "Absenderadresse" msgid "Change SMTP settings" msgstr "SMTP Einstellungen ändern" -#: cps/templates/admin.html:56 +#: cps/templates/admin.html:56 cps/templates/admin.html:76 msgid "Configuration" msgstr "Konfiguration" #: cps/templates/admin.html:59 -msgid "Log File" -msgstr "Log Datei" +msgid "Calibre DB dir" +msgstr "Calibre DB Pfad" -#: cps/templates/admin.html:60 +#: cps/templates/admin.html:60 cps/templates/config_edit.html:32 msgid "Log Level" msgstr "Log Level" @@ -408,7 +432,7 @@ msgstr "Log Level" msgid "Port" msgstr "Port" -#: cps/templates/admin.html:62 +#: cps/templates/admin.html:62 cps/templates/config_edit.html:19 msgid "Books per page" msgstr "Bücher pro Seite" @@ -424,102 +448,156 @@ msgstr "Öffentliche Registrierung" msgid "Anonymous browsing" msgstr "Anonymer Zugriff" -#: cps/templates/admin.html:76 +#: cps/templates/admin.html:77 msgid "Administration" msgstr "Administration" -#: cps/templates/admin.html:78 +#: cps/templates/admin.html:79 msgid "Restart Calibre-web" msgstr "Calibre-web Neustarten" -#: cps/templates/detail.html:38 -msgid "Book" -msgstr "Buch" - -#: cps/templates/detail.html:38 -msgid "of" -msgstr "von" - -#: cps/templates/detail.html:44 -msgid "language" -msgstr "Sprache" +#: cps/templates/admin.html:80 +msgid "Stop Calibre-web" +msgstr "Stoppe Calibre-web" -#: cps/templates/detail.html:103 -msgid "Description:" -msgstr "Beschreibung" +#: cps/templates/admin.html:91 +msgid "Do you really want to restart Calibre-web?" +msgstr "Calibre-web wirklich neustarten?" -#: cps/templates/detail.html:131 -msgid "Read in browser" -msgstr "Im Browser lesen" +#: cps/templates/admin.html:92 cps/templates/admin.html:107 +msgid "Ok" +msgstr "Ok" -#: cps/templates/detail.html:151 -msgid "Add to shelf" -msgstr "Zu Bücherregal hinzufügen" +#: cps/templates/admin.html:93 cps/templates/admin.html:108 +#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:54 +#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17 +#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:107 +msgid "Back" +msgstr "Zurück" -#: cps/templates/detail.html:191 -msgid "Edit metadata" -msgstr "Metadaten bearbeiten" +#: cps/templates/admin.html:106 +msgid "Do you really want to stop Calibre-web?" +msgstr "Calibre-web wirklich stoppen" -#: cps/templates/edit_book.html:14 cps/templates/search_form.html:6 +#: cps/templates/book_edit.html:16 cps/templates/search_form.html:6 msgid "Book Title" msgstr "Buchtitel" -#: cps/templates/edit_book.html:18 cps/templates/search_form.html:10 +#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10 msgid "Author" msgstr "Autor" -#: cps/templates/edit_book.html:22 +#: cps/templates/book_edit.html:24 msgid "Description" msgstr "Beschreibung" -#: cps/templates/edit_book.html:26 cps/templates/search_form.html:13 +#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13 msgid "Tags" msgstr "Tags" -#: cps/templates/edit_book.html:31 cps/templates/layout.html:133 +#: cps/templates/book_edit.html:33 cps/templates/layout.html:133 #: cps/templates/search_form.html:33 msgid "Series" msgstr "Serien" -#: cps/templates/edit_book.html:35 +#: cps/templates/book_edit.html:37 msgid "Series id" msgstr "Serien ID" -#: cps/templates/edit_book.html:39 +#: cps/templates/book_edit.html:41 msgid "Rating" msgstr "Bewertung" -#: cps/templates/edit_book.html:43 +#: cps/templates/book_edit.html:45 msgid "Cover URL (jpg)" msgstr "Cover URL (jpg)" -#: cps/templates/edit_book.html:48 cps/templates/user_edit.html:27 +#: cps/templates/book_edit.html:50 cps/templates/user_edit.html:27 msgid "Language" msgstr "Sprache" -#: cps/templates/edit_book.html:59 +#: cps/templates/book_edit.html:61 msgid "Yes" msgstr "Ja" -#: cps/templates/edit_book.html:60 +#: cps/templates/book_edit.html:62 msgid "No" msgstr "Nein" -#: cps/templates/edit_book.html:102 +#: cps/templates/book_edit.html:104 msgid "view book after edit" msgstr "Buch nach Bearbeitung ansehen" -#: cps/templates/edit_book.html:105 cps/templates/login.html:19 -#: cps/templates/search_form.html:75 cps/templates/shelf_edit.html:15 -#: cps/templates/user_edit.html:97 +#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:52 +#: cps/templates/login.html:19 cps/templates/search_form.html:75 +#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:105 msgid "Submit" msgstr "Abschicken" -#: cps/templates/edit_book.html:106 cps/templates/email_edit.html:36 -#: cps/templates/shelf_edit.html:17 cps/templates/shelf_order.html:12 -#: cps/templates/user_edit.html:99 -msgid "Back" -msgstr "Zurück" +#: cps/templates/config_edit.html:7 +msgid "Location of Calibre database" +msgstr "Speicherort der Calibre Datenbank" + +#: cps/templates/config_edit.html:11 +msgid "Server Port" +msgstr "Server Port" + +#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7 +msgid "Title" +msgstr "Titel" + +#: cps/templates/config_edit.html:23 +msgid "No. of random books to show" +msgstr "Anzahl Anzeige zufällige Bücher" + +#: cps/templates/config_edit.html:28 +msgid "Regular expression for title sorting" +msgstr "Regulärer Ausdruck für Titelsortierung" + +#: cps/templates/config_edit.html:42 +msgid "Enable uploading" +msgstr "Hochladen aktivieren" + +#: cps/templates/config_edit.html:46 +msgid "Enable anonymous browsing" +msgstr "Anonymes Browsen aktivieren" + +#: cps/templates/config_edit.html:50 +msgid "Enable public registration" +msgstr "Öffentliche Registrierung aktivieren" + +#: cps/templates/config_edit.html:57 cps/templates/layout.html:91 +#: cps/templates/login.html:4 +msgid "Login" +msgstr "Login" + +#: cps/templates/detail.html:40 +msgid "Book" +msgstr "Buch" + +#: cps/templates/detail.html:40 +msgid "of" +msgstr "von" + +#: cps/templates/detail.html:46 +msgid "language" +msgstr "Sprache" + +#: cps/templates/detail.html:105 +msgid "Description:" +msgstr "Beschreibung" + +#: cps/templates/detail.html:133 +msgid "Read in browser" +msgstr "Im Browser lesen" + +#: cps/templates/detail.html:153 +msgid "Add to shelf" +msgstr "Zu Bücherregal hinzufügen" + +#: cps/templates/detail.html:193 +msgid "Edit metadata" +msgstr "Metadaten bearbeiten" #: cps/templates/email_edit.html:11 msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)" @@ -623,10 +701,6 @@ msgstr "Erweiterte Suche" msgid "Logout" msgstr "Logout" -#: cps/templates/layout.html:91 cps/templates/login.html:4 -msgid "Login" -msgstr "Login" - #: cps/templates/layout.html:92 cps/templates/register.html:18 msgid "Register" msgstr "Registrieren" @@ -745,10 +819,6 @@ msgstr "Bücherregal umbenennen" msgid "Change order" msgstr "Reihenfolge ändern" -#: cps/templates/shelf_edit.html:7 -msgid "Title" -msgstr "Titel" - #: cps/templates/shelf_edit.html:12 msgid "should the shelf be public?" msgstr "Soll das Bücherregal öffentlich sein?" @@ -807,37 +877,45 @@ msgstr "Zeige Sprachauswahl" #: cps/templates/user_edit.html:57 msgid "Show series selection" -msgstr "Zeige Auswahl Serien" +msgstr "Zeige Serienauswahl" #: cps/templates/user_edit.html:61 msgid "Show category selection" -msgstr "Zeige Kategorie Auswahl" +msgstr "Zeige Kategorienauswahl" + +#: cps/templates/user_edit.html:65 +msgid "Show author selection" +msgstr "Zeige Autorenauswahl" + +#: cps/templates/user_edit.html:69 +msgid "Show random books in detail view" +msgstr "Zeige zufällige Bücher in der Detailansicht" -#: cps/templates/user_edit.html:68 +#: cps/templates/user_edit.html:76 msgid "Admin user" msgstr "Admin Benutzer" -#: cps/templates/user_edit.html:73 +#: cps/templates/user_edit.html:81 msgid "Allow Downloads" msgstr "Downloads erlauben" -#: cps/templates/user_edit.html:77 +#: cps/templates/user_edit.html:85 msgid "Allow Uploads" msgstr "Uploads erlauben" -#: cps/templates/user_edit.html:81 +#: cps/templates/user_edit.html:89 msgid "Allow Edit" msgstr "Bearbeiten erlauben" -#: cps/templates/user_edit.html:86 +#: cps/templates/user_edit.html:94 msgid "Allow Changing Password" msgstr "Passwort ändern erlauben" -#: cps/templates/user_edit.html:93 +#: cps/templates/user_edit.html:101 msgid "Delete this user" msgstr "Benutzer löschen" -#: cps/templates/user_edit.html:104 +#: cps/templates/user_edit.html:112 msgid "Recent Downloads" msgstr "Letzte Downloads" diff --git a/cps/translations/es/LC_MESSAGES/messages.po b/cps/translations/es/LC_MESSAGES/messages.po index d55eed7b..cdec2672 100644 --- a/cps/translations/es/LC_MESSAGES/messages.po +++ b/cps/translations/es/LC_MESSAGES/messages.po @@ -14,7 +14,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n" -"POT-Creation-Date: 2017-01-21 11:44+0100\n" +"POT-Creation-Date: 2017-01-28 20:35+0100\n" "PO-Revision-Date: 2016-11-13 18:35+0100\n" "Last-Translator: Juan F. Villa \n" "Language: es\n" @@ -25,284 +25,308 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:948 +#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:982 msgid "not installed" msgstr "No instalado" -#: cps/helper.py:98 +#: cps/helper.py:136 +#, python-format +msgid "Failed to send mail: %s" +msgstr "Fallo al enviar el correo : %s" + +#: cps/helper.py:143 msgid "Calibre-web test email" msgstr "Prueba de Correo Calibre-web" -#: cps/helper.py:99 cps/helper.py:155 +#: cps/helper.py:144 cps/helper.py:154 msgid "This email has been sent via calibre web." msgstr "Este mensaje ha sido enviado via Calibre Web." -#: cps/helper.py:136 cps/helper.py:225 -#, python-format -msgid "Failed to send mail: %s" -msgstr "Fallo al enviar el correo : %s" - -#: cps/helper.py:154 cps/templates/detail.html:127 +#: cps/helper.py:153 cps/templates/detail.html:129 msgid "Send to Kindle" msgstr "Enviar a Kindle" -#: cps/helper.py:177 cps/helper.py:192 +#: cps/helper.py:171 cps/helper.py:186 msgid "Could not find any formats suitable for sending by email" msgstr "Formato no compatible para enviar por correo electronico" -#: cps/helper.py:186 +#: cps/helper.py:180 msgid "Could not convert epub to mobi" msgstr "No fue posible convertir de epub a mobi" -#: cps/helper.py:245 +#: cps/helper.py:206 msgid "The requested file could not be read. Maybe wrong permissions?" msgstr "El fichero solicitado no puede ser leido. Problema de permisos?" -#: cps/ub.py:259 +#: cps/ub.py:380 msgid "Guest" msgstr "" -#: cps/web.py:742 +#: cps/web.py:774 msgid "Latest Books" msgstr "Libros recientes" -#: cps/web.py:767 +#: cps/web.py:799 msgid "Hot Books (most downloaded)" msgstr "Libros Populares (los mas descargados)" -#: cps/templates/index.xml:29 cps/web.py:775 +#: cps/templates/index.xml:29 cps/web.py:808 msgid "Random Books" msgstr "Libros al Azar" -#: cps/web.py:788 +#: cps/web.py:821 msgid "Author list" msgstr "Lista de Autores" -#: cps/web.py:805 +#: cps/web.py:838 #, python-format msgid "Author: %(nam)s" msgstr "Autor : %(nam)s" -#: cps/templates/index.xml:50 cps/web.py:818 +#: cps/templates/index.xml:50 cps/web.py:851 msgid "Series list" msgstr "lista de Series" -#: cps/web.py:829 +#: cps/web.py:862 #, python-format msgid "Series: %(serie)s" msgstr "Series : %(serie)s" -#: cps/web.py:831 cps/web.py:927 cps/web.py:1126 cps/web.py:1874 +#: cps/web.py:864 cps/web.py:961 cps/web.py:1179 cps/web.py:2041 msgid "Error opening eBook. File does not exist or file is not accessible:" msgstr "Error en apertura del Objeto. El archivo no existe o no es accesible" -#: cps/web.py:862 +#: cps/web.py:895 msgid "Available languages" msgstr "Lenguajes disponibles" -#: cps/web.py:877 +#: cps/web.py:910 #, python-format msgid "Language: %(name)s" msgstr "Lenguaje: %(name)s" -#: cps/templates/index.xml:43 cps/web.py:890 +#: cps/templates/index.xml:43 cps/web.py:923 msgid "Category list" msgstr "Lista de Categorias" -#: cps/web.py:900 +#: cps/web.py:933 #, python-format msgid "Category: %(name)s" msgstr "Categoria : %(name)s" -#: cps/web.py:956 +#: cps/web.py:992 msgid "Statistics" msgstr "Estadisticas" -#: cps/web.py:965 -msgid "Server restarts" -msgstr "Reinicio del Servidor" +#: cps/web.py:1013 +msgid "Performing Restart, please reload page" +msgstr "" + +#: cps/web.py:1015 +msgid "Performing shutdown of server, please close window" +msgstr "" + +#: cps/web.py:1091 cps/web.py:1104 +msgid "search" +msgstr "" -#: cps/web.py:1102 cps/web.py:1109 cps/web.py:1116 cps/web.py:1123 +#: cps/web.py:1155 cps/web.py:1162 cps/web.py:1169 cps/web.py:1176 msgid "Read a Book" msgstr "Leer un Libro" -#: cps/web.py:1172 cps/web.py:1510 +#: cps/web.py:1227 cps/web.py:1649 msgid "Please fill out all fields!" msgstr "Por favor llenar todos los campos!" -#: cps/web.py:1188 +#: cps/web.py:1228 cps/web.py:1244 cps/web.py:1249 cps/web.py:1251 +msgid "register" +msgstr "Registrarse" + +#: cps/web.py:1243 msgid "An unknown error occured. Please try again later." msgstr "Ocurrio un error. Intentar de nuevo mas tarde." -#: cps/web.py:1193 +#: cps/web.py:1248 msgid "This username or email address is already in use." msgstr "Usuario o direccion de correo en uso." -#: cps/web.py:1196 -msgid "register" -msgstr "Registrarse" - -#: cps/web.py:1212 +#: cps/web.py:1266 #, python-format msgid "you are now logged in as: '%(nickname)s'" msgstr "Sesion iniciada como : '%(nickname)s'" -#: cps/web.py:1216 +#: cps/web.py:1270 msgid "Wrong Username or Password" msgstr "Usuario o contraseña invalido" -#: cps/web.py:1218 +#: cps/web.py:1272 msgid "login" msgstr "Iniciar Sesion" -#: cps/web.py:1235 +#: cps/web.py:1289 msgid "Please configure the SMTP mail settings first..." msgstr "Configurar primero los parametros SMTP por favor..." -#: cps/web.py:1239 +#: cps/web.py:1293 #, python-format msgid "Book successfully send to %(kindlemail)s" msgstr "Envio de Libro a %(kindlemail)s correctamente" -#: cps/web.py:1243 +#: cps/web.py:1297 #, python-format msgid "There was an error sending this book: %(res)s" msgstr "Ha sucedido un error en el envio del Libro: %(res)s" -#: cps/web.py:1245 +#: cps/web.py:1299 msgid "Please configure your kindle email address first..." msgstr "Configurar primero la dirección de correo Kindle por favor..." -#: cps/web.py:1265 +#: cps/web.py:1319 #, python-format msgid "Book has been added to shelf: %(sname)s" msgstr "El libro fue agregado a el estante: %(sname)s" -#: cps/web.py:1286 +#: cps/web.py:1340 #, python-format msgid "Book has been removed from shelf: %(sname)s" msgstr "El libro fue removido del estante: %(sname)s" -#: cps/web.py:1304 cps/web.py:1325 +#: cps/web.py:1359 cps/web.py:1383 #, python-format msgid "A shelf with the name '%(title)s' already exists." msgstr "Une étagère de ce nom '%(title)s' existe déjà." -#: cps/web.py:1309 +#: cps/web.py:1364 #, python-format msgid "Shelf %(title)s created" msgstr "Estante %(title)s creado" -#: cps/web.py:1311 cps/web.py:1336 +#: cps/web.py:1366 cps/web.py:1394 msgid "There was an error" msgstr "Hemos tenido un error" -#: cps/web.py:1312 cps/web.py:1314 +#: cps/web.py:1367 cps/web.py:1369 msgid "create a shelf" msgstr "Crear un Estante" -#: cps/web.py:1334 +#: cps/web.py:1392 #, python-format msgid "Shelf %(title)s changed" msgstr "" -#: cps/web.py:1337 cps/web.py:1339 +#: cps/web.py:1395 cps/web.py:1397 msgid "Edit a shelf" msgstr "" -#: cps/web.py:1360 +#: cps/web.py:1415 #, python-format msgid "successfully deleted shelf %(name)s" msgstr "Estante %(name)s fue borrado correctamente" -#: cps/web.py:1381 +#: cps/web.py:1437 #, python-format msgid "Shelf: '%(name)s'" msgstr "Estante: '%(name)s'" -#: cps/web.py:1409 +#: cps/web.py:1468 #, python-format msgid "Change order of Shelf: '%(name)s'" msgstr "" -#: cps/web.py:1469 +#: cps/web.py:1528 msgid "Found an existing account for this email address." msgstr "Existe una cuenta vinculada a esta cuenta de correo." -#: cps/web.py:1471 cps/web.py:1474 +#: cps/web.py:1530 cps/web.py:1534 #, python-format msgid "%(name)s's profile" msgstr "Perfil de %(name)s" -#: cps/web.py:1472 +#: cps/web.py:1531 msgid "Profile updated" msgstr "Perfil actualizado" -#: cps/web.py:1483 cps/web.py:1491 +#: cps/web.py:1544 msgid "Admin page" msgstr "" -#: cps/templates/admin.html:33 cps/web.py:1511 +#: cps/web.py:1604 +msgid "Calibre-web configuration updated" +msgstr "" + +#: cps/web.py:1611 cps/web.py:1617 cps/web.py:1630 +msgid "Basic Configuration" +msgstr "" + +#: cps/web.py:1615 +msgid "DB location is not valid, please enter correct path" +msgstr "" + +#: cps/templates/admin.html:33 cps/web.py:1651 cps/web.py:1693 msgid "Add new user" msgstr "Agregar un nuevo usuario" -#: cps/web.py:1544 +#: cps/web.py:1687 #, python-format msgid "User '%(user)s' created" msgstr "Usuario '%(user)s' creado" -#: cps/web.py:1548 +#: cps/web.py:1691 msgid "Found an existing account for this email address or nickname." msgstr "Se ha encontrado una cuenta vinculada a esta cuenta de correo o usuario." -#: cps/web.py:1568 +#: cps/web.py:1711 msgid "Mail settings updated" msgstr "Parametros de correo actualizados" -#: cps/web.py:1574 +#: cps/web.py:1717 #, python-format msgid "Test E-Mail successfully send to %(kindlemail)s" msgstr "Exito al realizar envio de prueba a %(kindlemail)s" -#: cps/web.py:1577 +#: cps/web.py:1720 #, python-format msgid "There was an error sending the Test E-Mail: %(res)s" msgstr "Error al realizar envio de prueba a E-Mail: %(res)s" -#: cps/web.py:1578 +#: cps/web.py:1721 msgid "Edit mail settings" msgstr "Editar parametros de correo" -#: cps/web.py:1606 +#: cps/web.py:1749 #, python-format msgid "User '%(nick)s' deleted" msgstr "Usuario '%(nick)s' borrado" -#: cps/web.py:1661 +#: cps/web.py:1825 #, python-format msgid "User '%(nick)s' updated" msgstr "Usuario '%(nick)s' Actualizado" -#: cps/web.py:1664 +#: cps/web.py:1828 msgid "An unknown error occured." msgstr "Oups ! Error inesperado." -#: cps/web.py:1666 +#: cps/web.py:1831 #, python-format msgid "Edit User %(nick)s" msgstr "Editar Usuario %(nick)s" -#: cps/web.py:1904 +#: cps/web.py:2036 cps/web.py:2039 cps/web.py:2113 +msgid "edit metadata" +msgstr "" + +#: cps/web.py:2071 #, python-format msgid "Failed to create path %s (Permission denied)." msgstr "Fallo al crear la ruta %s (permiso negado)" -#: cps/web.py:1909 +#: cps/web.py:2076 #, python-format msgid "Failed to store file %s (Permission denied)." msgstr "Fallo al almacenar el archivo %s (permiso negado)" -#: cps/web.py:1914 +#: cps/web.py:2081 #, python-format msgid "Failed to delete file %s (Permission denied)." msgstr "Fallo al borrar el archivo %s (permiso negado)" @@ -331,7 +355,7 @@ msgstr "DLS" msgid "Admin" msgstr "Administracion" -#: cps/templates/admin.html:12 cps/templates/detail.html:114 +#: cps/templates/admin.html:12 cps/templates/detail.html:116 msgid "Download" msgstr "Descarga" @@ -379,15 +403,15 @@ msgstr "Desde el correo" msgid "Change SMTP settings" msgstr "Cambiar parametros smtp" -#: cps/templates/admin.html:56 +#: cps/templates/admin.html:56 cps/templates/admin.html:76 msgid "Configuration" msgstr "" #: cps/templates/admin.html:59 -msgid "Log File" +msgid "Calibre DB dir" msgstr "" -#: cps/templates/admin.html:60 +#: cps/templates/admin.html:60 cps/templates/config_edit.html:32 msgid "Log Level" msgstr "" @@ -395,7 +419,7 @@ msgstr "" msgid "Port" msgstr "" -#: cps/templates/admin.html:62 +#: cps/templates/admin.html:62 cps/templates/config_edit.html:19 msgid "Books per page" msgstr "" @@ -411,102 +435,156 @@ msgstr "" msgid "Anonymous browsing" msgstr "" -#: cps/templates/admin.html:76 +#: cps/templates/admin.html:77 msgid "Administration" msgstr "" -#: cps/templates/admin.html:78 +#: cps/templates/admin.html:79 msgid "Restart Calibre-web" msgstr "" -#: cps/templates/detail.html:38 -msgid "Book" -msgstr "Libro" - -#: cps/templates/detail.html:38 -msgid "of" -msgstr "de" - -#: cps/templates/detail.html:44 -msgid "language" -msgstr "Lenguaje" +#: cps/templates/admin.html:80 +msgid "Stop Calibre-web" +msgstr "" -#: cps/templates/detail.html:103 -msgid "Description:" -msgstr "Descripcion :" +#: cps/templates/admin.html:91 +msgid "Do you really want to restart Calibre-web?" +msgstr "" -#: cps/templates/detail.html:131 -msgid "Read in browser" -msgstr "Ver en el navegador" +#: cps/templates/admin.html:92 cps/templates/admin.html:107 +msgid "Ok" +msgstr "" -#: cps/templates/detail.html:151 -msgid "Add to shelf" -msgstr "Agregar al estante" +#: cps/templates/admin.html:93 cps/templates/admin.html:108 +#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:54 +#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17 +#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:107 +msgid "Back" +msgstr "Regresar" -#: cps/templates/detail.html:191 -msgid "Edit metadata" -msgstr "Editar la metadata" +#: cps/templates/admin.html:106 +msgid "Do you really want to stop Calibre-web?" +msgstr "" -#: cps/templates/edit_book.html:14 cps/templates/search_form.html:6 +#: cps/templates/book_edit.html:16 cps/templates/search_form.html:6 msgid "Book Title" msgstr "Titulo del Libro" -#: cps/templates/edit_book.html:18 cps/templates/search_form.html:10 +#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10 msgid "Author" msgstr "Autor" -#: cps/templates/edit_book.html:22 +#: cps/templates/book_edit.html:24 msgid "Description" msgstr "Descripcion" -#: cps/templates/edit_book.html:26 cps/templates/search_form.html:13 +#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13 msgid "Tags" msgstr "Etiqueta" -#: cps/templates/edit_book.html:31 cps/templates/layout.html:133 +#: cps/templates/book_edit.html:33 cps/templates/layout.html:133 #: cps/templates/search_form.html:33 msgid "Series" msgstr "Series" -#: cps/templates/edit_book.html:35 +#: cps/templates/book_edit.html:37 msgid "Series id" msgstr "Id de la serie" -#: cps/templates/edit_book.html:39 +#: cps/templates/book_edit.html:41 msgid "Rating" msgstr "Puntaje" -#: cps/templates/edit_book.html:43 +#: cps/templates/book_edit.html:45 msgid "Cover URL (jpg)" msgstr "URL de la Cubierta (jpg)" -#: cps/templates/edit_book.html:48 cps/templates/user_edit.html:27 +#: cps/templates/book_edit.html:50 cps/templates/user_edit.html:27 msgid "Language" msgstr "Lenguaje" -#: cps/templates/edit_book.html:59 +#: cps/templates/book_edit.html:61 msgid "Yes" msgstr "Si" -#: cps/templates/edit_book.html:60 +#: cps/templates/book_edit.html:62 msgid "No" msgstr "No" -#: cps/templates/edit_book.html:102 +#: cps/templates/book_edit.html:104 msgid "view book after edit" msgstr "Ver libro tras la edicion" -#: cps/templates/edit_book.html:105 cps/templates/login.html:19 -#: cps/templates/search_form.html:75 cps/templates/shelf_edit.html:15 -#: cps/templates/user_edit.html:97 +#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:52 +#: cps/templates/login.html:19 cps/templates/search_form.html:75 +#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:105 msgid "Submit" msgstr "Enviar" -#: cps/templates/edit_book.html:106 cps/templates/email_edit.html:36 -#: cps/templates/shelf_edit.html:17 cps/templates/shelf_order.html:12 -#: cps/templates/user_edit.html:99 -msgid "Back" -msgstr "Regresar" +#: cps/templates/config_edit.html:7 +msgid "Location of Calibre database" +msgstr "" + +#: cps/templates/config_edit.html:11 +msgid "Server Port" +msgstr "" + +#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7 +msgid "Title" +msgstr "Titulo" + +#: cps/templates/config_edit.html:23 +msgid "No. of random books to show" +msgstr "" + +#: cps/templates/config_edit.html:28 +msgid "Regular expression for title sorting" +msgstr "" + +#: cps/templates/config_edit.html:42 +msgid "Enable uploading" +msgstr "" + +#: cps/templates/config_edit.html:46 +msgid "Enable anonymous browsing" +msgstr "" + +#: cps/templates/config_edit.html:50 +msgid "Enable public registration" +msgstr "" + +#: cps/templates/config_edit.html:57 cps/templates/layout.html:91 +#: cps/templates/login.html:4 +msgid "Login" +msgstr "Inicio de Sesion" + +#: cps/templates/detail.html:40 +msgid "Book" +msgstr "Libro" + +#: cps/templates/detail.html:40 +msgid "of" +msgstr "de" + +#: cps/templates/detail.html:46 +msgid "language" +msgstr "Lenguaje" + +#: cps/templates/detail.html:105 +msgid "Description:" +msgstr "Descripcion :" + +#: cps/templates/detail.html:133 +msgid "Read in browser" +msgstr "Ver en el navegador" + +#: cps/templates/detail.html:153 +msgid "Add to shelf" +msgstr "Agregar al estante" + +#: cps/templates/detail.html:193 +msgid "Edit metadata" +msgstr "Editar la metadata" #: cps/templates/email_edit.html:11 msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)" @@ -608,10 +686,6 @@ msgstr "Busqueda avanzada" msgid "Logout" msgstr "Cerrar Sesion" -#: cps/templates/layout.html:91 cps/templates/login.html:4 -msgid "Login" -msgstr "Inicio de Sesion" - #: cps/templates/layout.html:92 cps/templates/register.html:18 msgid "Register" msgstr "Registro" @@ -730,10 +804,6 @@ msgstr "Editar nombre del estante" msgid "Change order" msgstr "" -#: cps/templates/shelf_edit.html:7 -msgid "Title" -msgstr "Titulo" - #: cps/templates/shelf_edit.html:12 msgid "should the shelf be public?" msgstr "Debe ser el estante publico?" @@ -798,31 +868,39 @@ msgstr "Mostrar series seleccionadas" msgid "Show category selection" msgstr "Mostrar categorias elegidas" -#: cps/templates/user_edit.html:68 +#: cps/templates/user_edit.html:65 +msgid "Show author selection" +msgstr "" + +#: cps/templates/user_edit.html:69 +msgid "Show random books in detail view" +msgstr "" + +#: cps/templates/user_edit.html:76 msgid "Admin user" msgstr "Usuario Administrador" -#: cps/templates/user_edit.html:73 +#: cps/templates/user_edit.html:81 msgid "Allow Downloads" msgstr "Permitir descargas" -#: cps/templates/user_edit.html:77 +#: cps/templates/user_edit.html:85 msgid "Allow Uploads" msgstr "Permitir subidas de archivos" -#: cps/templates/user_edit.html:81 +#: cps/templates/user_edit.html:89 msgid "Allow Edit" msgstr "Permitir editar" -#: cps/templates/user_edit.html:86 +#: cps/templates/user_edit.html:94 msgid "Allow Changing Password" msgstr "Permitir cambiar la clave" -#: cps/templates/user_edit.html:93 +#: cps/templates/user_edit.html:101 msgid "Delete this user" msgstr "Borrar este usuario" -#: cps/templates/user_edit.html:104 +#: cps/templates/user_edit.html:112 msgid "Recent Downloads" msgstr "Descargas Recientes" diff --git a/cps/translations/fr/LC_MESSAGES/messages.po b/cps/translations/fr/LC_MESSAGES/messages.po index 293eef7b..eafc7a81 100644 --- a/cps/translations/fr/LC_MESSAGES/messages.po +++ b/cps/translations/fr/LC_MESSAGES/messages.po @@ -20,7 +20,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n" -"POT-Creation-Date: 2017-01-21 11:44+0100\n" +"POT-Creation-Date: 2017-01-28 20:35+0100\n" "PO-Revision-Date: 2016-11-13 18:35+0100\n" "Last-Translator: Nicolas Roudninski \n" "Language: fr\n" @@ -31,288 +31,312 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:948 +#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:982 msgid "not installed" msgstr "" -#: cps/helper.py:98 +#: cps/helper.py:136 +#, python-format +msgid "Failed to send mail: %s" +msgstr "Impossible d'envoyer le courriel : %s" + +#: cps/helper.py:143 msgid "Calibre-web test email" msgstr "" -#: cps/helper.py:99 cps/helper.py:155 +#: cps/helper.py:144 cps/helper.py:154 msgid "This email has been sent via calibre web." msgstr "Ce message a été envoyé depuis calibre web." -#: cps/helper.py:136 cps/helper.py:225 -#, python-format -msgid "Failed to send mail: %s" -msgstr "Impossible d'envoyer le courriel : %s" - -#: cps/helper.py:154 cps/templates/detail.html:127 +#: cps/helper.py:153 cps/templates/detail.html:129 msgid "Send to Kindle" msgstr "Envoyer ver Kindle" -#: cps/helper.py:177 cps/helper.py:192 +#: cps/helper.py:171 cps/helper.py:186 msgid "Could not find any formats suitable for sending by email" msgstr "Impossible de trouver un format adapté à envoyer par courriel" -#: cps/helper.py:186 +#: cps/helper.py:180 msgid "Could not convert epub to mobi" msgstr "Impossible de convertir epub vers mobi" -#: cps/helper.py:245 +#: cps/helper.py:206 msgid "The requested file could not be read. Maybe wrong permissions?" msgstr "" "Le fichier demandé ne peux pas être lu. Peut-être de mauvaises " "permissions ?" -#: cps/ub.py:259 +#: cps/ub.py:380 msgid "Guest" msgstr "" -#: cps/web.py:742 +#: cps/web.py:774 msgid "Latest Books" msgstr "Derniers livres" -#: cps/web.py:767 +#: cps/web.py:799 msgid "Hot Books (most downloaded)" msgstr "Livres populaires (les plus téléchargés)" -#: cps/templates/index.xml:29 cps/web.py:775 +#: cps/templates/index.xml:29 cps/web.py:808 msgid "Random Books" msgstr "Livres au hasard" -#: cps/web.py:788 +#: cps/web.py:821 msgid "Author list" msgstr "Liste des auteurs" -#: cps/web.py:805 +#: cps/web.py:838 #, python-format msgid "Author: %(nam)s" msgstr "Auteur : %(nam)s" -#: cps/templates/index.xml:50 cps/web.py:818 +#: cps/templates/index.xml:50 cps/web.py:851 msgid "Series list" msgstr "Liste des séries" -#: cps/web.py:829 +#: cps/web.py:862 #, python-format msgid "Series: %(serie)s" msgstr "Séries : %(serie)s" -#: cps/web.py:831 cps/web.py:927 cps/web.py:1126 cps/web.py:1874 +#: cps/web.py:864 cps/web.py:961 cps/web.py:1179 cps/web.py:2041 msgid "Error opening eBook. File does not exist or file is not accessible:" msgstr "" "Erreur d'ouverture du livre numérique. Le fichier n'existe pas ou n'est " "pas accessible :" -#: cps/web.py:862 +#: cps/web.py:895 msgid "Available languages" msgstr "Langues disponibles" -#: cps/web.py:877 +#: cps/web.py:910 #, python-format msgid "Language: %(name)s" msgstr "Langue : %(name)s" -#: cps/templates/index.xml:43 cps/web.py:890 +#: cps/templates/index.xml:43 cps/web.py:923 msgid "Category list" msgstr "Liste des catégories" -#: cps/web.py:900 +#: cps/web.py:933 #, python-format msgid "Category: %(name)s" msgstr "Catégorie : %(name)s" -#: cps/web.py:956 +#: cps/web.py:992 msgid "Statistics" msgstr "Statistiques" -#: cps/web.py:965 -msgid "Server restarts" +#: cps/web.py:1013 +msgid "Performing Restart, please reload page" +msgstr "" + +#: cps/web.py:1015 +msgid "Performing shutdown of server, please close window" +msgstr "" + +#: cps/web.py:1091 cps/web.py:1104 +msgid "search" msgstr "" -#: cps/web.py:1102 cps/web.py:1109 cps/web.py:1116 cps/web.py:1123 +#: cps/web.py:1155 cps/web.py:1162 cps/web.py:1169 cps/web.py:1176 msgid "Read a Book" msgstr "Lire un livre" -#: cps/web.py:1172 cps/web.py:1510 +#: cps/web.py:1227 cps/web.py:1649 msgid "Please fill out all fields!" msgstr "SVP, complétez tous les champs !" -#: cps/web.py:1188 +#: cps/web.py:1228 cps/web.py:1244 cps/web.py:1249 cps/web.py:1251 +msgid "register" +msgstr "S'enregistrer" + +#: cps/web.py:1243 msgid "An unknown error occured. Please try again later." msgstr "Une erreur a eu lieu. Merci de réessayez plus tard." -#: cps/web.py:1193 +#: cps/web.py:1248 msgid "This username or email address is already in use." msgstr "Ce nom d'utilisateur ou cette adresse de courriel est déjà utilisée." -#: cps/web.py:1196 -msgid "register" -msgstr "S'enregistrer" - -#: cps/web.py:1212 +#: cps/web.py:1266 #, python-format msgid "you are now logged in as: '%(nickname)s'" msgstr "Vous êtes maintenant connecté sous : '%(nickname)s'" -#: cps/web.py:1216 +#: cps/web.py:1270 msgid "Wrong Username or Password" msgstr "Mauvais nom d'utilisateur ou mot de passe" -#: cps/web.py:1218 +#: cps/web.py:1272 msgid "login" msgstr "Connexion" -#: cps/web.py:1235 +#: cps/web.py:1289 msgid "Please configure the SMTP mail settings first..." msgstr "Veillez configurer les paramètres smtp d'abord..." -#: cps/web.py:1239 +#: cps/web.py:1293 #, python-format msgid "Book successfully send to %(kindlemail)s" msgstr "Livres envoyés à %(kindlemail)s avec succès" -#: cps/web.py:1243 +#: cps/web.py:1297 #, python-format msgid "There was an error sending this book: %(res)s" msgstr "Il y a eu une erreur en envoyant ce livre : %(res)s" -#: cps/web.py:1245 +#: cps/web.py:1299 msgid "Please configure your kindle email address first..." msgstr "Veuillez configurer votre adresse kindle d'abord..." -#: cps/web.py:1265 +#: cps/web.py:1319 #, python-format msgid "Book has been added to shelf: %(sname)s" msgstr "Le livre a bien été ajouté à l'étagère : %(sname)s" -#: cps/web.py:1286 +#: cps/web.py:1340 #, python-format msgid "Book has been removed from shelf: %(sname)s" msgstr "Le livre a été supprimé de l'étagère %(sname)s" -#: cps/web.py:1304 cps/web.py:1325 +#: cps/web.py:1359 cps/web.py:1383 #, python-format msgid "A shelf with the name '%(title)s' already exists." msgstr "Une étagère de ce nom '%(title)s' existe déjà." -#: cps/web.py:1309 +#: cps/web.py:1364 #, python-format msgid "Shelf %(title)s created" msgstr "Étagère %(title)s créée" -#: cps/web.py:1311 cps/web.py:1336 +#: cps/web.py:1366 cps/web.py:1394 msgid "There was an error" msgstr "Il y a eu une erreur" -#: cps/web.py:1312 cps/web.py:1314 +#: cps/web.py:1367 cps/web.py:1369 msgid "create a shelf" msgstr "Créer une étagère" -#: cps/web.py:1334 +#: cps/web.py:1392 #, python-format msgid "Shelf %(title)s changed" msgstr "" -#: cps/web.py:1337 cps/web.py:1339 +#: cps/web.py:1395 cps/web.py:1397 msgid "Edit a shelf" msgstr "" -#: cps/web.py:1360 +#: cps/web.py:1415 #, python-format msgid "successfully deleted shelf %(name)s" msgstr "L'étagère %(name)s a été supprimé avec succès" -#: cps/web.py:1381 +#: cps/web.py:1437 #, python-format msgid "Shelf: '%(name)s'" msgstr "Étagère : '%(name)s'" -#: cps/web.py:1409 +#: cps/web.py:1468 #, python-format msgid "Change order of Shelf: '%(name)s'" msgstr "" -#: cps/web.py:1469 +#: cps/web.py:1528 msgid "Found an existing account for this email address." msgstr "Un compte avec cette adresse de courriel existe déjà." -#: cps/web.py:1471 cps/web.py:1474 +#: cps/web.py:1530 cps/web.py:1534 #, python-format msgid "%(name)s's profile" msgstr "Profil de %(name)s" -#: cps/web.py:1472 +#: cps/web.py:1531 msgid "Profile updated" msgstr "Profil mis à jour" -#: cps/web.py:1483 cps/web.py:1491 +#: cps/web.py:1544 msgid "Admin page" msgstr "" -#: cps/templates/admin.html:33 cps/web.py:1511 +#: cps/web.py:1604 +msgid "Calibre-web configuration updated" +msgstr "" + +#: cps/web.py:1611 cps/web.py:1617 cps/web.py:1630 +msgid "Basic Configuration" +msgstr "" + +#: cps/web.py:1615 +msgid "DB location is not valid, please enter correct path" +msgstr "" + +#: cps/templates/admin.html:33 cps/web.py:1651 cps/web.py:1693 msgid "Add new user" msgstr "Ajouter un nouvel utilisateur" -#: cps/web.py:1544 +#: cps/web.py:1687 #, python-format msgid "User '%(user)s' created" msgstr "Utilisateur '%(user)s' créé" -#: cps/web.py:1548 +#: cps/web.py:1691 msgid "Found an existing account for this email address or nickname." msgstr "Un compte avec cette adresse de courriel ou ce surnom existe déjà." -#: cps/web.py:1568 +#: cps/web.py:1711 msgid "Mail settings updated" msgstr "Paramètres de courriel mis à jour" -#: cps/web.py:1574 +#: cps/web.py:1717 #, python-format msgid "Test E-Mail successfully send to %(kindlemail)s" msgstr "" -#: cps/web.py:1577 +#: cps/web.py:1720 #, python-format msgid "There was an error sending the Test E-Mail: %(res)s" msgstr "" -#: cps/web.py:1578 +#: cps/web.py:1721 msgid "Edit mail settings" msgstr "Éditer les paramètres de courriel" -#: cps/web.py:1606 +#: cps/web.py:1749 #, python-format msgid "User '%(nick)s' deleted" msgstr "Utilisateur '%(nick)s' supprimé" -#: cps/web.py:1661 +#: cps/web.py:1825 #, python-format msgid "User '%(nick)s' updated" msgstr "Utilisateur '%(nick)s' mis à jour" -#: cps/web.py:1664 +#: cps/web.py:1828 msgid "An unknown error occured." msgstr "Oups ! Une erreur inconnue a eu lieu." -#: cps/web.py:1666 +#: cps/web.py:1831 #, python-format msgid "Edit User %(nick)s" msgstr "Éditer l'utilisateur %(nick)s" -#: cps/web.py:1904 +#: cps/web.py:2036 cps/web.py:2039 cps/web.py:2113 +msgid "edit metadata" +msgstr "" + +#: cps/web.py:2071 #, python-format msgid "Failed to create path %s (Permission denied)." msgstr "Impossible de créer le chemin %s (permission refusée)" -#: cps/web.py:1909 +#: cps/web.py:2076 #, python-format msgid "Failed to store file %s (Permission denied)." msgstr "Impossible d'enregistrer le fichier %s (permission refusée)" -#: cps/web.py:1914 +#: cps/web.py:2081 #, python-format msgid "Failed to delete file %s (Permission denied)." msgstr "Impossible de supprimer le fichier %s (permission refusée)" @@ -341,7 +365,7 @@ msgstr "DLS" msgid "Admin" msgstr "Administration" -#: cps/templates/admin.html:12 cps/templates/detail.html:114 +#: cps/templates/admin.html:12 cps/templates/detail.html:116 msgid "Download" msgstr "Télécharger" @@ -389,15 +413,15 @@ msgstr "Expéditeur des courriels" msgid "Change SMTP settings" msgstr "Changer les paramètre smtp" -#: cps/templates/admin.html:56 +#: cps/templates/admin.html:56 cps/templates/admin.html:76 msgid "Configuration" msgstr "" #: cps/templates/admin.html:59 -msgid "Log File" +msgid "Calibre DB dir" msgstr "" -#: cps/templates/admin.html:60 +#: cps/templates/admin.html:60 cps/templates/config_edit.html:32 msgid "Log Level" msgstr "" @@ -405,7 +429,7 @@ msgstr "" msgid "Port" msgstr "" -#: cps/templates/admin.html:62 +#: cps/templates/admin.html:62 cps/templates/config_edit.html:19 msgid "Books per page" msgstr "" @@ -421,102 +445,156 @@ msgstr "" msgid "Anonymous browsing" msgstr "" -#: cps/templates/admin.html:76 +#: cps/templates/admin.html:77 msgid "Administration" msgstr "" -#: cps/templates/admin.html:78 +#: cps/templates/admin.html:79 msgid "Restart Calibre-web" msgstr "" -#: cps/templates/detail.html:38 -msgid "Book" -msgstr "Livre" - -#: cps/templates/detail.html:38 -msgid "of" +#: cps/templates/admin.html:80 +msgid "Stop Calibre-web" msgstr "" -#: cps/templates/detail.html:44 -msgid "language" -msgstr "Langue" - -#: cps/templates/detail.html:103 -msgid "Description:" -msgstr "Description :" +#: cps/templates/admin.html:91 +msgid "Do you really want to restart Calibre-web?" +msgstr "" -#: cps/templates/detail.html:131 -msgid "Read in browser" -msgstr "Lire dans le navigateur" +#: cps/templates/admin.html:92 cps/templates/admin.html:107 +msgid "Ok" +msgstr "" -#: cps/templates/detail.html:151 -msgid "Add to shelf" -msgstr "Ajouter à l'étagère" +#: cps/templates/admin.html:93 cps/templates/admin.html:108 +#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:54 +#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17 +#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:107 +msgid "Back" +msgstr "Retour" -#: cps/templates/detail.html:191 -msgid "Edit metadata" -msgstr "Éditer les métadonnées" +#: cps/templates/admin.html:106 +msgid "Do you really want to stop Calibre-web?" +msgstr "" -#: cps/templates/edit_book.html:14 cps/templates/search_form.html:6 +#: cps/templates/book_edit.html:16 cps/templates/search_form.html:6 msgid "Book Title" msgstr "Titre du livre" -#: cps/templates/edit_book.html:18 cps/templates/search_form.html:10 +#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10 msgid "Author" msgstr "Auteur" -#: cps/templates/edit_book.html:22 +#: cps/templates/book_edit.html:24 msgid "Description" msgstr "Description" -#: cps/templates/edit_book.html:26 cps/templates/search_form.html:13 +#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13 msgid "Tags" msgstr "Étiquette" -#: cps/templates/edit_book.html:31 cps/templates/layout.html:133 +#: cps/templates/book_edit.html:33 cps/templates/layout.html:133 #: cps/templates/search_form.html:33 msgid "Series" msgstr "Séries" -#: cps/templates/edit_book.html:35 +#: cps/templates/book_edit.html:37 msgid "Series id" msgstr "Id de série" -#: cps/templates/edit_book.html:39 +#: cps/templates/book_edit.html:41 msgid "Rating" msgstr "Évaluation" -#: cps/templates/edit_book.html:43 +#: cps/templates/book_edit.html:45 msgid "Cover URL (jpg)" msgstr "Adresse de la couverture (jpg)" -#: cps/templates/edit_book.html:48 cps/templates/user_edit.html:27 +#: cps/templates/book_edit.html:50 cps/templates/user_edit.html:27 msgid "Language" msgstr "Langue" -#: cps/templates/edit_book.html:59 +#: cps/templates/book_edit.html:61 msgid "Yes" msgstr "Oui" -#: cps/templates/edit_book.html:60 +#: cps/templates/book_edit.html:62 msgid "No" msgstr "Non" -#: cps/templates/edit_book.html:102 +#: cps/templates/book_edit.html:104 msgid "view book after edit" msgstr "Voir le livre après l'édition" -#: cps/templates/edit_book.html:105 cps/templates/login.html:19 -#: cps/templates/search_form.html:75 cps/templates/shelf_edit.html:15 -#: cps/templates/user_edit.html:97 +#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:52 +#: cps/templates/login.html:19 cps/templates/search_form.html:75 +#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:105 msgid "Submit" msgstr "Soumettre" -#: cps/templates/edit_book.html:106 cps/templates/email_edit.html:36 -#: cps/templates/shelf_edit.html:17 cps/templates/shelf_order.html:12 -#: cps/templates/user_edit.html:99 -msgid "Back" -msgstr "Retour" +#: cps/templates/config_edit.html:7 +msgid "Location of Calibre database" +msgstr "" + +#: cps/templates/config_edit.html:11 +msgid "Server Port" +msgstr "" + +#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7 +msgid "Title" +msgstr "Titre" + +#: cps/templates/config_edit.html:23 +msgid "No. of random books to show" +msgstr "" + +#: cps/templates/config_edit.html:28 +msgid "Regular expression for title sorting" +msgstr "" + +#: cps/templates/config_edit.html:42 +msgid "Enable uploading" +msgstr "" + +#: cps/templates/config_edit.html:46 +msgid "Enable anonymous browsing" +msgstr "" + +#: cps/templates/config_edit.html:50 +msgid "Enable public registration" +msgstr "" + +#: cps/templates/config_edit.html:57 cps/templates/layout.html:91 +#: cps/templates/login.html:4 +msgid "Login" +msgstr "Connexion" + +#: cps/templates/detail.html:40 +msgid "Book" +msgstr "Livre" + +#: cps/templates/detail.html:40 +msgid "of" +msgstr "" + +#: cps/templates/detail.html:46 +msgid "language" +msgstr "Langue" + +#: cps/templates/detail.html:105 +msgid "Description:" +msgstr "Description :" + +#: cps/templates/detail.html:133 +msgid "Read in browser" +msgstr "Lire dans le navigateur" + +#: cps/templates/detail.html:153 +msgid "Add to shelf" +msgstr "Ajouter à l'étagère" + +#: cps/templates/detail.html:193 +msgid "Edit metadata" +msgstr "Éditer les métadonnées" #: cps/templates/email_edit.html:11 msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)" @@ -618,10 +696,6 @@ msgstr "Recherche avancée" msgid "Logout" msgstr "Déconnexion" -#: cps/templates/layout.html:91 cps/templates/login.html:4 -msgid "Login" -msgstr "Connexion" - #: cps/templates/layout.html:92 cps/templates/register.html:18 msgid "Register" msgstr "S'enregistrer" @@ -740,10 +814,6 @@ msgstr "" msgid "Change order" msgstr "" -#: cps/templates/shelf_edit.html:7 -msgid "Title" -msgstr "Titre" - #: cps/templates/shelf_edit.html:12 msgid "should the shelf be public?" msgstr "Cette étagère doit-elle être publique ?" @@ -808,31 +878,39 @@ msgstr "Montrer la sélection des séries" msgid "Show category selection" msgstr "Montrer la sélection des catégories" -#: cps/templates/user_edit.html:68 +#: cps/templates/user_edit.html:65 +msgid "Show author selection" +msgstr "" + +#: cps/templates/user_edit.html:69 +msgid "Show random books in detail view" +msgstr "" + +#: cps/templates/user_edit.html:76 msgid "Admin user" msgstr "Utilisateur admin" -#: cps/templates/user_edit.html:73 +#: cps/templates/user_edit.html:81 msgid "Allow Downloads" msgstr "Permettre les téléchargements" -#: cps/templates/user_edit.html:77 +#: cps/templates/user_edit.html:85 msgid "Allow Uploads" msgstr "Permettre les téléversements" -#: cps/templates/user_edit.html:81 +#: cps/templates/user_edit.html:89 msgid "Allow Edit" msgstr "Permettre l'édition" -#: cps/templates/user_edit.html:86 +#: cps/templates/user_edit.html:94 msgid "Allow Changing Password" msgstr "Permettre le changement de mot de passe" -#: cps/templates/user_edit.html:93 +#: cps/templates/user_edit.html:101 msgid "Delete this user" msgstr "Supprimer cet utilisateur" -#: cps/templates/user_edit.html:104 +#: cps/templates/user_edit.html:112 msgid "Recent Downloads" msgstr "Téléchargements récents" diff --git a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po index b392864c..02aef35c 100644 --- a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po +++ b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: Calibre-web\n" "Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n" -"POT-Creation-Date: 2017-01-21 11:44+0100\n" +"POT-Creation-Date: 2017-01-28 20:35+0100\n" "PO-Revision-Date: 2017-01-06 17:00+0800\n" "Last-Translator: dalin \n" "Language: zh_Hans_CN\n" @@ -26,284 +26,308 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:948 +#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:982 msgid "not installed" msgstr "未安装" -#: cps/helper.py:98 +#: cps/helper.py:136 +#, python-format +msgid "Failed to send mail: %s" +msgstr "发送邮件失败: %s" + +#: cps/helper.py:143 msgid "Calibre-web test email" msgstr "Calibre-web 测试邮件" -#: cps/helper.py:99 cps/helper.py:155 +#: cps/helper.py:144 cps/helper.py:154 msgid "This email has been sent via calibre web." msgstr "此邮件由calibre web发送" -#: cps/helper.py:136 cps/helper.py:225 -#, python-format -msgid "Failed to send mail: %s" -msgstr "发送邮件失败: %s" - -#: cps/helper.py:154 cps/templates/detail.html:127 +#: cps/helper.py:153 cps/templates/detail.html:129 msgid "Send to Kindle" msgstr "发送到Kindle" -#: cps/helper.py:177 cps/helper.py:192 +#: cps/helper.py:171 cps/helper.py:186 msgid "Could not find any formats suitable for sending by email" msgstr "无法找到适合邮件发送的格式" -#: cps/helper.py:186 +#: cps/helper.py:180 msgid "Could not convert epub to mobi" msgstr "无法转换epub到mobi" -#: cps/helper.py:245 +#: cps/helper.py:206 msgid "The requested file could not be read. Maybe wrong permissions?" msgstr "无法读取所请求的文件。可能是错误权限不对?" -#: cps/ub.py:259 +#: cps/ub.py:380 msgid "Guest" msgstr "" -#: cps/web.py:742 +#: cps/web.py:774 msgid "Latest Books" msgstr "最新书籍" -#: cps/web.py:767 +#: cps/web.py:799 msgid "Hot Books (most downloaded)" msgstr "热门书籍(最多下载)" -#: cps/templates/index.xml:29 cps/web.py:775 +#: cps/templates/index.xml:29 cps/web.py:808 msgid "Random Books" msgstr "随机书籍" -#: cps/web.py:788 +#: cps/web.py:821 msgid "Author list" msgstr "作者列表" -#: cps/web.py:805 +#: cps/web.py:838 #, python-format msgid "Author: %(nam)s" msgstr "作者: %(nam)s" -#: cps/templates/index.xml:50 cps/web.py:818 +#: cps/templates/index.xml:50 cps/web.py:851 msgid "Series list" msgstr "丛书列表" -#: cps/web.py:829 +#: cps/web.py:862 #, python-format msgid "Series: %(serie)s" msgstr "丛书: %(serie)s" -#: cps/web.py:831 cps/web.py:927 cps/web.py:1126 cps/web.py:1874 +#: cps/web.py:864 cps/web.py:961 cps/web.py:1179 cps/web.py:2041 msgid "Error opening eBook. File does not exist or file is not accessible:" msgstr "无法打开电子书。 文件不存在或者文件不可访问:" -#: cps/web.py:862 +#: cps/web.py:895 msgid "Available languages" msgstr "可用语言" -#: cps/web.py:877 +#: cps/web.py:910 #, python-format msgid "Language: %(name)s" msgstr "语言: %(name)s" -#: cps/templates/index.xml:43 cps/web.py:890 +#: cps/templates/index.xml:43 cps/web.py:923 msgid "Category list" msgstr "分类列表" -#: cps/web.py:900 +#: cps/web.py:933 #, python-format msgid "Category: %(name)s" msgstr "分类: %(name)s" -#: cps/web.py:956 +#: cps/web.py:992 msgid "Statistics" msgstr "统计" -#: cps/web.py:965 -msgid "Server restarts" -msgstr "重启服务器" +#: cps/web.py:1013 +msgid "Performing Restart, please reload page" +msgstr "" -#: cps/web.py:1102 cps/web.py:1109 cps/web.py:1116 cps/web.py:1123 +#: cps/web.py:1015 +msgid "Performing shutdown of server, please close window" +msgstr "" + +#: cps/web.py:1091 cps/web.py:1104 +msgid "search" +msgstr "" + +#: cps/web.py:1155 cps/web.py:1162 cps/web.py:1169 cps/web.py:1176 msgid "Read a Book" msgstr "阅读一本书" -#: cps/web.py:1172 cps/web.py:1510 +#: cps/web.py:1227 cps/web.py:1649 msgid "Please fill out all fields!" msgstr "请填写所有字段" -#: cps/web.py:1188 +#: cps/web.py:1228 cps/web.py:1244 cps/web.py:1249 cps/web.py:1251 +msgid "register" +msgstr "注册" + +#: cps/web.py:1243 msgid "An unknown error occured. Please try again later." msgstr "发生一个未知错误。请稍后再试。" -#: cps/web.py:1193 +#: cps/web.py:1248 msgid "This username or email address is already in use." msgstr "此用户名或邮箱已被使用。" -#: cps/web.py:1196 -msgid "register" -msgstr "注册" - -#: cps/web.py:1212 +#: cps/web.py:1266 #, python-format msgid "you are now logged in as: '%(nickname)s'" msgstr "您现在已以'%(nickname)s'身份登录" -#: cps/web.py:1216 +#: cps/web.py:1270 msgid "Wrong Username or Password" msgstr "用户名或密码错误" -#: cps/web.py:1218 +#: cps/web.py:1272 msgid "login" msgstr "登录" -#: cps/web.py:1235 +#: cps/web.py:1289 msgid "Please configure the SMTP mail settings first..." msgstr "请先配置SMTP邮箱..." -#: cps/web.py:1239 +#: cps/web.py:1293 #, python-format msgid "Book successfully send to %(kindlemail)s" msgstr "此书已被成功发给 %(kindlemail)s" -#: cps/web.py:1243 +#: cps/web.py:1297 #, python-format msgid "There was an error sending this book: %(res)s" msgstr "发送这本书的时候出现错误: %(res)s" -#: cps/web.py:1245 +#: cps/web.py:1299 msgid "Please configure your kindle email address first..." msgstr "请先配置您的kindle电子邮箱地址..." -#: cps/web.py:1265 +#: cps/web.py:1319 #, python-format msgid "Book has been added to shelf: %(sname)s" msgstr "此书已被添加到书架: %(sname)s" -#: cps/web.py:1286 +#: cps/web.py:1340 #, python-format msgid "Book has been removed from shelf: %(sname)s" msgstr "此书已从书架 %(sname)s 中删除" -#: cps/web.py:1304 cps/web.py:1325 +#: cps/web.py:1359 cps/web.py:1383 #, python-format msgid "A shelf with the name '%(title)s' already exists." msgstr "已存在书架 '%(title)s'。" -#: cps/web.py:1309 +#: cps/web.py:1364 #, python-format msgid "Shelf %(title)s created" msgstr "书架 %(title)s 已被创建" -#: cps/web.py:1311 cps/web.py:1336 +#: cps/web.py:1366 cps/web.py:1394 msgid "There was an error" msgstr "发生错误" -#: cps/web.py:1312 cps/web.py:1314 +#: cps/web.py:1367 cps/web.py:1369 msgid "create a shelf" msgstr "创建书架" -#: cps/web.py:1334 +#: cps/web.py:1392 #, python-format msgid "Shelf %(title)s changed" msgstr "书架 %(title)s 已被修改" -#: cps/web.py:1337 cps/web.py:1339 +#: cps/web.py:1395 cps/web.py:1397 msgid "Edit a shelf" msgstr "编辑书架" -#: cps/web.py:1360 +#: cps/web.py:1415 #, python-format msgid "successfully deleted shelf %(name)s" msgstr "成功删除书架 %(name)s" -#: cps/web.py:1381 +#: cps/web.py:1437 #, python-format msgid "Shelf: '%(name)s'" msgstr "书架: '%(name)s'" -#: cps/web.py:1409 +#: cps/web.py:1468 #, python-format msgid "Change order of Shelf: '%(name)s'" msgstr "修改书架 '%(name)s' 顺序" -#: cps/web.py:1469 +#: cps/web.py:1528 msgid "Found an existing account for this email address." msgstr "找到已使用此邮箱的账号。" -#: cps/web.py:1471 cps/web.py:1474 +#: cps/web.py:1530 cps/web.py:1534 #, python-format msgid "%(name)s's profile" msgstr "%(name)s 的资料" -#: cps/web.py:1472 +#: cps/web.py:1531 msgid "Profile updated" msgstr "资料已更新" -#: cps/web.py:1483 cps/web.py:1491 +#: cps/web.py:1544 msgid "Admin page" msgstr "管理页" -#: cps/templates/admin.html:33 cps/web.py:1511 +#: cps/web.py:1604 +msgid "Calibre-web configuration updated" +msgstr "" + +#: cps/web.py:1611 cps/web.py:1617 cps/web.py:1630 +msgid "Basic Configuration" +msgstr "" + +#: cps/web.py:1615 +msgid "DB location is not valid, please enter correct path" +msgstr "" + +#: cps/templates/admin.html:33 cps/web.py:1651 cps/web.py:1693 msgid "Add new user" msgstr "添加新用户" -#: cps/web.py:1544 +#: cps/web.py:1687 #, python-format msgid "User '%(user)s' created" msgstr "用户 '%(user)s' 已被创建" -#: cps/web.py:1548 +#: cps/web.py:1691 msgid "Found an existing account for this email address or nickname." msgstr "已找到使用此邮箱或昵称的账号。" -#: cps/web.py:1568 +#: cps/web.py:1711 msgid "Mail settings updated" msgstr "邮箱设置已更新" -#: cps/web.py:1574 +#: cps/web.py:1717 #, python-format msgid "Test E-Mail successfully send to %(kindlemail)s" msgstr "测试邮件已成功发送到 %(kindlemail)s" -#: cps/web.py:1577 +#: cps/web.py:1720 #, python-format msgid "There was an error sending the Test E-Mail: %(res)s" msgstr "发送测试邮件时发生错误: %(res)s" -#: cps/web.py:1578 +#: cps/web.py:1721 msgid "Edit mail settings" msgstr "编辑邮箱设置" -#: cps/web.py:1606 +#: cps/web.py:1749 #, python-format msgid "User '%(nick)s' deleted" msgstr "用户 '%(nick)s' 已被删除" -#: cps/web.py:1661 +#: cps/web.py:1825 #, python-format msgid "User '%(nick)s' updated" msgstr "用户 '%(nick)s' 已被更新" -#: cps/web.py:1664 +#: cps/web.py:1828 msgid "An unknown error occured." msgstr "发生未知错误。" -#: cps/web.py:1666 +#: cps/web.py:1831 #, python-format msgid "Edit User %(nick)s" msgstr "编辑用户 %(nick)s" -#: cps/web.py:1904 +#: cps/web.py:2036 cps/web.py:2039 cps/web.py:2113 +msgid "edit metadata" +msgstr "" + +#: cps/web.py:2071 #, python-format msgid "Failed to create path %s (Permission denied)." msgstr "创建路径 %s 失败(权限拒绝)。" -#: cps/web.py:1909 +#: cps/web.py:2076 #, python-format msgid "Failed to store file %s (Permission denied)." msgstr "存储文件 %s 失败(权限拒绝)。" -#: cps/web.py:1914 +#: cps/web.py:2081 #, python-format msgid "Failed to delete file %s (Permission denied)." msgstr "删除文件 %s 失败(权限拒绝)。" @@ -332,7 +356,7 @@ msgstr "" msgid "Admin" msgstr "管理" -#: cps/templates/admin.html:12 cps/templates/detail.html:114 +#: cps/templates/admin.html:12 cps/templates/detail.html:116 msgid "Download" msgstr "下载" @@ -380,15 +404,15 @@ msgstr "来自邮箱" msgid "Change SMTP settings" msgstr "修改SMTP设置" -#: cps/templates/admin.html:56 +#: cps/templates/admin.html:56 cps/templates/admin.html:76 msgid "Configuration" msgstr "配置" #: cps/templates/admin.html:59 -msgid "Log File" -msgstr "日志文件" +msgid "Calibre DB dir" +msgstr "" -#: cps/templates/admin.html:60 +#: cps/templates/admin.html:60 cps/templates/config_edit.html:32 msgid "Log Level" msgstr "日志级别" @@ -396,7 +420,7 @@ msgstr "日志级别" msgid "Port" msgstr "端口" -#: cps/templates/admin.html:62 +#: cps/templates/admin.html:62 cps/templates/config_edit.html:19 msgid "Books per page" msgstr "每页书籍数" @@ -412,102 +436,156 @@ msgstr "开放注册" msgid "Anonymous browsing" msgstr "匿名浏览" -#: cps/templates/admin.html:76 +#: cps/templates/admin.html:77 msgid "Administration" msgstr "管理" -#: cps/templates/admin.html:78 +#: cps/templates/admin.html:79 msgid "Restart Calibre-web" msgstr "重启 Calibre-web" -#: cps/templates/detail.html:38 -msgid "Book" +#: cps/templates/admin.html:80 +msgid "Stop Calibre-web" msgstr "" -#: cps/templates/detail.html:38 -msgid "of" +#: cps/templates/admin.html:91 +msgid "Do you really want to restart Calibre-web?" msgstr "" -#: cps/templates/detail.html:44 -msgid "language" -msgstr "语言" - -#: cps/templates/detail.html:103 -msgid "Description:" -msgstr "简介:" - -#: cps/templates/detail.html:131 -msgid "Read in browser" -msgstr "在浏览器中阅读" +#: cps/templates/admin.html:92 cps/templates/admin.html:107 +msgid "Ok" +msgstr "" -#: cps/templates/detail.html:151 -msgid "Add to shelf" -msgstr "添加到书架" +#: cps/templates/admin.html:93 cps/templates/admin.html:108 +#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:54 +#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17 +#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:107 +msgid "Back" +msgstr "后退" -#: cps/templates/detail.html:191 -msgid "Edit metadata" -msgstr "编辑元数据" +#: cps/templates/admin.html:106 +msgid "Do you really want to stop Calibre-web?" +msgstr "" -#: cps/templates/edit_book.html:14 cps/templates/search_form.html:6 +#: cps/templates/book_edit.html:16 cps/templates/search_form.html:6 msgid "Book Title" msgstr "书名" -#: cps/templates/edit_book.html:18 cps/templates/search_form.html:10 +#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10 msgid "Author" msgstr "作者" -#: cps/templates/edit_book.html:22 +#: cps/templates/book_edit.html:24 msgid "Description" msgstr "简介" -#: cps/templates/edit_book.html:26 cps/templates/search_form.html:13 +#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13 msgid "Tags" msgstr "标签" -#: cps/templates/edit_book.html:31 cps/templates/layout.html:133 +#: cps/templates/book_edit.html:33 cps/templates/layout.html:133 #: cps/templates/search_form.html:33 msgid "Series" msgstr "丛书" -#: cps/templates/edit_book.html:35 +#: cps/templates/book_edit.html:37 msgid "Series id" msgstr "丛书ID" -#: cps/templates/edit_book.html:39 +#: cps/templates/book_edit.html:41 msgid "Rating" msgstr "评分" -#: cps/templates/edit_book.html:43 +#: cps/templates/book_edit.html:45 msgid "Cover URL (jpg)" msgstr "封面URL (jpg)" -#: cps/templates/edit_book.html:48 cps/templates/user_edit.html:27 +#: cps/templates/book_edit.html:50 cps/templates/user_edit.html:27 msgid "Language" msgstr "语言" -#: cps/templates/edit_book.html:59 +#: cps/templates/book_edit.html:61 msgid "Yes" msgstr "确认" -#: cps/templates/edit_book.html:60 +#: cps/templates/book_edit.html:62 msgid "No" msgstr "" -#: cps/templates/edit_book.html:102 +#: cps/templates/book_edit.html:104 msgid "view book after edit" msgstr "编辑后查看书籍" -#: cps/templates/edit_book.html:105 cps/templates/login.html:19 -#: cps/templates/search_form.html:75 cps/templates/shelf_edit.html:15 -#: cps/templates/user_edit.html:97 +#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:52 +#: cps/templates/login.html:19 cps/templates/search_form.html:75 +#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:105 msgid "Submit" msgstr "提交" -#: cps/templates/edit_book.html:106 cps/templates/email_edit.html:36 -#: cps/templates/shelf_edit.html:17 cps/templates/shelf_order.html:12 -#: cps/templates/user_edit.html:99 -msgid "Back" -msgstr "后退" +#: cps/templates/config_edit.html:7 +msgid "Location of Calibre database" +msgstr "" + +#: cps/templates/config_edit.html:11 +msgid "Server Port" +msgstr "" + +#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7 +msgid "Title" +msgstr "" + +#: cps/templates/config_edit.html:23 +msgid "No. of random books to show" +msgstr "" + +#: cps/templates/config_edit.html:28 +msgid "Regular expression for title sorting" +msgstr "" + +#: cps/templates/config_edit.html:42 +msgid "Enable uploading" +msgstr "" + +#: cps/templates/config_edit.html:46 +msgid "Enable anonymous browsing" +msgstr "" + +#: cps/templates/config_edit.html:50 +msgid "Enable public registration" +msgstr "" + +#: cps/templates/config_edit.html:57 cps/templates/layout.html:91 +#: cps/templates/login.html:4 +msgid "Login" +msgstr "登录" + +#: cps/templates/detail.html:40 +msgid "Book" +msgstr "" + +#: cps/templates/detail.html:40 +msgid "of" +msgstr "" + +#: cps/templates/detail.html:46 +msgid "language" +msgstr "语言" + +#: cps/templates/detail.html:105 +msgid "Description:" +msgstr "简介:" + +#: cps/templates/detail.html:133 +msgid "Read in browser" +msgstr "在浏览器中阅读" + +#: cps/templates/detail.html:153 +msgid "Add to shelf" +msgstr "添加到书架" + +#: cps/templates/detail.html:193 +msgid "Edit metadata" +msgstr "编辑元数据" #: cps/templates/email_edit.html:11 msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)" @@ -609,10 +687,6 @@ msgstr "高级搜索" msgid "Logout" msgstr "注销" -#: cps/templates/layout.html:91 cps/templates/login.html:4 -msgid "Login" -msgstr "登录" - #: cps/templates/layout.html:92 cps/templates/register.html:18 msgid "Register" msgstr "注册" @@ -731,10 +805,6 @@ msgstr "编辑书架名" msgid "Change order" msgstr "修改顺序" -#: cps/templates/shelf_edit.html:7 -msgid "Title" -msgstr "" - #: cps/templates/shelf_edit.html:12 msgid "should the shelf be public?" msgstr "要公开此书架吗?" @@ -799,31 +869,39 @@ msgstr "显示丛书选择" msgid "Show category selection" msgstr "显示分类选择" -#: cps/templates/user_edit.html:68 +#: cps/templates/user_edit.html:65 +msgid "Show author selection" +msgstr "" + +#: cps/templates/user_edit.html:69 +msgid "Show random books in detail view" +msgstr "" + +#: cps/templates/user_edit.html:76 msgid "Admin user" msgstr "管理用户" -#: cps/templates/user_edit.html:73 +#: cps/templates/user_edit.html:81 msgid "Allow Downloads" msgstr "允许下载" -#: cps/templates/user_edit.html:77 +#: cps/templates/user_edit.html:85 msgid "Allow Uploads" msgstr "允许上传" -#: cps/templates/user_edit.html:81 +#: cps/templates/user_edit.html:89 msgid "Allow Edit" msgstr "允许编辑" -#: cps/templates/user_edit.html:86 +#: cps/templates/user_edit.html:94 msgid "Allow Changing Password" msgstr "允许修改密码" -#: cps/templates/user_edit.html:93 +#: cps/templates/user_edit.html:101 msgid "Delete this user" msgstr "删除此用户" -#: cps/templates/user_edit.html:104 +#: cps/templates/user_edit.html:112 msgid "Recent Downloads" msgstr "最近下载" diff --git a/messages.pot b/messages.pot index 97f67c0f..93c2d81a 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2017-01-18 19:12+0100\n" +"POT-Creation-Date: 2017-01-28 20:35+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,284 +17,308 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:948 +#: cps/book_formats.py:109 cps/book_formats.py:113 cps/web.py:982 msgid "not installed" msgstr "" -#: cps/helper.py:98 -msgid "Calibre-web test email" +#: cps/helper.py:136 +#, python-format +msgid "Failed to send mail: %s" msgstr "" -#: cps/helper.py:99 cps/helper.py:155 -msgid "This email has been sent via calibre web." +#: cps/helper.py:143 +msgid "Calibre-web test email" msgstr "" -#: cps/helper.py:136 cps/helper.py:225 -#, python-format -msgid "Failed to send mail: %s" +#: cps/helper.py:144 cps/helper.py:154 +msgid "This email has been sent via calibre web." msgstr "" -#: cps/helper.py:154 cps/templates/detail.html:127 +#: cps/helper.py:153 cps/templates/detail.html:129 msgid "Send to Kindle" msgstr "" -#: cps/helper.py:177 cps/helper.py:192 +#: cps/helper.py:171 cps/helper.py:186 msgid "Could not find any formats suitable for sending by email" msgstr "" -#: cps/helper.py:186 +#: cps/helper.py:180 msgid "Could not convert epub to mobi" msgstr "" -#: cps/helper.py:245 +#: cps/helper.py:206 msgid "The requested file could not be read. Maybe wrong permissions?" msgstr "" -#: cps/ub.py:259 +#: cps/ub.py:380 msgid "Guest" msgstr "" -#: cps/web.py:742 +#: cps/web.py:774 msgid "Latest Books" msgstr "" -#: cps/web.py:767 +#: cps/web.py:799 msgid "Hot Books (most downloaded)" msgstr "" -#: cps/templates/index.xml:29 cps/web.py:775 +#: cps/templates/index.xml:29 cps/web.py:808 msgid "Random Books" msgstr "" -#: cps/web.py:788 +#: cps/web.py:821 msgid "Author list" msgstr "" -#: cps/web.py:805 +#: cps/web.py:838 #, python-format msgid "Author: %(nam)s" msgstr "" -#: cps/templates/index.xml:50 cps/web.py:818 +#: cps/templates/index.xml:50 cps/web.py:851 msgid "Series list" msgstr "" -#: cps/web.py:829 +#: cps/web.py:862 #, python-format msgid "Series: %(serie)s" msgstr "" -#: cps/web.py:831 cps/web.py:927 cps/web.py:1126 cps/web.py:1874 +#: cps/web.py:864 cps/web.py:961 cps/web.py:1179 cps/web.py:2041 msgid "Error opening eBook. File does not exist or file is not accessible:" msgstr "" -#: cps/web.py:862 +#: cps/web.py:895 msgid "Available languages" msgstr "" -#: cps/web.py:877 +#: cps/web.py:910 #, python-format msgid "Language: %(name)s" msgstr "" -#: cps/templates/index.xml:43 cps/web.py:890 +#: cps/templates/index.xml:43 cps/web.py:923 msgid "Category list" msgstr "" -#: cps/web.py:900 +#: cps/web.py:933 #, python-format msgid "Category: %(name)s" msgstr "" -#: cps/web.py:956 +#: cps/web.py:992 msgid "Statistics" msgstr "" -#: cps/web.py:965 -msgid "Server restarts" +#: cps/web.py:1013 +msgid "Performing Restart, please reload page" +msgstr "" + +#: cps/web.py:1015 +msgid "Performing shutdown of server, please close window" msgstr "" -#: cps/web.py:1102 cps/web.py:1109 cps/web.py:1116 cps/web.py:1123 +#: cps/web.py:1091 cps/web.py:1104 +msgid "search" +msgstr "" + +#: cps/web.py:1155 cps/web.py:1162 cps/web.py:1169 cps/web.py:1176 msgid "Read a Book" msgstr "" -#: cps/web.py:1172 cps/web.py:1510 +#: cps/web.py:1227 cps/web.py:1649 msgid "Please fill out all fields!" msgstr "" -#: cps/web.py:1188 -msgid "An unknown error occured. Please try again later." +#: cps/web.py:1228 cps/web.py:1244 cps/web.py:1249 cps/web.py:1251 +msgid "register" msgstr "" -#: cps/web.py:1193 -msgid "This username or email address is already in use." +#: cps/web.py:1243 +msgid "An unknown error occured. Please try again later." msgstr "" -#: cps/web.py:1196 -msgid "register" +#: cps/web.py:1248 +msgid "This username or email address is already in use." msgstr "" -#: cps/web.py:1212 +#: cps/web.py:1266 #, python-format msgid "you are now logged in as: '%(nickname)s'" msgstr "" -#: cps/web.py:1216 +#: cps/web.py:1270 msgid "Wrong Username or Password" msgstr "" -#: cps/web.py:1218 +#: cps/web.py:1272 msgid "login" msgstr "" -#: cps/web.py:1235 +#: cps/web.py:1289 msgid "Please configure the SMTP mail settings first..." msgstr "" -#: cps/web.py:1239 +#: cps/web.py:1293 #, python-format msgid "Book successfully send to %(kindlemail)s" msgstr "" -#: cps/web.py:1243 +#: cps/web.py:1297 #, python-format msgid "There was an error sending this book: %(res)s" msgstr "" -#: cps/web.py:1245 +#: cps/web.py:1299 msgid "Please configure your kindle email address first..." msgstr "" -#: cps/web.py:1265 +#: cps/web.py:1319 #, python-format msgid "Book has been added to shelf: %(sname)s" msgstr "" -#: cps/web.py:1286 +#: cps/web.py:1340 #, python-format msgid "Book has been removed from shelf: %(sname)s" msgstr "" -#: cps/web.py:1304 cps/web.py:1325 +#: cps/web.py:1359 cps/web.py:1383 #, python-format msgid "A shelf with the name '%(title)s' already exists." msgstr "" -#: cps/web.py:1309 +#: cps/web.py:1364 #, python-format msgid "Shelf %(title)s created" msgstr "" -#: cps/web.py:1311 cps/web.py:1336 +#: cps/web.py:1366 cps/web.py:1394 msgid "There was an error" msgstr "" -#: cps/web.py:1312 cps/web.py:1314 +#: cps/web.py:1367 cps/web.py:1369 msgid "create a shelf" msgstr "" -#: cps/web.py:1334 +#: cps/web.py:1392 #, python-format msgid "Shelf %(title)s changed" msgstr "" -#: cps/web.py:1337 cps/web.py:1339 +#: cps/web.py:1395 cps/web.py:1397 msgid "Edit a shelf" msgstr "" -#: cps/web.py:1360 +#: cps/web.py:1415 #, python-format msgid "successfully deleted shelf %(name)s" msgstr "" -#: cps/web.py:1381 +#: cps/web.py:1437 #, python-format msgid "Shelf: '%(name)s'" msgstr "" -#: cps/web.py:1409 +#: cps/web.py:1468 #, python-format msgid "Change order of Shelf: '%(name)s'" msgstr "" -#: cps/web.py:1469 +#: cps/web.py:1528 msgid "Found an existing account for this email address." msgstr "" -#: cps/web.py:1471 cps/web.py:1474 +#: cps/web.py:1530 cps/web.py:1534 #, python-format msgid "%(name)s's profile" msgstr "" -#: cps/web.py:1472 +#: cps/web.py:1531 msgid "Profile updated" msgstr "" -#: cps/web.py:1483 cps/web.py:1491 +#: cps/web.py:1544 msgid "Admin page" msgstr "" -#: cps/templates/admin.html:33 cps/web.py:1511 +#: cps/web.py:1604 +msgid "Calibre-web configuration updated" +msgstr "" + +#: cps/web.py:1611 cps/web.py:1617 cps/web.py:1630 +msgid "Basic Configuration" +msgstr "" + +#: cps/web.py:1615 +msgid "DB location is not valid, please enter correct path" +msgstr "" + +#: cps/templates/admin.html:33 cps/web.py:1651 cps/web.py:1693 msgid "Add new user" msgstr "" -#: cps/web.py:1544 +#: cps/web.py:1687 #, python-format msgid "User '%(user)s' created" msgstr "" -#: cps/web.py:1548 +#: cps/web.py:1691 msgid "Found an existing account for this email address or nickname." msgstr "" -#: cps/web.py:1568 +#: cps/web.py:1711 msgid "Mail settings updated" msgstr "" -#: cps/web.py:1574 +#: cps/web.py:1717 #, python-format msgid "Test E-Mail successfully send to %(kindlemail)s" msgstr "" -#: cps/web.py:1577 +#: cps/web.py:1720 #, python-format msgid "There was an error sending the Test E-Mail: %(res)s" msgstr "" -#: cps/web.py:1578 +#: cps/web.py:1721 msgid "Edit mail settings" msgstr "" -#: cps/web.py:1606 +#: cps/web.py:1749 #, python-format msgid "User '%(nick)s' deleted" msgstr "" -#: cps/web.py:1661 +#: cps/web.py:1825 #, python-format msgid "User '%(nick)s' updated" msgstr "" -#: cps/web.py:1664 +#: cps/web.py:1828 msgid "An unknown error occured." msgstr "" -#: cps/web.py:1666 +#: cps/web.py:1831 #, python-format msgid "Edit User %(nick)s" msgstr "" -#: cps/web.py:1904 +#: cps/web.py:2036 cps/web.py:2039 cps/web.py:2113 +msgid "edit metadata" +msgstr "" + +#: cps/web.py:2071 #, python-format msgid "Failed to create path %s (Permission denied)." msgstr "" -#: cps/web.py:1909 +#: cps/web.py:2076 #, python-format msgid "Failed to store file %s (Permission denied)." msgstr "" -#: cps/web.py:1914 +#: cps/web.py:2081 #, python-format msgid "Failed to delete file %s (Permission denied)." msgstr "" @@ -323,7 +347,7 @@ msgstr "" msgid "Admin" msgstr "" -#: cps/templates/admin.html:12 cps/templates/detail.html:114 +#: cps/templates/admin.html:12 cps/templates/detail.html:116 msgid "Download" msgstr "" @@ -371,15 +395,15 @@ msgstr "" msgid "Change SMTP settings" msgstr "" -#: cps/templates/admin.html:56 +#: cps/templates/admin.html:56 cps/templates/admin.html:76 msgid "Configuration" msgstr "" #: cps/templates/admin.html:59 -msgid "Log File" +msgid "Calibre DB dir" msgstr "" -#: cps/templates/admin.html:60 +#: cps/templates/admin.html:60 cps/templates/config_edit.html:32 msgid "Log Level" msgstr "" @@ -387,7 +411,7 @@ msgstr "" msgid "Port" msgstr "" -#: cps/templates/admin.html:62 +#: cps/templates/admin.html:62 cps/templates/config_edit.html:19 msgid "Books per page" msgstr "" @@ -403,101 +427,155 @@ msgstr "" msgid "Anonymous browsing" msgstr "" -#: cps/templates/admin.html:76 +#: cps/templates/admin.html:77 msgid "Administration" msgstr "" -#: cps/templates/admin.html:78 +#: cps/templates/admin.html:79 msgid "Restart Calibre-web" msgstr "" -#: cps/templates/detail.html:38 -msgid "Book" -msgstr "" - -#: cps/templates/detail.html:38 -msgid "of" -msgstr "" - -#: cps/templates/detail.html:44 -msgid "language" +#: cps/templates/admin.html:80 +msgid "Stop Calibre-web" msgstr "" -#: cps/templates/detail.html:103 -msgid "Description:" +#: cps/templates/admin.html:91 +msgid "Do you really want to restart Calibre-web?" msgstr "" -#: cps/templates/detail.html:131 -msgid "Read in browser" +#: cps/templates/admin.html:92 cps/templates/admin.html:107 +msgid "Ok" msgstr "" -#: cps/templates/detail.html:151 -msgid "Add to shelf" +#: cps/templates/admin.html:93 cps/templates/admin.html:108 +#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:54 +#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17 +#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:107 +msgid "Back" msgstr "" -#: cps/templates/detail.html:191 -msgid "Edit metadata" +#: cps/templates/admin.html:106 +msgid "Do you really want to stop Calibre-web?" msgstr "" -#: cps/templates/edit_book.html:14 cps/templates/search_form.html:6 +#: cps/templates/book_edit.html:16 cps/templates/search_form.html:6 msgid "Book Title" msgstr "" -#: cps/templates/edit_book.html:18 cps/templates/search_form.html:10 +#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10 msgid "Author" msgstr "" -#: cps/templates/edit_book.html:22 +#: cps/templates/book_edit.html:24 msgid "Description" msgstr "" -#: cps/templates/edit_book.html:26 cps/templates/search_form.html:13 +#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13 msgid "Tags" msgstr "" -#: cps/templates/edit_book.html:31 cps/templates/layout.html:133 +#: cps/templates/book_edit.html:33 cps/templates/layout.html:133 #: cps/templates/search_form.html:33 msgid "Series" msgstr "" -#: cps/templates/edit_book.html:35 +#: cps/templates/book_edit.html:37 msgid "Series id" msgstr "" -#: cps/templates/edit_book.html:39 +#: cps/templates/book_edit.html:41 msgid "Rating" msgstr "" -#: cps/templates/edit_book.html:43 +#: cps/templates/book_edit.html:45 msgid "Cover URL (jpg)" msgstr "" -#: cps/templates/edit_book.html:48 cps/templates/user_edit.html:27 +#: cps/templates/book_edit.html:50 cps/templates/user_edit.html:27 msgid "Language" msgstr "" -#: cps/templates/edit_book.html:59 +#: cps/templates/book_edit.html:61 msgid "Yes" msgstr "" -#: cps/templates/edit_book.html:60 +#: cps/templates/book_edit.html:62 msgid "No" msgstr "" -#: cps/templates/edit_book.html:102 +#: cps/templates/book_edit.html:104 msgid "view book after edit" msgstr "" -#: cps/templates/edit_book.html:105 cps/templates/login.html:19 -#: cps/templates/search_form.html:75 cps/templates/shelf_edit.html:15 -#: cps/templates/user_edit.html:97 +#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:52 +#: cps/templates/login.html:19 cps/templates/search_form.html:75 +#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:105 msgid "Submit" msgstr "" -#: cps/templates/edit_book.html:106 cps/templates/email_edit.html:36 -#: cps/templates/shelf_edit.html:17 cps/templates/shelf_order.html:12 -#: cps/templates/user_edit.html:99 -msgid "Back" +#: cps/templates/config_edit.html:7 +msgid "Location of Calibre database" +msgstr "" + +#: cps/templates/config_edit.html:11 +msgid "Server Port" +msgstr "" + +#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7 +msgid "Title" +msgstr "" + +#: cps/templates/config_edit.html:23 +msgid "No. of random books to show" +msgstr "" + +#: cps/templates/config_edit.html:28 +msgid "Regular expression for title sorting" +msgstr "" + +#: cps/templates/config_edit.html:42 +msgid "Enable uploading" +msgstr "" + +#: cps/templates/config_edit.html:46 +msgid "Enable anonymous browsing" +msgstr "" + +#: cps/templates/config_edit.html:50 +msgid "Enable public registration" +msgstr "" + +#: cps/templates/config_edit.html:57 cps/templates/layout.html:91 +#: cps/templates/login.html:4 +msgid "Login" +msgstr "" + +#: cps/templates/detail.html:40 +msgid "Book" +msgstr "" + +#: cps/templates/detail.html:40 +msgid "of" +msgstr "" + +#: cps/templates/detail.html:46 +msgid "language" +msgstr "" + +#: cps/templates/detail.html:105 +msgid "Description:" +msgstr "" + +#: cps/templates/detail.html:133 +msgid "Read in browser" +msgstr "" + +#: cps/templates/detail.html:153 +msgid "Add to shelf" +msgstr "" + +#: cps/templates/detail.html:193 +msgid "Edit metadata" msgstr "" #: cps/templates/email_edit.html:11 @@ -600,10 +678,6 @@ msgstr "" msgid "Logout" msgstr "" -#: cps/templates/layout.html:91 cps/templates/login.html:4 -msgid "Login" -msgstr "" - #: cps/templates/layout.html:92 cps/templates/register.html:18 msgid "Register" msgstr "" @@ -722,10 +796,6 @@ msgstr "" msgid "Change order" msgstr "" -#: cps/templates/shelf_edit.html:7 -msgid "Title" -msgstr "" - #: cps/templates/shelf_edit.html:12 msgid "should the shelf be public?" msgstr "" @@ -790,31 +860,39 @@ msgstr "" msgid "Show category selection" msgstr "" -#: cps/templates/user_edit.html:68 +#: cps/templates/user_edit.html:65 +msgid "Show author selection" +msgstr "" + +#: cps/templates/user_edit.html:69 +msgid "Show random books in detail view" +msgstr "" + +#: cps/templates/user_edit.html:76 msgid "Admin user" msgstr "" -#: cps/templates/user_edit.html:73 +#: cps/templates/user_edit.html:81 msgid "Allow Downloads" msgstr "" -#: cps/templates/user_edit.html:77 +#: cps/templates/user_edit.html:85 msgid "Allow Uploads" msgstr "" -#: cps/templates/user_edit.html:81 +#: cps/templates/user_edit.html:89 msgid "Allow Edit" msgstr "" -#: cps/templates/user_edit.html:86 +#: cps/templates/user_edit.html:94 msgid "Allow Changing Password" msgstr "" -#: cps/templates/user_edit.html:93 +#: cps/templates/user_edit.html:101 msgid "Delete this user" msgstr "" -#: cps/templates/user_edit.html:104 +#: cps/templates/user_edit.html:112 msgid "Recent Downloads" msgstr "" From e9d0bff559da192f94c77336584b48655bcaf37d Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Sat, 28 Jan 2017 20:54:31 +0100 Subject: [PATCH 07/19] - added statistics for Tags and series - Loglevel is displayed as text instead of value --- cps/templates/admin.html | 2 +- cps/templates/stats.html | 8 ++++++++ cps/ub.py | 13 +++++++++++-- cps/web.py | 4 +++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 9ac1c858..4f5c67ec 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -66,7 +66,7 @@ {{config.config_calibre_dir}} - {{config.config_log_level}} + {{config.get_Log_Level()}} {{config.config_port}} {{config.config_books_per_page}} {% if config.config_uploading %}{% else %}{% endif %} diff --git a/cps/templates/stats.html b/cps/templates/stats.html index 49c13fc4..630039ab 100644 --- a/cps/templates/stats.html +++ b/cps/templates/stats.html @@ -40,6 +40,14 @@ {{authorcounter}} {{_('Authors in this Library')}} + + {{categorycounter}} + {{_('Categories in this Library')}} + + + {{seriecounter}} + {{_('Series in this Library')}} + {% endblock %} diff --git a/cps/ub.py b/cps/ub.py index d5612fdb..29d42fc0 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -288,8 +288,17 @@ class Config: def get_main_dir(self): return self.config_main_dir - #def is_Calibre_Configured(self): - # return self.db_configured + def get_Log_Level(self): + ret_value="" + if self.config_log_level == logging.INFO: + ret_value='INFO' + elif self.config_log_level == logging.DEBUG: + ret_value='DEBUG' + elif self.config_log_level == logging.WARNING: + ret_value='WARNING' + elif self.config_log_level == logging.ERROR: + ret_value='ERROR' + return ret_value # Migrate database to current version, has to be updated after every database change. Currently migration from diff --git a/cps/web.py b/cps/web.py index 035e3631..a37497ef 100755 --- a/cps/web.py +++ b/cps/web.py @@ -973,6 +973,8 @@ def admin_forbidden(): def stats(): counter = len(db.session.query(db.Books).all()) authors = len(db.session.query(db.Authors).all()) + categorys = len(db.session.query(db.Tags).all()) + series = len(db.session.query(db.Series).all()) versions = uploader.book_formats.get_versions() vendorpath = os.path.join(config.get_main_dir + "vendor" + os.sep) if sys.platform == "win32": @@ -989,7 +991,7 @@ def stats(): versions['KindlegenVersion'] = lines versions['PythonVersion'] = sys.version return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions, - title=_(u"Statistics")) + categorycounter=categorys, seriecounter=series, title=_(u"Statistics")) @app.route("/shutdown") From 935b6e314346c255c1b9455b9955b6b4920f152d Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Sun, 29 Jan 2017 21:06:08 +0100 Subject: [PATCH 08/19] Code cosmetics Bugfix download opds added changable title to opds feed removed unused search.xml file --- cps.py | 3 +++ cps/book_formats.py | 3 +++ cps/epub.py | 3 +++ cps/fb2.py | 2 ++ cps/templates/feed.xml | 4 ++-- cps/templates/index.xml | 4 ++-- cps/templates/osd.xml | 10 +++++----- cps/templates/search.xml | 20 -------------------- cps/uploader.py | 3 +++ cps/web.py | 32 ++++++++++++++++---------------- 10 files changed, 39 insertions(+), 45 deletions(-) delete mode 100644 cps/templates/search.xml diff --git a/cps.py b/cps.py index b8f51e02..4c8bfbe2 100755 --- a/cps.py +++ b/cps.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import os import sys import time diff --git a/cps/book_formats.py b/cps/book_formats.py index bd7d17c5..3aa26f4e 100644 --- a/cps/book_formats.py +++ b/cps/book_formats.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import logging import uploader import os diff --git a/cps/epub.py b/cps/epub.py index 3c79b82b..49900724 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import zipfile from lxml import etree import os diff --git a/cps/fb2.py b/cps/fb2.py index ccc85207..f073c71e 100644 --- a/cps/fb2.py +++ b/cps/fb2.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- from lxml import etree import os diff --git a/cps/templates/feed.xml b/cps/templates/feed.xml index 8f8679de..9e9c9c02 100644 --- a/cps/templates/feed.xml +++ b/cps/templates/feed.xml @@ -29,9 +29,9 @@ - Calibre Web + {{instance}} - Calibre Web + {{instance}} https://github.com/janeczku/calibre-web diff --git a/cps/templates/index.xml b/cps/templates/index.xml index c522ec8d..fc1d5b12 100644 --- a/cps/templates/index.xml +++ b/cps/templates/index.xml @@ -6,9 +6,9 @@ type="application/atom+xml;profile=opds-catalog;kind=navigation"/> - Calibre Web + {{instance}} - Calibre Web + {{instance}} https://github.com/janeczku/calibre-web diff --git a/cps/templates/osd.xml b/cps/templates/osd.xml index 1d943981..35c1147c 100644 --- a/cps/templates/osd.xml +++ b/cps/templates/osd.xml @@ -1,16 +1,16 @@ - Calibre Web - Calibre Web - Calibre Web ebook catalog - janeczku + {{instance}} + {{instance}} + {{_('instanceCalibre Web ebook catalog')}} + Janeczku https://github.com/janeczku/calibre-web open - de-DE + {{lang}} UTF-8 UTF-8 diff --git a/cps/templates/search.xml b/cps/templates/search.xml deleted file mode 100644 index ddbb3e91..00000000 --- a/cps/templates/search.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - Calibre Web - Calibre Web - Calibre Web ebook catalog - janeczku - https://github.com/janeczku/calibre-web - - - - - - open - de-DE - UTF-8 - UTF-8 - diff --git a/cps/uploader.py b/cps/uploader.py index 0da801fa..41283361 100644 --- a/cps/uploader.py +++ b/cps/uploader.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import os from tempfile import gettempdir import hashlib diff --git a/cps/web.py b/cps/web.py index a37497ef..735318e9 100755 --- a/cps/web.py +++ b/cps/web.py @@ -106,7 +106,7 @@ mimetypes.add_type('image/vnd.djvu', '.djvu') app = (Flask(__name__)) app.wsgi_app = ReverseProxied(app.wsgi_app) -'''formatter = logging.Formatter( +formatter = logging.Formatter( "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s") file_handler = RotatingFileHandler(os.path.join(config.get_main_dir, "calibre-web.log"), maxBytes=50000, backupCount=1) file_handler.setFormatter(formatter) @@ -115,7 +115,7 @@ app.logger.setLevel(config.config_log_level) app.logger.info('Starting Calibre Web...') logging.getLogger("book_formats").addHandler(file_handler) -logging.getLogger("book_formats").setLevel(config.config_log_level)''' +logging.getLogger("book_formats").setLevel(config.config_log_level) Principal(app) @@ -434,7 +434,7 @@ def feed_index(): filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) else: filter = True - xml = render_template('index.xml') + xml = render_title_template('index.xml') response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -443,7 +443,7 @@ def feed_index(): @app.route("/opds/osd") @requires_basic_auth_if_no_ano def feed_osd(): - xml = render_template('osd.xml') + xml = render_title_template('osd.xml',lang='de-DE') response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -472,9 +472,9 @@ def feed_search(term): db.Books.title.like("%" + term + "%"))).filter(filter).all() entriescount = len(entries) if len(entries) > 0 else 1 pagination = Pagination(1, entriescount, entriescount) - xml = render_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) + xml = render_title_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) else: - xml = render_template('feed.xml', searchterm="") + xml = render_title_template('feed.xml', searchterm="") response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -494,7 +494,7 @@ def feed_new(): config.config_books_per_page) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Books).filter(filter).all())) - xml = render_template('feed.xml', entries=entries, pagination=pagination) + xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -512,7 +512,7 @@ def feed_discover(): # off = 0 entries = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_books_per_page) pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page)) - xml = render_template('feed.xml', entries=entries, pagination=pagination) + xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -533,7 +533,7 @@ def feed_hot(): pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Books).filter(filter).filter( db.Books.ratings.any(db.Ratings.rating > 9)).all())) - xml = render_template('feed.xml', entries=entries, pagination=pagination) + xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -553,7 +553,7 @@ def feed_authorindex(): authors = db.session.query(db.Authors).order_by(db.Authors.sort).offset(off).limit(config.config_books_per_page) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Authors).all())) - xml = render_template('feed.xml', authors=authors, pagination=pagination) + xml = render_title_template('feed.xml', authors=authors, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -574,7 +574,7 @@ def feed_author(id): pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id)).filter( filter).all())) - xml = render_template('feed.xml', entries=entries, pagination=pagination) + xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -589,7 +589,7 @@ def feed_categoryindex(): entries = db.session.query(db.Tags).order_by(db.Tags.name).offset(off).limit(config.config_books_per_page) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Tags).all())) - xml = render_template('feed.xml', categorys=entries, pagination=pagination) + xml = render_title_template('feed.xml', categorys=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -610,7 +610,7 @@ def feed_category(id): pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id == id)).filter( filter).all())) - xml = render_template('feed.xml', entries=entries, pagination=pagination) + xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -629,7 +629,7 @@ def feed_seriesindex(): entries = db.session.query(db.Series).order_by(db.Series.name).offset(off).limit(config.config_books_per_page) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Series).all())) - xml = render_template('feed.xml', series=entries, pagination=pagination) + xml = render_title_template('feed.xml', series=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -650,7 +650,7 @@ def feed_series(id): pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Books).filter(db.Books.series.any(db.Series.id == id)).filter( filter).all())) - xml = render_template('feed.xml', entries=entries, pagination=pagination) + xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -670,7 +670,7 @@ def get_opds_download_link(book_id, format): if len(author) > 0: file_name = author + '-' + file_name file_name = helper.get_valid_filename(file_name) - response = make_response(send_from_directory(os.path.join(config.DB_ROOT, book.path), data.name + "." + format)) + response = make_response(send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + format)) response.headers["Content-Disposition"] = "attachment; filename=\"%s.%s\"" % (data.name, format) return response From b1293c84bc4d13dd1bb644f4a16590bf9493eb99 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Mon, 30 Jan 2017 18:58:36 +0100 Subject: [PATCH 09/19] - Implemented autoupdater for testing - failed logins are logged - no of backups for log-file increased to 2 --- .gitattributes | 1 + cps/helper.py | 104 ++++++++++++++++++++++++++++++++++++++- cps/templates/admin.html | 16 ++++++ cps/web.py | 40 ++++++++++++++- 4 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..01c7be27 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +web.py ident \ No newline at end of file diff --git a/cps/helper.py b/cps/helper.py index d861af38..63000609 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -21,7 +21,7 @@ from email.MIMEText import MIMEText from email.generator import Generator from flask_babel import gettext as _ import subprocess - +import shutil def update_download(book_id, user_id): check = ub.session.query(ub.Downloads).filter(ub.Downloads.user_id == user_id).filter(ub.Downloads.book_id == @@ -254,3 +254,105 @@ def update_dir_stucture(book_id, calibrepath): os.renames(path, new_author_path) book.path = new_authordir + os.sep + book.path.split(os.sep)[1] db.session.commit() + + +def file_to_list(file): + return [x.strip() for x in open(file, 'r') if not x.startswith('#EXT')] + +def one_minus_two(one, two): + return [x for x in one if x not in set(two)] + +def reduce_dirs(delete_files, new_list): + new_delete = [] + for file in delete_files: + parts = file.split(os.sep) + sub = '' + for i in range(len(parts)): + sub = os.path.join(sub, parts[i]) + if sub == '': + sub = os.sep + count = 0 + for song in new_list: + if song.startswith(sub): + count += 1 + break + if count == 0: + if sub != '\\': + new_delete.append(sub) + break + return list(set(new_delete)) + +def reduce_files(remove_items, exclude_items): + rf = [] + for item in remove_items: + if not item in exclude_items: + rf.append(item) + return rf + +def moveallfiles(root_src_dir, root_dst_dir): + change_permissions = False + if sys.platform == "win32" or sys.platform == "darwin": + change_permissions=True + else: + app.logger.debug('OS-System : '+sys.platform ) + new_permissions=os.stat(root_dst_dir) + for src_dir, dirs, files in os.walk(root_src_dir): + dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1) + if not os.path.exists(dst_dir): + os.makedirs(dst_dir) + if change_permissions: + os.chown(dst_dir,new_permissions.ST_UID,new_permissions.ST_GID) + for file_ in files: + src_file = os.path.join(src_dir, file_) + dst_file = os.path.join(dst_dir, file_) + if change_permissions: + permission=os.stat(dst_file) + if os.path.exists(dst_file): + os.remove(dst_file) + shutil.move(src_file, dst_dir) + if change_permissions: + os.chown(dst_file, permission.ST_UID, permission.ST_GID) + return + + +def update_source(source,destination): + # destination files + old_list=list() + exclude = (['vendor' + os.sep + 'kindlegen.exe','vendor' + os.sep + 'kindlegen','/app.db','vendor','/update.py']) + for root, dirs, files in os.walk(destination, topdown=True): + for name in files: + old_list.append(os.path.join(root, name).replace(destination, '')) + for name in dirs: + old_list.append(os.path.join(root, name).replace(destination, '')) + # source files + new_list = list() + for root, dirs, files in os.walk(source, topdown=True): + for name in files: + new_list.append(os.path.join(root, name).replace(source, '')) + for name in dirs: + new_list.append(os.path.join(root, name).replace(source, '')) + + delete_files = one_minus_two(old_list, new_list) + print('raw delete list', delete_files) + + rf= reduce_files(delete_files, exclude) + print('reduced delete list', rf) + + remove_items = reduce_dirs(rf, new_list) + print('delete files', remove_items) + + moveallfiles(source, destination) + + for item in remove_items: + item_path = os.path.join(destination, item[1:]) + if os.path.isdir(item_path): + # app.logger.info("Delete dir "+ item_path) + print("Delete dir "+ item_path) + #shutil.rmtree(item_path) + else: + try: + print("Delete file "+ item_path) + #os.remove(item_path) + except: + print("Could not remove:"+item_path) + shutil.rmtree(source, ignore_errors=True) \ No newline at end of file diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 4f5c67ec..48343a76 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -78,6 +78,8 @@ {% if not development %}
{{_('Restart Calibre-web')}}
{{_('Stop Calibre-web')}}
+
{{_('Check for update')}}
+ {% endif %}
@@ -134,5 +136,19 @@ return alert(data.text);} }); }); + $("#check_for_update").click(function() { + $("#check_for_update").html('Checking...'); + $.ajax({ + dataType: 'json', + url: "{{url_for('get_update_status')}}", + success: function(data) { + if (data.status == true) { + $("#check_for_update").addClass('hidden'); + $("#perform_update").removeClass('hidden'); + }else{ + $("#check_for_update").html('{{_('Check for update')}}'); + };} + }); + }); {% endblock %} diff --git a/cps/web.py b/cps/web.py index 735318e9..4d4612d2 100755 --- a/cps/web.py +++ b/cps/web.py @@ -3,6 +3,7 @@ import mimetypes import logging from logging.handlers import RotatingFileHandler +from tempfile import gettempdir import textwrap from flask import Flask, render_template, session, request, Response, redirect, url_for, send_from_directory, \ make_response, g, flash, abort @@ -39,6 +40,7 @@ import re import db from shutil import move, copyfile from tornado.ioloop import IOLoop +import StringIO try: from wand.image import Image @@ -108,7 +110,7 @@ app.wsgi_app = ReverseProxied(app.wsgi_app) formatter = logging.Formatter( "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s") -file_handler = RotatingFileHandler(os.path.join(config.get_main_dir, "calibre-web.log"), maxBytes=50000, backupCount=1) +file_handler = RotatingFileHandler(os.path.join(config.get_main_dir, "calibre-web.log"), maxBytes=50000, backupCount=2) file_handler.setFormatter(formatter) app.logger.addHandler(file_handler) app.logger.setLevel(config.config_log_level) @@ -707,6 +709,20 @@ def get_tags_json(): json_dumps = json.dumps([dict(r) for r in entries]) return json_dumps +@app.route("/get_update_status", methods=['GET']) +@login_required_if_no_ano +def get_update_status(): + status = {} + if request.method == "GET": + commit_id = '$Id$' + commit = requests.get('https://api.github.com/repos/janeczku/calibre-web/git/refs/heads/master').json() + if "object" in commit and commit['object']['sha'] != commit_id[5:-2]: + status['status'] = True + else: + status['status'] = False + return json.dumps(status) + + @app.route("/get_languages_json", methods=['GET', 'POST']) @login_required_if_no_ano @@ -1019,6 +1035,27 @@ def shutdown(): else: abort(404) +@app.route("/update") +@login_required +@admin_required +def update(): + global global_task + r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True) + fname = re.findall("filename=(.+)", r.headers['content-disposition'])[0] + z = zipfile.ZipFile(StringIO.StringIO(r.content)) + tmp_dir = gettempdir() + z.extractall(tmp_dir) + helper.update_source(os.path.join(tmp_dir,os.path.splitext(fname)[0]),config.get_main_dir) + global_task = 0 + db.session.close() + db.engine.dispose() + ub.session.close() + ub.engine.dispose() + # stop tornado server + server = IOLoop.instance() + server.add_callback(server.stop) + flash(_(u"Update done"), category="info") + return logout() @app.route("/search", methods=["GET"]) @login_required_if_no_ano @@ -1269,6 +1306,7 @@ def login(): # test= return redirect(url_for("index")) else: + app.logger.info('Login failed for user "'+form['username']+'"') flash(_(u"Wrong Username or Password"), category="error") return render_title_template('login.html', title=_(u"login")) From 1d3be7f4c6b8bf0b0da3b4017cfcf9a343b9c909 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Mon, 30 Jan 2017 19:03:55 +0100 Subject: [PATCH 10/19] Bugfix Git ident id --- cps/web.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cps/web.py b/cps/web.py index 4d4612d2..a1d29fb9 100755 --- a/cps/web.py +++ b/cps/web.py @@ -714,6 +714,7 @@ def get_tags_json(): def get_update_status(): status = {} if request.method == "GET": + # should be automatically replaced by git with current commit hash commit_id = '$Id$' commit = requests.get('https://api.github.com/repos/janeczku/calibre-web/git/refs/heads/master').json() if "object" in commit and commit['object']['sha'] != commit_id[5:-2]: From af0417758c175bd3b4fbdfcbc6ae56c015cdd3b4 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Mon, 30 Jan 2017 19:10:22 +0100 Subject: [PATCH 11/19] Bugfix change permissions during update --- cps/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cps/helper.py b/cps/helper.py index 63000609..f3f5b660 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -290,9 +290,9 @@ def reduce_files(remove_items, exclude_items): return rf def moveallfiles(root_src_dir, root_dst_dir): - change_permissions = False + change_permissions = True if sys.platform == "win32" or sys.platform == "darwin": - change_permissions=True + change_permissions=False else: app.logger.debug('OS-System : '+sys.platform ) new_permissions=os.stat(root_dst_dir) From 9b09de12b5412200a4e7b03418a5bb39f87afdad Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Mon, 30 Jan 2017 19:45:03 +0100 Subject: [PATCH 12/19] Bugfix update (still not working correct) --- cps/helper.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cps/helper.py b/cps/helper.py index f3f5b660..87191629 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -305,13 +305,19 @@ def moveallfiles(root_src_dir, root_dst_dir): for file_ in files: src_file = os.path.join(src_dir, file_) dst_file = os.path.join(dst_dir, file_) - if change_permissions: - permission=os.stat(dst_file) if os.path.exists(dst_file): + if change_permissions: + permission=os.stat(dst_file) os.remove(dst_file) + else: + if change_permissions: + permission=new_permissions shutil.move(src_file, dst_dir) if change_permissions: - os.chown(dst_file, permission.ST_UID, permission.ST_GID) + try: + os.chown(dst_file, permission.ST_UID, permission.ST_GID) + except: + pass return From 296d2615fe892355cf361d991d8a10d72a7b9bea Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Mon, 30 Jan 2017 19:52:32 +0100 Subject: [PATCH 13/19] Code cosmetics --- .gitattributes | 2 +- cps/web.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index 01c7be27..14949c1b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -web.py ident \ No newline at end of file +* ident \ No newline at end of file diff --git a/cps/web.py b/cps/web.py index a1d29fb9..e42063a6 100755 --- a/cps/web.py +++ b/cps/web.py @@ -129,9 +129,7 @@ lm = LoginManager(app) lm.init_app(app) lm.login_view = 'login' lm.anonymous_user = ub.Anonymous - app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' - db.setup_db() @babel.localeselector From c45968a1ed77de73f82ed52e80d4df4053707614 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Tue, 31 Jan 2017 19:29:47 +0100 Subject: [PATCH 14/19] Bugfix Updater --- .gitattributes | 2 +- cps/helper.py | 33 ++++++++++++++++++++------------- cps/web.py | 4 ++-- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.gitattributes b/.gitattributes index 14949c1b..7d82586e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -* ident \ No newline at end of file +web.py ident export-subst diff --git a/cps/helper.py b/cps/helper.py index 87191629..0aa7707b 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -292,32 +292,40 @@ def reduce_files(remove_items, exclude_items): def moveallfiles(root_src_dir, root_dst_dir): change_permissions = True if sys.platform == "win32" or sys.platform == "darwin": - change_permissions=False + change_permissions=False else: - app.logger.debug('OS-System : '+sys.platform ) - new_permissions=os.stat(root_dst_dir) + app.logger.debug('Update on OS-System : '+sys.platform ) + #print('OS-System: '+sys.platform ) + new_permissions=os.stat(root_dst_dir) + #print new_permissions for src_dir, dirs, files in os.walk(root_src_dir): dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1) if not os.path.exists(dst_dir): os.makedirs(dst_dir) + #print('Create-Dir: '+dst_dir) if change_permissions: - os.chown(dst_dir,new_permissions.ST_UID,new_permissions.ST_GID) + #print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid)) + os.chown(dst_dir,new_permissions.st_uid,new_permissions.st_gid) for file_ in files: src_file = os.path.join(src_dir, file_) dst_file = os.path.join(dst_dir, file_) if os.path.exists(dst_file): if change_permissions: permission=os.stat(dst_file) + #print('Remove file before copy: '+dst_file) os.remove(dst_file) else: if change_permissions: permission=new_permissions shutil.move(src_file, dst_dir) + #print('Move File '+src_file+' to '+dst_dir) if change_permissions: try: - os.chown(dst_file, permission.ST_UID, permission.ST_GID) + os.chown(dst_file, permission.st_uid, permission.st_uid) + #print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid)) except: - pass + e = sys.exc_info() + #print('Fail '+str(dst_file)+' error: '+str(e)) return @@ -339,26 +347,25 @@ def update_source(source,destination): new_list.append(os.path.join(root, name).replace(source, '')) delete_files = one_minus_two(old_list, new_list) - print('raw delete list', delete_files) + #print('raw delete list', delete_files) rf= reduce_files(delete_files, exclude) - print('reduced delete list', rf) + #print('reduced delete list', rf) remove_items = reduce_dirs(rf, new_list) - print('delete files', remove_items) + #print('delete files', remove_items) moveallfiles(source, destination) for item in remove_items: item_path = os.path.join(destination, item[1:]) if os.path.isdir(item_path): - # app.logger.info("Delete dir "+ item_path) print("Delete dir "+ item_path) - #shutil.rmtree(item_path) + shutil.rmtree(item_path) else: try: print("Delete file "+ item_path) - #os.remove(item_path) + os.remove(item_path) except: print("Could not remove:"+item_path) - shutil.rmtree(source, ignore_errors=True) \ No newline at end of file + #shutil.rmtree(source, ignore_errors=True) diff --git a/cps/web.py b/cps/web.py index e42063a6..9443c7e0 100755 --- a/cps/web.py +++ b/cps/web.py @@ -713,9 +713,9 @@ def get_update_status(): status = {} if request.method == "GET": # should be automatically replaced by git with current commit hash - commit_id = '$Id$' + commit_id = '$Format:%H$' commit = requests.get('https://api.github.com/repos/janeczku/calibre-web/git/refs/heads/master').json() - if "object" in commit and commit['object']['sha'] != commit_id[5:-2]: + if "object" in commit and commit['object']['sha'] != commit_id: status['status'] = True else: status['status'] = False From f21c65ac50fc32cff55eb407c9a3c741373fc4fc Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Tue, 31 Jan 2017 20:48:01 +0100 Subject: [PATCH 15/19] - Bugfix Migration of database for config_log_level - Bugfix Updater deleting temporary sourcefolder --- cps/helper.py | 2 +- cps/ub.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cps/helper.py b/cps/helper.py index 0aa7707b..a0867660 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -368,4 +368,4 @@ def update_source(source,destination): os.remove(item_path) except: print("Could not remove:"+item_path) - #shutil.rmtree(source, ignore_errors=True) + shutil.rmtree(source, ignore_errors=True) diff --git a/cps/ub.py b/cps/ub.py index 29d42fc0..183ea26e 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -325,7 +325,7 @@ def migrate_Database(): conn.execute("ALTER TABLE Settings ADD column `config_random_books` INTEGER DEFAULT 4") conn.execute("ALTER TABLE Settings ADD column `config_title_regex` String DEFAULT " "'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+'") - conn.execute("ALTER TABLE Settings ADD column `config_log_level` SmallInteger DEFAULT '" + logging.INFO + "'") + conn.execute("ALTER TABLE Settings ADD column `config_log_level` SmallInteger DEFAULT " + str(logging.INFO)) conn.execute("ALTER TABLE Settings ADD column `config_uploading` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0") From 176c7dce708a19c318ad04265ecbbeea980807e8 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Thu, 2 Feb 2017 19:36:31 +0100 Subject: [PATCH 16/19] - Fix for #100 - Fix migration of shelf order - fix show random books in detail view for authors and series --- cps/templates/detail.html | 4 ++-- cps/templates/discover.html | 4 ++-- cps/templates/index.html | 2 +- cps/templates/list.html | 2 +- cps/templates/search.html | 4 ++-- cps/templates/shelf.html | 4 ++-- cps/templates/user_edit.html | 2 +- cps/ub.py | 7 +++++++ cps/web.py | 7 +++++-- 9 files changed, 23 insertions(+), 13 deletions(-) diff --git a/cps/templates/detail.html b/cps/templates/detail.html index a7b8c78e..753db662 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -15,7 +15,7 @@

{{entry.title}}

{% for author in entry.authors %} - {{author.name}} + {{author.name}} {% if not loop.last %} & {% endif %} @@ -65,7 +65,7 @@ {% for tag in entry.tags %} - {{tag.name}} + {{tag.name}} {%endfor%} diff --git a/cps/templates/discover.html b/cps/templates/discover.html index fb76172e..51157972 100644 --- a/cps/templates/discover.html +++ b/cps/templates/discover.html @@ -9,13 +9,13 @@

{% if entry.has_cover is defined %} - + {% endif %}

{{entry.title|shortentitle}}

-

{{entry.authors[0].name}}

+

{{entry.authors[0].name}}

{% if entry.ratings.__len__() > 0 %}
{% for number in range((entry.ratings[0].rating/2)|int(2)) %} diff --git a/cps/templates/index.html b/cps/templates/index.html index 9abbaff1..835bde23 100755 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -55,7 +55,7 @@

{{entry.title|shortentitle}}

{% for author in entry.authors %} - {{author.name}} + {{author.name}} {% if not loop.last %} & {% endif %} diff --git a/cps/templates/list.html b/cps/templates/list.html index 07911535..412f9196 100644 --- a/cps/templates/list.html +++ b/cps/templates/list.html @@ -5,7 +5,7 @@ {% for entry in entries %}

{{entry.count}}
- +
{% endfor %}
diff --git a/cps/templates/search.html b/cps/templates/search.html index 75f3aa7c..efdf40b9 100644 --- a/cps/templates/search.html +++ b/cps/templates/search.html @@ -16,7 +16,7 @@
{% if entry.has_cover is defined %} - + {% endif %}
@@ -24,7 +24,7 @@

{{entry.title|shortentitle}}

{% for author in entry.authors %} - {{author.name}} + {{author.name}} {% if not loop.last %} & {% endif %} diff --git a/cps/templates/shelf.html b/cps/templates/shelf.html index 4f8a5f94..4c23f827 100644 --- a/cps/templates/shelf.html +++ b/cps/templates/shelf.html @@ -15,13 +15,13 @@

{% if entry.has_cover is defined %} - + {% endif %}

{{entry.title|shortentitle}}

-

{{entry.authors[0].name}}

+

{{entry.authors[0].name}}

{% if entry.ratings.__len__() > 0 %}
{% for number in range((entry.ratings[0].rating/2)|int(2)) %} diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index 674ca2aa..8e4e0c8c 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -113,7 +113,7 @@ {% for entry in downloads %} {% endfor %} diff --git a/cps/ub.py b/cps/ub.py index 183ea26e..b68e0598 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -330,6 +330,13 @@ def migrate_Database(): conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0") session.commit() + try: + session.query(exists().where(BookShelf.order)).scalar() + session.commit() + except exc.OperationalError: # Database is not compatible, some rows are missing + conn = engine.connect() + conn.execute("ALTER TABLE book_shelf_link ADD column 'order' INTEGER DEFAULT 1") + session.commit() try: create = False session.query(exists().where(User.sidebar_view)).scalar() diff --git a/cps/web.py b/cps/web.py index 9443c7e0..36cf971b 100755 --- a/cps/web.py +++ b/cps/web.py @@ -797,7 +797,7 @@ def hot_books(page): filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) else: filter = True - if current_user.show_random_books(): + if current_user.show_detail_random(): random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_random_books) else: random = false @@ -839,11 +839,12 @@ def author_list(): @app.route("/author/") @login_required_if_no_ano def author(name): + name=requests.utils.unquote(name) if current_user.filter_language() != "all": filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) else: filter = True - if current_user.show_random_books(): + if current_user.show_detail_random(): random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_random_books) else: random = false @@ -870,6 +871,7 @@ def series_list(): @app.route("/series//'") @login_required_if_no_ano def series(name, page): + name = requests.utils.unquote(name) entries, random, pagination = fill_indexpage(page, db.Books, db.Books.series.any(db.Series.name == name), db.Books.series_index) if entries: @@ -942,6 +944,7 @@ def category_list(): @app.route('/category//') @login_required_if_no_ano def category(name, page): + name = requests.utils.unquote(name) entries, random, pagination = fill_indexpage(page, db.Books, db.Books.tags.any(db.Tags.name == name), db.Books.timestamp.desc()) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, From 35f24d294a378e69ac8484046856919d0e5b53cf Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Fri, 3 Feb 2017 10:51:58 +0100 Subject: [PATCH 17/19] Update readme.md --- readme.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 75d50672..4629a47f 100755 --- a/readme.md +++ b/readme.md @@ -24,6 +24,7 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d - Upload new books in PDF, epub, fb2 format - Support for Calibre custom columns - Fine grained per-user permissions +- Self update capability ## Quick start @@ -42,10 +43,13 @@ The configuration can be changed as admin in the admin panel under "Configuratio Server Port: Changes the port calibre-web is listening, changes take effect after pressing submit button + Enable public registration: -Tick to enable public user registration. +Tick to enable public user registration. + Enable anonymous browsing: -Tick to allow not logged in users to browse the catalog, anonymous user permissions can be set as admin ("Guest" user) +Tick to allow not logged in users to browse the catalog, anonymous user permissions can be set as admin ("Guest" user) + Enable uploading: Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick library to be installed. From e46320b12f851b9a85e4afe4470d7d709af899c3 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Fri, 3 Feb 2017 13:20:35 +0100 Subject: [PATCH 18/19] folders now relying on ids not names (removes all encoding issues on link folders) Language filter working in opds feed removed redundant code authos now paginating in opds feed --- cps/templates/feed.xml | 24 ++----- cps/templates/index.html | 4 +- cps/templates/list.html | 2 +- cps/web.py | 134 ++++++++++++++------------------------- 4 files changed, 55 insertions(+), 109 deletions(-) diff --git a/cps/templates/feed.xml b/cps/templates/feed.xml index 9e9c9c02..5bda14f9 100644 --- a/cps/templates/feed.xml +++ b/cps/templates/feed.xml @@ -60,28 +60,12 @@ {% endfor %} {% endfor %} - {% for author in authors %} - - {{author.name}} - {{ url_for('feed_author', id=author.id) }} - - - - {% endfor %} - {% for entry in categorys %} - - {{entry.name}} - {{ url_for('feed_category', id=entry.id) }} - - - - {% endfor %} - {% for entry in series %} + {% for entry in listelements %} {{entry.name}} - {{ url_for('feed_series', id=entry.id) }} - - + {{ url_for(folder, id=entry.id) }} + + {% endfor %} diff --git a/cps/templates/index.html b/cps/templates/index.html index 835bde23..a59ae6b3 100755 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -18,7 +18,7 @@

{{entry.title|shortentitle}}

-

{{entry.authors[0].name}}

+

{{entry.authors[0].name}}

{% if entry.ratings.__len__() > 0 %}
{% for number in range((entry.ratings[0].rating/2)|int(2)) %} @@ -55,7 +55,7 @@

{{entry.title|shortentitle}}

{% for author in entry.authors %} - {{author.name}} + {{author.name}} {% if not loop.last %} & {% endif %} diff --git a/cps/templates/list.html b/cps/templates/list.html index 412f9196..f3886c4b 100644 --- a/cps/templates/list.html +++ b/cps/templates/list.html @@ -5,7 +5,7 @@ {% for entry in entries %}

{{entry.count}}
- +
{% endfor %}
diff --git a/cps/web.py b/cps/web.py index 36cf971b..f4168244 100755 --- a/cps/web.py +++ b/cps/web.py @@ -484,16 +484,10 @@ def feed_search(term): @requires_basic_auth_if_no_ano def feed_new(): off = request.args.get("offset") - if current_user.filter_language() != "all": - filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) - else: - filter = True if not off: off = 0 - entries = db.session.query(db.Books).filter(filter).order_by(db.Books.timestamp.desc()).offset(off).limit( - config.config_books_per_page) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Books).filter(filter).all())) + entries, random, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, True, db.Books.timestamp.desc()) xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -503,13 +497,10 @@ def feed_new(): @app.route("/opds/discover") @requires_basic_auth_if_no_ano def feed_discover(): - # off = request.args.get("start_index") if current_user.filter_language() != "all": filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) else: filter = True - # if not off: - # off = 0 entries = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_books_per_page) pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page)) xml = render_title_template('feed.xml', entries=entries, pagination=pagination) @@ -522,17 +513,10 @@ def feed_discover(): @requires_basic_auth_if_no_ano def feed_hot(): off = request.args.get("offset") - if current_user.filter_language() != "all": - filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) - else: - filter = True if not off: off = 0 - entries = db.session.query(db.Books).filter(filter).filter(db.Books.ratings.any(db.Ratings.rating > 9)).offset( - off).limit(config.config_books_per_page) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Books).filter(filter).filter( - db.Books.ratings.any(db.Ratings.rating > 9)).all())) + entries, random, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.ratings.any(db.Ratings.rating > 9), db.Books.timestamp.desc()) xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -543,17 +527,17 @@ def feed_hot(): @requires_basic_auth_if_no_ano def feed_authorindex(): off = request.args.get("offset") - # ToDo: Language filter not working + if not off: + off = 0 if current_user.filter_language() != "all": filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) else: filter = True - if not off: - off = 0 - authors = db.session.query(db.Authors).order_by(db.Authors.sort).offset(off).limit(config.config_books_per_page) + entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(filter)\ + .group_by('books_authors_link.author').order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Authors).all())) - xml = render_title_template('feed.xml', authors=authors, pagination=pagination) + xml = render_title_template('feed.xml', listelements=entries, folder='feed_author', pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -563,17 +547,10 @@ def feed_authorindex(): @requires_basic_auth_if_no_ano def feed_author(id): off = request.args.get("offset") - if current_user.filter_language() != "all": - filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) - else: - filter = True if not off: off = 0 - entries = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id)).filter( - filter).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.id == id)).filter( - filter).all())) + entries, random, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.authors.any(db.Authors.id == id), db.Books.timestamp.desc()) xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -586,10 +563,15 @@ def feed_categoryindex(): off = request.args.get("offset") if not off: off = 0 - entries = db.session.query(db.Tags).order_by(db.Tags.name).offset(off).limit(config.config_books_per_page) + if current_user.filter_language() != "all": + filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) + else: + filter = True + entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(filter).\ + group_by('books_tags_link.tag').order_by(db.Tags.name).offset(off).limit(config.config_books_per_page) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Tags).all())) - xml = render_title_template('feed.xml', categorys=entries, pagination=pagination) + xml = render_title_template('feed.xml', listelements=entries, folder='feed_category', pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -599,17 +581,10 @@ def feed_categoryindex(): @requires_basic_auth_if_no_ano def feed_category(id): off = request.args.get("offset") - if current_user.filter_language() != "all": - filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) - else: - filter = True if not off: off = 0 - entries = db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id == id)).order_by( - db.Books.timestamp.desc()).filter(filter).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Books).filter(db.Books.tags.any(db.Tags.id == id)).filter( - filter).all())) + entries, random, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.tags.any(db.Tags.id == id), db.Books.timestamp.desc()) xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -620,16 +595,17 @@ def feed_category(id): @requires_basic_auth_if_no_ano def feed_seriesindex(): off = request.args.get("offset") + if not off: + off = 0 if current_user.filter_language() != "all": filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) else: filter = True - if not off: - off = 0 - entries = db.session.query(db.Series).order_by(db.Series.name).offset(off).limit(config.config_books_per_page) + entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(filter).\ + group_by('books_series_link.series').order_by(db.Series.sort).offset(off).all() pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(db.session.query(db.Series).all())) - xml = render_title_template('feed.xml', series=entries, pagination=pagination) + xml = render_title_template('feed.xml', listelements=entries, folder='feed_series', pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" return response @@ -639,17 +615,10 @@ def feed_seriesindex(): @requires_basic_auth_if_no_ano def feed_series(id): off = request.args.get("offset") - if current_user.filter_language() != "all": - filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) - else: - filter = True if not off: off = 0 - entries = db.session.query(db.Books).filter(db.Books.series.any(db.Series.id == id)).order_by( - db.Books.timestamp.desc()).filter(filter).offset(off).limit(config.config_books_per_page) - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(db.session.query(db.Books).filter(db.Books.series.any(db.Series.id == id)).filter( - filter).all())) + entries, random, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.series.any(db.Series.id == id),db.Books.series_index) xml = render_title_template('feed.xml', entries=entries, pagination=pagination) response = make_response(xml) response.headers["Content-Type"] = "application/xml" @@ -728,7 +697,6 @@ def get_update_status(): def get_languages_json(): if request.method == "GET": query = request.args.get('q').lower() - # entries = db.session.execute("select lang_code from languages where lang_code like '%" + query + "%'") languages = db.session.query(db.Languages).all() for lang in languages: try: @@ -736,7 +704,6 @@ def get_languages_json(): lang.name = cur_l.get_language_name(get_locale()) except: lang.name = _(isoLanguages.get(part3=lang.lang_code).name) - entries = [s for s in languages if query in s.name.lower()] json_dumps = json.dumps([dict(name=r.name) for r in entries]) return json_dumps @@ -782,8 +749,6 @@ def get_matching_tags(): @app.route('/page/') @login_required_if_no_ano def index(page): - #if not config.db_configured: - # return redirect(url_for('basic_configuration')) entries, random, pagination = fill_indexpage(page, db.Books, True, db.Books.timestamp.desc()) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Latest Books")) @@ -836,22 +801,18 @@ def author_list(): return render_title_template('list.html', entries=entries, folder='author', title=_(u"Author list")) -@app.route("/author/") +@app.route("/author/", defaults={'page': 1}) +@app.route("/author//'") @login_required_if_no_ano -def author(name): - name=requests.utils.unquote(name) - if current_user.filter_language() != "all": - filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) - else: - filter = True - if current_user.show_detail_random(): - random = db.session.query(db.Books).filter(filter).order_by(func.random()).limit(config.config_random_books) +def author(id,page): + entries, random, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == id), + db.Books.timestamp.desc()) + name = db.session.query(db.Authors).filter(db.Authors.id == id).first().name + if entries: + return render_title_template('index.html', random=random, entries=entries, title=_(u"Author: %(name)s", name=name)) else: - random = false - - entries = db.session.query(db.Books).filter(db.Books.authors.any(db.Authors.name.like("%" + name + "%"))).filter( - filter).all() - return render_title_template('index.html', random=random, entries=entries, title=_(u"Author: %(nam)s", nam=name)) + flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") + return redirect(url_for("index")) @app.route("/series") @@ -867,13 +828,13 @@ def series_list(): return render_title_template('list.html', entries=entries, folder='series', title=_(u"Series list")) -@app.route("/series//", defaults={'page': 1}) -@app.route("/series//'") +@app.route("/series//", defaults={'page': 1}) +@app.route("/series//'") @login_required_if_no_ano -def series(name, page): - name = requests.utils.unquote(name) - entries, random, pagination = fill_indexpage(page, db.Books, db.Books.series.any(db.Series.name == name), +def series(id, page): + entries, random, pagination = fill_indexpage(page, db.Books, db.Books.series.any(db.Series.id == id), db.Books.series_index) + name=db.session.query(db.Series).filter(db.Series.id == id).first().name if entries: return render_title_template('index.html', random=random, pagination=pagination, entries=entries, title=_(u"Series: %(serie)s", serie=name)) @@ -940,13 +901,14 @@ def category_list(): return render_title_template('list.html', entries=entries, folder='category', title=_(u"Category list")) -@app.route("/category/", defaults={'page': 1}) -@app.route('/category//') +@app.route("/category/", defaults={'page': 1}) +@app.route('/category//') @login_required_if_no_ano -def category(name, page): - name = requests.utils.unquote(name) - entries, random, pagination = fill_indexpage(page, db.Books, db.Books.tags.any(db.Tags.name == name), +def category(id, page): + entries, random, pagination = fill_indexpage(page, db.Books, db.Books.tags.any(db.Tags.id == id), db.Books.timestamp.desc()) + + name=db.session.query(db.Tags).filter(db.Tags.id == id).first().name return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Category: %(name)s", name=name)) From 861920af88f4c6ca97a311da0c2085ad23bf37f1 Mon Sep 17 00:00:00 2001 From: OzzieIsaacs Date: Fri, 3 Feb 2017 13:44:13 +0100 Subject: [PATCH 19/19] Pubdate is now showing up (#95) Bugfix links in detailview --- cps/templates/detail.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 753db662..4fa37a1d 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -15,7 +15,7 @@

{{entry.title}}

{% for author in entry.authors %} - {{author.name}} + {{author.name}} {% if not loop.last %} & {% endif %} @@ -37,7 +37,7 @@ {% endif %} {% if entry.series|length > 0 %} -

{{_('Book')}} {{entry.series_index}} {{_('of')}} {{entry.series[0].name}}

+

{{_('Book')}} {{entry.series_index}} {{_('of')}} {{entry.series[0].name}}

{% endif %} {% if entry.languages.__len__() > 0 %} @@ -58,20 +58,21 @@

{% endif %} - {% if entry.tags|length > 0 %}

{% for tag in entry.tags %} - {{tag.name}} + {{tag.name}} {%endfor%}

{% endif %} - + {% if entry.pubdate != '0101-01-01 00:00:00' %} +

{{_('Publishing date')}}: {{entry.pubdate[:10]}}

+ {% endif %} {% if cc|length > 0 %}

@@ -101,7 +102,7 @@ {% endif %} - {% if entry.comments|length > 0 %} + {% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}

{{_('Description:')}}

{{entry.comments[0].text|safe}} {% endif %}