Commit d6ee8f75 authored by Ozzieisaacs's avatar Ozzieisaacs

More refactoring

parent a00d93a2
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys import sys
base_path = os.path.dirname(os.path.abspath(__file__))
# Insert local directories into path
sys.path.append(base_path)
sys.path.append(os.path.join(base_path, 'cps'))
sys.path.append(os.path.join(base_path, 'vendor'))
from cps import create_app from cps import create_app
from cps.web import web from cps.opds import opds
from cps import Server from cps import Server
from cps.web import web
from cps.jinjia import jinjia
from cps.about import about
from cps.shelf import shelf
from cps.admin import admi
from cps.gdrive import gdrive
from cps.editbooks import editbook
if __name__ == '__main__': if __name__ == '__main__':
app = create_app() app = create_app()
app.register_blueprint(web) app.register_blueprint(web)
app.register_blueprint(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(shelf)
app.register_blueprint(admi)
app.register_blueprint(gdrive)
app.register_blueprint(editbook)
Server.startServer() Server.startServer()
......
...@@ -4,32 +4,56 @@ ...@@ -4,32 +4,56 @@
# import logging # import logging
# from logging.handlers import SMTPHandler, RotatingFileHandler # from logging.handlers import SMTPHandler, RotatingFileHandler
# import os # import os
import mimetypes
from flask import Flask# , request, current_app from flask import Flask, request, g
from flask_login import LoginManager from flask_login import LoginManager
from flask_babel import Babel # , lazy_gettext as _l from flask_babel import Babel
import cache_buster import cache_buster
from reverseproxy import ReverseProxied from reverseproxy import ReverseProxied
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from flask_principal import Principal from flask_principal import Principal
# from flask_sqlalchemy import SQLAlchemy from babel.core import UnknownLocaleError
from babel import Locale as LC
from babel import negotiate_locale
import os import os
import ub import ub
from ub import Config, Settings from ub import Config, Settings
import cPickle try:
import cPickle
except ImportError:
import pickle as cPickle
# Normal
babel = Babel()
mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub')
mimetypes.add_type('application/fb2+zip', '.fb2')
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')
mimetypes.add_type('application/mpeg', '.mpeg')
mimetypes.add_type('application/mpeg', '.mp3')
mimetypes.add_type('application/mp4', '.m4a')
mimetypes.add_type('application/mp4', '.m4b')
mimetypes.add_type('application/ogg', '.ogg')
mimetypes.add_type('application/ogg', '.oga')
app = Flask(__name__)
lm = LoginManager() lm = LoginManager()
lm.login_view = 'web.login' lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous lm.anonymous_user = ub.Anonymous
ub.init_db()
ub_session = ub.session
# ub_session.start()
config = Config() config = Config()
...@@ -42,15 +66,14 @@ searched_ids = {} ...@@ -42,15 +66,14 @@ searched_ids = {}
from worker import WorkerThread from worker import WorkerThread
global_WorkerThread = WorkerThread() global_WorkerThread = WorkerThread()
from server import server from server import server
Server = server() Server = server()
babel = Babel()
def create_app(): def create_app():
app = Flask(__name__)
app.wsgi_app = ReverseProxied(app.wsgi_app) app.wsgi_app = ReverseProxied(app.wsgi_app)
cache_buster.init_cache_busting(app) cache_buster.init_cache_busting(app)
...@@ -71,15 +94,38 @@ def create_app(): ...@@ -71,15 +94,38 @@ def create_app():
logging.getLogger("book_formats").setLevel(config.config_log_level) logging.getLogger("book_formats").setLevel(config.config_log_level)
Principal(app) Principal(app)
lm.init_app(app) lm.init_app(app)
babel.init_app(app)
app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT') app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT')
Server.init_app(app) Server.init_app(app)
db.setup_db() db.setup_db()
babel.init_app(app)
global_WorkerThread.start() global_WorkerThread.start()
# app.config.from_object(config_class)
# db.init_app(app)
# login.init_app(app)
return app return app
@babel.localeselector
def get_locale():
# if a user is logged in, use the locale from the user settings
user = getattr(g, 'user', None)
# user = None
if user is not None and hasattr(user, "locale"):
if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale
translations = [str(item) for item in babel.list_translations()] + ['en']
preferred = list()
for x in request.accept_languages.values():
try:
preferred.append(str(LC.parse(x.replace('-', '_'))))
except (UnknownLocaleError, ValueError) as e:
app.logger.debug("Could not parse locale: %s", e)
preferred.append('en')
return negotiate_locale(preferred, translations)
@babel.timezoneselector
def get_timezone():
user = getattr(g, 'user', None)
if user is not None:
return user.timezone
from updater import Updater
updater_thread = Updater()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import Blueprint
from flask_login import login_required
import db
import sys
import uploader
from babel import __version__ as babelVersion
from sqlalchemy import __version__ as sqlalchemyVersion
from flask_principal import __version__ as flask_principalVersion
from iso639 import __version__ as iso639Version
from pytz import __version__ as pytzVersion
from flask import __version__ as flaskVersion
from werkzeug import __version__ as werkzeugVersion
from jinja2 import __version__ as jinja2Version
import converter
from flask_babel import gettext as _
from cps import Server
import requests
from web import render_title_template
try:
from flask_login import __version__ as flask_loginVersion
except ImportError:
from flask_login.__about__ import __version__ as flask_loginVersion
about = Blueprint('about', __name__)
@about.route("/stats")
@login_required
def stats():
counter = db.session.query(db.Books).count()
authors = db.session.query(db.Authors).count()
categorys = db.session.query(db.Tags).count()
series = db.session.query(db.Series).count()
versions = uploader.get_versions()
versions['Babel'] = 'v' + babelVersion
versions['Sqlalchemy'] = 'v' + sqlalchemyVersion
versions['Werkzeug'] = 'v' + werkzeugVersion
versions['Jinja2'] = 'v' + jinja2Version
versions['Flask'] = 'v' + flaskVersion
versions['Flask Login'] = 'v' + flask_loginVersion
versions['Flask Principal'] = 'v' + flask_principalVersion
versions['Iso 639'] = 'v' + iso639Version
versions['pytz'] = 'v' + pytzVersion
versions['Requests'] = 'v' + requests.__version__
versions['pySqlite'] = 'v' + db.engine.dialect.dbapi.version
versions['Sqlite'] = 'v' + db.engine.dialect.dbapi.sqlite_version
versions.update(converter.versioncheck())
versions.update(Server.getNameVersion())
versions['Python'] = sys.version
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions,
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
This diff is collapsed.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2016-2019 lemmsh cervinko Kennyl matthazinski OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import uploader
import os
from flask_babel import gettext as _
import comic
try:
from lxml.etree import LXML_VERSION as lxmlversion
except ImportError:
lxmlversion = None
__author__ = 'lemmsh'
logger = logging.getLogger("book_formats")
try:
from wand.image import Image
from wand import version as ImageVersion
use_generic_pdf_cover = False
except (ImportError, RuntimeError) as e:
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
use_generic_pdf_cover = True
try:
from PyPDF2 import PdfFileReader
from PyPDF2 import __version__ as PyPdfVersion
use_pdf_meta = True
except ImportError as e:
logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e)
use_pdf_meta = False
try:
import epub
use_epub_meta = True
except ImportError as e:
logger.warning('cannot import epub, extracting epub metadata will not work: %s', e)
use_epub_meta = False
try:
import fb2
use_fb2_meta = True
except ImportError as e:
logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e)
use_fb2_meta = False
def process(tmp_file_path, original_file_name, original_file_extension):
meta = None
try:
if ".PDF" == original_file_extension.upper():
meta = pdf_meta(tmp_file_path, original_file_name, original_file_extension)
if ".EPUB" == original_file_extension.upper() and use_epub_meta is True:
meta = epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension)
if ".FB2" == original_file_extension.upper() and use_fb2_meta is True:
meta = fb2.get_fb2_info(tmp_file_path, original_file_extension)
if original_file_extension.upper() in ['.CBZ', '.CBT']:
meta = comic.get_comic_info(tmp_file_path, original_file_name, original_file_extension)
except Exception as ex:
logger.warning('cannot parse metadata, using default: %s', ex)
if meta and meta.title.strip() and meta.author.strip():
return meta
else:
return default_meta(tmp_file_path, original_file_name, original_file_extension)
def default_meta(tmp_file_path, original_file_name, original_file_extension):
return uploader.BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=original_file_name,
author=u"Unknown",
cover=None,
description="",
tags="",
series="",
series_id="",
languages="")
def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
if use_pdf_meta:
pdf = PdfFileReader(open(tmp_file_path, 'rb'))
doc_info = pdf.getDocumentInfo()
else:
doc_info = None
if doc_info is not None:
author = doc_info.author if doc_info.author else u"Unknown"
title = doc_info.title if doc_info.title else original_file_name
subject = doc_info.subject
else:
author = u"Unknown"
title = original_file_name
subject = ""
return uploader.BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title,
author=author,
cover=pdf_preview(tmp_file_path, original_file_name),
description=subject,
tags="",
series="",
series_id="",
languages="")
def pdf_preview(tmp_file_path, tmp_dir):
if use_generic_pdf_cover:
return None
else:
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
img.compression_quality = 88
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
def get_versions():
if not use_generic_pdf_cover:
IVersion = ImageVersion.MAGICK_VERSION
WVersion = ImageVersion.VERSION
else:
IVersion = _(u'not installed')
WVersion = _(u'not installed')
if use_pdf_meta:
PVersion='v'+PyPdfVersion
else:
PVersion=_(u'not installed')
if lxmlversion:
XVersion = 'v'+'.'.join(map(str, lxmlversion))
else:
XVersion = _(u'not installed')
return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion, 'Wand Version': WVersion}
...@@ -24,13 +24,14 @@ import ub ...@@ -24,13 +24,14 @@ import ub
import re import re
from flask_babel import gettext as _ from flask_babel import gettext as _
from subproc_wrapper import process_open from subproc_wrapper import process_open
from cps import config
def versionKindle(): def versionKindle():
versions = _(u'not installed') versions = _(u'not installed')
if os.path.exists(ub.config.config_converterpath): if os.path.exists(config.config_converterpath):
try: try:
p = process_open(ub.config.config_converterpath) p = process_open(config.config_converterpath)
# p = subprocess.Popen(ub.config.config_converterpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # p = subprocess.Popen(ub.config.config_converterpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait() p.wait()
for lines in p.stdout.readlines(): for lines in p.stdout.readlines():
...@@ -45,9 +46,9 @@ def versionKindle(): ...@@ -45,9 +46,9 @@ def versionKindle():
def versionCalibre(): def versionCalibre():
versions = _(u'not installed') versions = _(u'not installed')
if os.path.exists(ub.config.config_converterpath): if os.path.exists(config.config_converterpath):
try: try:
p = process_open([ub.config.config_converterpath, '--version']) p = process_open([config.config_converterpath, '--version'])
# p = subprocess.Popen([ub.config.config_converterpath, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # p = subprocess.Popen([ub.config.config_converterpath, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait() p.wait()
for lines in p.stdout.readlines(): for lines in p.stdout.readlines():
...@@ -61,9 +62,9 @@ def versionCalibre(): ...@@ -61,9 +62,9 @@ def versionCalibre():
def versioncheck(): def versioncheck():
if ub.config.config_ebookconverter == 1: if config.config_ebookconverter == 1:
return versionKindle() return versionKindle()
elif ub.config.config_ebookconverter == 2: elif config.config_ebookconverter == 2:
return versionCalibre() return versionCalibre()
else: else:
return {'ebook_converter':_(u'not configured')} return {'ebook_converter':_(u'not configured')}
......
This diff is collapsed.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from flask import Blueprint
import gdriveutils
from flask import flash, request, redirect, url_for, abort
from flask_babel import gettext as _
from cps import app, config, ub, db
from flask_login import login_required
import json
from uuid import uuid4
from time import time
import tempfile
from shutil import move, copyfile
from web import admin_required
try:
from googleapiclient.errors import HttpError
except ImportError:
pass
gdrive = Blueprint('gdrive', __name__)
current_milli_time = lambda: int(round(time() * 1000))
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
@gdrive.route("/gdrive/authenticate")
@login_required
@admin_required
def authenticate_google_drive():
try:
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
except gdriveutils.InvalidConfigError:
flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'),
category="error")
return redirect(url_for('web.index'))
return redirect(authUrl)
@gdrive.route("/gdrive/callback")
def google_drive_callback():
auth_code = request.args.get('code')
if not auth_code:
abort(403)
try:
credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code)
with open(os.path.join(config.get_main_dir,'gdrive_credentials'), 'w') as f:
f.write(credentials.to_json())
except ValueError as error:
app.logger.error(error)
return redirect(url_for('configuration'))
@gdrive.route("/gdrive/watch/subscribe")
@login_required
@admin_required
def watch_gdrive():
if not config.config_google_drive_watch_changes_response:
with open(os.path.join(config.get_main_dir,'client_secrets.json'), 'r') as settings:
filedata = json.load(settings)
if filedata['web']['redirect_uris'][0].endswith('/'):
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))]
else:
filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))]
address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0]
notification_id = str(uuid4())
try:
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000)
settings = ub.session.query(ub.Settings).first()
settings.config_google_drive_watch_changes_response = json.dumps(result)
ub.session.merge(settings)
ub.session.commit()
settings = ub.session.query(ub.Settings).first()
config.loadSettings()
except HttpError as e:
reason=json.loads(e.content)['error']['errors'][0]
if reason['reason'] == u'push.webhookUrlUnauthorized':
flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error")
else:
flash(reason['message'], category="error")
return redirect(url_for('configuration'))
@gdrive.route("/gdrive/watch/revoke")
@login_required
@admin_required
def revoke_watch_gdrive():
last_watch_response = config.config_google_drive_watch_changes_response
if last_watch_response:
try:
gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'],
last_watch_response['resourceId'])
except HttpError:
pass
settings = ub.session.query(ub.Settings).first()
settings.config_google_drive_watch_changes_response = None
ub.session.merge(settings)
ub.session.commit()
config.loadSettings()
return redirect(url_for('configuration'))
@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST'])
def on_received_watch_confirmation():
app.logger.debug(request.headers)
if request.headers.get('X-Goog-Channel-Token') == gdrive_watch_callback_token \
and request.headers.get('X-Goog-Resource-State') == 'change' \
and request.data:
data = request.data
def updateMetaData():
app.logger.info('Change received from gdrive')
app.logger.debug(data)
try:
j = json.loads(data)
app.logger.info('Getting change details')
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
app.logger.debug(response)
if response:
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
if not response['deleted'] and response['file']['title'] == 'metadata.db' and response['file']['md5Checksum'] != hashlib.md5(dbpath):
tmpDir = tempfile.gettempdir()
app.logger.info('Database file updated')
copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time())))
app.logger.info('Backing up existing and downloading updated metadata.db')
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db"))
app.logger.info('Setting up new DB')
# prevent error on windows, as os.rename does on exisiting files
move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath)
db.setup_db()
except Exception as e:
app.logger.info(e.message)
app.logger.exception(e)
updateMetaData()
return ''
...@@ -27,7 +27,7 @@ except ImportError: ...@@ -27,7 +27,7 @@ except ImportError:
gdrive_support = False gdrive_support = False
import os import os
from cps import config from cps import config, app
import cli import cli
import shutil import shutil
from flask import Response, stream_with_context from flask import Response, stream_with_context
...@@ -37,8 +37,6 @@ from sqlalchemy.ext.declarative import declarative_base ...@@ -37,8 +37,6 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import * from sqlalchemy.orm import *
import web
class Singleton: class Singleton:
""" """
A non-thread-safe helper class to ease implementing singletons. A non-thread-safe helper class to ease implementing singletons.
...@@ -89,6 +87,10 @@ class Gdrive: ...@@ -89,6 +87,10 @@ class Gdrive:
def __init__(self): def __init__(self):
self.drive = getDrive(gauth=Gauth.Instance().auth) self.drive = getDrive(gauth=Gauth.Instance().auth)
def is_gdrive_ready():
return os.path.exists(os.path.join(config.get_main_dir, 'settings.yaml')) and \
os.path.exists(os.path.join(config.get_main_dir, 'gdrive_credentials'))
engine = create_engine('sqlite:///{0}'.format(cli.gdpath), echo=False) engine = create_engine('sqlite:///{0}'.format(cli.gdpath), echo=False)
Base = declarative_base() Base = declarative_base()
...@@ -157,9 +159,9 @@ def getDrive(drive=None, gauth=None): ...@@ -157,9 +159,9 @@ def getDrive(drive=None, gauth=None):
try: try:
gauth.Refresh() gauth.Refresh()
except RefreshError as e: except RefreshError as e:
web.app.logger.error("Google Drive error: " + e.message) app.logger.error("Google Drive error: " + e.message)
except Exception as e: except Exception as e:
web.app.logger.exception(e) app.logger.exception(e)
else: else:
# Initialize the saved creds # Initialize the saved creds
gauth.Authorize() gauth.Authorize()
...@@ -169,7 +171,7 @@ def getDrive(drive=None, gauth=None): ...@@ -169,7 +171,7 @@ def getDrive(drive=None, gauth=None):
try: try:
drive.auth.Refresh() drive.auth.Refresh()
except RefreshError as e: except RefreshError as e:
web.app.logger.error("Google Drive error: " + e.message) app.logger.error("Google Drive error: " + e.message)
return drive return drive
def listRootFolders(): def listRootFolders():
...@@ -206,7 +208,7 @@ def getEbooksFolderId(drive=None): ...@@ -206,7 +208,7 @@ def getEbooksFolderId(drive=None):
try: try:
gDriveId.gdrive_id = getEbooksFolder(drive)['id'] gDriveId.gdrive_id = getEbooksFolder(drive)['id']
except Exception: except Exception:
web.app.logger.error('Error gDrive, root ID not found') app.logger.error('Error gDrive, root ID not found')
gDriveId.path = '/' gDriveId.path = '/'
session.merge(gDriveId) session.merge(gDriveId)
session.commit() session.commit()
...@@ -455,10 +457,10 @@ def getChangeById (drive, change_id): ...@@ -455,10 +457,10 @@ def getChangeById (drive, change_id):
change = drive.auth.service.changes().get(changeId=change_id).execute() change = drive.auth.service.changes().get(changeId=change_id).execute()
return change return change
except (errors.HttpError) as error: except (errors.HttpError) as error:
web.app.logger.info(error.message) app.logger.info(error.message)
return None return None
except Exception as e: except Exception as e:
web.app.logger.info(e) app.logger.info(e)
return None return None
...@@ -527,6 +529,6 @@ def do_gdrive_download(df, headers): ...@@ -527,6 +529,6 @@ def do_gdrive_download(df, headers):
if resp.status == 206: if resp.status == 206:
yield content yield content
else: else:
web.app.logger.info('An error occurred: %s' % resp) app.logger.info('An error occurred: %s' % resp)
return return
return Response(stream_with_context(stream()), headers=headers) return Response(stream_with_context(stream()), headers=headers)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# custom jinja filters
from flask import Blueprint, request, url_for
import datetime
import re
from cps import mimetypes
from babel.dates import format_date
from flask_babel import get_locale
jinjia = Blueprint('jinjia', __name__)
# pagination links in jinja
@jinjia.app_template_filter('url_for_other_page')
def url_for_other_page(page):
args = request.view_args.copy()
args['page'] = page
return url_for(request.endpoint, **args)
# shortentitles to at longest nchar, shorten longer words if necessary
@jinjia.app_template_filter('shortentitle')
def shortentitle_filter(s, nchar=20):
text = s.split()
res = "" # result
suml = 0 # overall length
for line in text:
if suml >= 60:
res += '...'
break
# if word longer than 20 chars truncate line and append '...', otherwise add whole word to result
# string, and summarize total length to stop at chars given by nchar
if len(line) > nchar:
res += line[:(nchar-3)] + '[..] '
suml += nchar+3
else:
res += line + ' '
suml += len(line) + 1
return res.strip()
@jinjia.app_template_filter('mimetype')
def mimetype_filter(val):
try:
s = mimetypes.types_map['.' + val]
except Exception:
s = 'application/octet-stream'
return s
@jinjia.app_template_filter('formatdate')
def formatdate_filter(val):
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
return format_date(formatdate, format='medium', locale=get_locale())
@jinjia.app_template_filter('formatdateinput')
def format_date_input(val):
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
date_obj = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
input_date = date_obj.isoformat().split('T', 1)[0] # Hack to support dates <1900
return '' if input_date == "0101-01-01" else input_date
@jinjia.app_template_filter('strftime')
def timestamptodate(date, fmt=None):
date = datetime.datetime.fromtimestamp(
int(date)/1000
)
native = date.replace(tzinfo=None)
if fmt:
time_format = fmt
else:
time_format = '%d %m %Y - %H:%S'
return native.strftime(time_format)
@jinjia.app_template_filter('yesno')
def yesno(value, yes, no):
return yes if value else no
'''@jinjia.app_template_filter('canread')
def canread(ext):
if isinstance(ext, db.Data):
ext = ext.format
return ext.lower() in EXTENSIONS_READER'''
This diff is collapsed.
This diff is collapsed.
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
from math import ceil from math import ceil
# simple pagination for the feed # simple pagination for the feed
class Pagination(object): class Pagination(object):
def __init__(self, page, per_page, total_count): def __init__(self, page, per_page, total_count):
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
class ReverseProxied(object): class ReverseProxied(object):
"""Wrap the application in this middleware and configure the """Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind front-end server to add these headers, to let you quietly bind
......
...@@ -37,33 +37,34 @@ except ImportError: ...@@ -37,33 +37,34 @@ except ImportError:
gevent_present = False gevent_present = False
class server: class server:
wsgiserver = None wsgiserver = None
restart= False restart = False
app = None app = None
def __init__(self): def __init__(self):
signal.signal(signal.SIGINT, self.killServer) signal.signal(signal.SIGINT, self.killServer)
signal.signal(signal.SIGTERM, self.killServer) signal.signal(signal.SIGTERM, self.killServer)
def init_app(self,application): def init_app(self, application):
self.app = application self.app = application
def start_gevent(self): def start_gevent(self):
ssl_args = dict()
try: try:
ssl_args = dict() certfile_path = config.get_config_certfile()
certfile_path = config.get_config_certfile() keyfile_path = config.get_config_keyfile()
keyfile_path = config.get_config_keyfile()
if certfile_path and keyfile_path: if certfile_path and keyfile_path:
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
ssl_args = {"certfile": certfile_path, ssl_args = {"certfile": certfile_path,
"keyfile": keyfile_path} "keyfile": keyfile_path}
else: else:
self.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path)) self.app.logger.info('The specified paths for the ssl certificate file and/or key file seem '
'to be broken. Ignoring ssl. Cert path: %s | Key path: '
'%s' % (certfile_path, keyfile_path))
if os.name == 'nt': if os.name == 'nt':
self.wsgiserver= WSGIServer(('0.0.0.0', config.config_port), self.app, spawn=Pool(), **ssl_args) self.wsgiserver = WSGIServer(('0.0.0.0', config.config_port), self.app, spawn=Pool(), **ssl_args)
else: else:
self.wsgiserver = WSGIServer(('', config.config_port), self.app, spawn=Pool(), **ssl_args) self.wsgiserver = WSGIServer(('', config.config_port), self.app, spawn=Pool(), **ssl_args)
self.wsgiserver.serve_forever() self.wsgiserver.serve_forever()
...@@ -90,14 +91,16 @@ class server: ...@@ -90,14 +91,16 @@ class server:
try: try:
ssl = None ssl = None
self.app.logger.info('Starting Tornado server') self.app.logger.info('Starting Tornado server')
certfile_path = config.get_config_certfile() certfile_path = config.get_config_certfile()
keyfile_path = config.get_config_keyfile() keyfile_path = config.get_config_keyfile()
if certfile_path and keyfile_path: if certfile_path and keyfile_path:
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
ssl = {"certfile": certfile_path, ssl = {"certfile": certfile_path,
"keyfile": keyfile_path} "keyfile": keyfile_path}
else: else:
self.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path)) self.app.logger.info('The specified paths for the ssl certificate file and/or key file '
'seem to be broken. Ignoring ssl. Cert path: %s | Key '
'path: %s' % (certfile_path, keyfile_path))
# Max Buffersize set to 200MB # Max Buffersize set to 200MB
http_server = HTTPServer(WSGIContainer(self.app), http_server = HTTPServer(WSGIContainer(self.app),
...@@ -114,7 +117,7 @@ class server: ...@@ -114,7 +117,7 @@ class server:
global_WorkerThread.stop() global_WorkerThread.stop()
sys.exit(1) sys.exit(1)
if self.restart == True: if self.restart is True:
self.app.logger.info("Performing restart of Calibre-Web") self.app.logger.info("Performing restart of Calibre-Web")
global_WorkerThread.stop() global_WorkerThread.stop()
if os.name == 'nt': if os.name == 'nt':
...@@ -145,6 +148,6 @@ class server: ...@@ -145,6 +148,6 @@ class server:
@staticmethod @staticmethod
def getNameVersion(): def getNameVersion():
if gevent_present: if gevent_present:
return {'Gevent':'v' + geventVersion} return {'Gevent': 'v' + geventVersion}
else: else:
return {'Tornado':'v'+tornadoVersion} return {'Tornado': 'v' + tornadoVersion}
This diff is collapsed.
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
{% for user in content %} {% for user in content %}
{% if not user.role_anonymous() or config.config_anonbrowse %} {% if not user.role_anonymous() or config.config_anonbrowse %}
<tr> <tr>
<td><a href="{{url_for('web.edit_user', user_id=user.id)}}">{{user.nickname}}</a></td> <td><a href="{{url_for('admin.edit_user', user_id=user.id)}}">{{user.nickname}}</a></td>
<td>{{user.email}}</td> <td>{{user.email}}</td>
<td>{{user.kindle_mail}}</td> <td>{{user.kindle_mail}}</td>
<td>{{user.downloads.count()}}</td> <td>{{user.downloads.count()}}</td>
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</table> </table>
<div class="btn btn-default" id="admin_new_user"><a href="{{url_for('web.new_user')}}">{{_('Add new user')}}</a></div> <div class="btn btn-default" id="admin_new_user"><a href="{{url_for('admin.new_user')}}">{{_('Add new user')}}</a></div>
</div> </div>
</div> </div>
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<td class="hidden-xs">{{email.mail_from}}</td> <td class="hidden-xs">{{email.mail_from}}</td>
</tr> </tr>
</table> </table>
<div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('web.edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div> <div class="btn btn-default" id="admin_edit_email"><a href="{{url_for('admin.edit_mailsettings')}}">{{_('Change SMTP settings')}}</a></div>
</div> </div>
</div> </div>
...@@ -96,8 +96,8 @@ ...@@ -96,8 +96,8 @@
<div class="col-xs-6 col-sm-5">{% if config.config_remote_login %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div> <div class="col-xs-6 col-sm-5">{% if config.config_remote_login %}<span class="glyphicon glyphicon-ok"></span>{% else %}<span class="glyphicon glyphicon-remove"></span>{% endif %}</div>
</div> </div>
</div> </div>
<div class="btn btn-default"><a id="basic_config" href="{{url_for('web.configuration')}}">{{_('Basic Configuration')}}</a></div> <div class="btn btn-default"><a id="basic_config" href="{{url_for('admin.configuration')}}">{{_('Basic Configuration')}}</a></div>
<div class="btn btn-default"><a id="view_config" href="{{url_for('web.view_configuration')}}">{{_('UI Configuration')}}</a></div> <div class="btn btn-default"><a id="view_config" href="{{url_for('admin.view_configuration')}}">{{_('UI Configuration')}}</a></div>
</div> </div>
</div> </div>
......
...@@ -267,10 +267,10 @@ ...@@ -267,10 +267,10 @@
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button> <button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
{% if not origin %} {% if not origin %}
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('admin.admin') }}" class="btn btn-default">{{_('Back')}}</a>
{% endif %} {% endif %}
{% if success %} {% if success %}
<a href="{{ url_for('login') }}" name="login" class="btn btn-default">{{_('Login')}}</a> <a href="{{ url_for('web.login') }}" name="login" class="btn btn-default">{{_('Login')}}</a>
{% endif %} {% endif %}
</div> </div>
</form> </form>
......
...@@ -172,7 +172,7 @@ ...@@ -172,7 +172,7 @@
</div> </div>
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button> <button type="submit" name="submit" class="btn btn-default">{{_('Submit')}}</button>
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('admin.admin') }}" class="btn btn-default">{{_('Back')}}</a>
</div> </div>
</form> </form>
</div> </div>
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
</div> </div>
<button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save settings')}}</button> <button type="submit" name="submit" value="submit" class="btn btn-default">{{_('Save settings')}}</button>
<button type="submit" name="test" value="test" class="btn btn-default">{{_('Save settings and send Test E-Mail')}}</button> <button type="submit" name="test" value="test" class="btn btn-default">{{_('Save settings and send Test E-Mail')}}</button>
<a href="{{ url_for('admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('admin.admin') }}" id="back" class="btn btn-default">{{_('Back')}}</a>
</form> </form>
{% if g.allow_registration %} {% if g.allow_registration %}
<h2>{{_('Allowed domains for registering')}}</h2> <h2>{{_('Allowed domains for registering')}}</h2>
......
...@@ -6,10 +6,10 @@ ...@@ -6,10 +6,10 @@
href="{{request.script_root + request.full_path}}" href="{{request.script_root + request.full_path}}"
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/> type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
<link rel="start" <link rel="start"
href="{{url_for('feed_index')}}" href="{{url_for('opds.feed_index')}}"
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/> type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
<link rel="up" <link rel="up"
href="{{url_for('feed_index')}}" href="{{url_for('opds.feed_index')}}"
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/> type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
{% if pagination.has_prev %} {% if pagination.has_prev %}
<link rel="first" <link rel="first"
...@@ -28,9 +28,9 @@ ...@@ -28,9 +28,9 @@
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/> type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
{% endif %} {% endif %}
<link rel="search" <link rel="search"
href="{{url_for('feed_osd')}}" href="{{url_for('opds.feed_osd')}}"
type="application/opensearchdescription+xml"/> type="application/opensearchdescription+xml"/>
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/--> <!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('opds.feed_normal_search')}}?query={searchTerms}" rel="search"/-->
<title>{{instance}}</title> <title>{{instance}}</title>
<author> <author>
<name>{{instance}}</name> <name>{{instance}}</name>
...@@ -61,11 +61,11 @@ ...@@ -61,11 +61,11 @@
{% endfor %} {% endfor %}
{% if entry.comments[0] %}<summary>{{entry.comments[0].text|striptags}}</summary>{% endif %} {% if entry.comments[0] %}<summary>{{entry.comments[0].text|striptags}}</summary>{% endif %}
{% if entry.has_cover %} {% if entry.has_cover %}
<link type="image/jpeg" href="{{url_for('feed_get_cover', book_id=entry.id)}}" rel="http://opds-spec.org/image"/> <link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.id)}}" rel="http://opds-spec.org/image"/>
<link type="image/jpeg" href="{{url_for('feed_get_cover', book_id=entry.id)}}" rel="http://opds-spec.org/image/thumbnail"/> <link type="image/jpeg" href="{{url_for('opds.feed_get_cover', book_id=entry.id)}}" rel="http://opds-spec.org/image/thumbnail"/>
{% endif %} {% endif %}
{% for format in entry.data %} {% for format in entry.data %}
<link rel="http://opds-spec.org/acquisition" href="{{ url_for('get_opds_download_link', book_id=entry.id, book_format=format.format|lower)}}" <link rel="http://opds-spec.org/acquisition" href="{{ url_for('opds.get_opds_download_link', book_id=entry.id, book_format=format.format|lower)}}"
length="{{format.uncompressed_size}}" mtime="{{entry.atom_timestamp}}" type="{{format.format|lower|mimetype}}"/> length="{{format.uncompressed_size}}" mtime="{{entry.atom_timestamp}}" type="{{format.format|lower|mimetype}}"/>
{% endfor %} {% endfor %}
</entry> </entry>
......
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
<li><a id="top_tasks" href="{{url_for('web.get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span><span class="hidden-sm"> {{_('Tasks')}}</span></a></li> <li><a id="top_tasks" href="{{url_for('web.get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span><span class="hidden-sm"> {{_('Tasks')}}</span></a></li>
{% endif %} {% endif %}
{% if g.user.role_admin() %} {% if g.user.role_admin() %}
<li><a id="top_admin" href="{{url_for('web.admin')}}"><span class="glyphicon glyphicon-dashboard"></span><span class="hidden-sm"> {{_('Admin')}}</span></a></li> <li><a id="top_admin" href="{{url_for('admin.admin')}}"><span class="glyphicon glyphicon-dashboard"></span><span class="hidden-sm"> {{_('Admin')}}</span></a></li>
{% endif %} {% endif %}
<li><a id="top_user" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span><span class="hidden-sm"> {{g.user.nickname}}</span></a></li> <li><a id="top_user" href="{{url_for('web.profile')}}"><span class="glyphicon glyphicon-user"></span><span class="hidden-sm"> {{g.user.nickname}}</span></a></li>
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
...@@ -172,11 +172,11 @@ ...@@ -172,11 +172,11 @@
{% endfor %} {% endfor %}
<li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li> <li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %} {% for shelf in g.user.shelf %}
<li><a href="{{url_for('web.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li> <li><a href="{{url_for('shelf.show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
{% endfor %} {% endfor %}
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
<li id="nav_createshelf" class="create-shelf"><a href="{{url_for('web.create_shelf')}}">{{_('Create a Shelf')}}</a></li> <li id="nav_createshelf" class="create-shelf"><a href="{{url_for('shelf.create_shelf')}}">{{_('Create a Shelf')}}</a></li>
<li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('web.stats')}}"><span class="glyphicon glyphicon-info-sign"></span>{{_('About')}}</a></li> <li id="nav_about" {% if page == 'stat' %}class="active"{% endif %}><a href="{{url_for('about.stats')}}"><span class="glyphicon glyphicon-info-sign"></span>{{_('About')}}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
......
...@@ -35,18 +35,18 @@ ...@@ -35,18 +35,18 @@
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
</a> </a>
{% endif %} {% endif %}
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p> <p class="title">{{entry.title|shortentitle}}</p>
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('web.author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;
{% endif %} {% endif %}
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
<div class="discover"> <div class="discover">
<h2>{{title}}</h2> <h2>{{title}}</h2>
{% if g.user.role_download() %} {% if g.user.role_download() %}
<a id="shelf_down" href="{{ url_for('show_shelf_down', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a> <a id="shelf_down" href="{{ url_for('shelf.show_shelf_down', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
{% endif %} {% endif %}
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %} {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
<div id="delete_shelf" data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div> <div id="delete_shelf" data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
<a id="edit_shelf" href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a> <a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf') }} </a>
<a id="order_shelf" href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a> <a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="row"> <div class="row">
...@@ -17,21 +17,21 @@ ...@@ -17,21 +17,21 @@
{% for entry in entries %} {% for entry in entries %}
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}
</a> </a>
</div> </div>
<div class="meta"> <div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p> <p class="title">{{entry.title|shortentitle}}</p>
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a> <a href="{{url_for('web.author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %} {% if not loop.last %}
&amp; &amp;
{% endif %} {% endif %}
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
<div class="modal-body text-center"> <div class="modal-body text-center">
<span>{{_('Shelf will be lost for everybody and forever!')}}</span> <span>{{_('Shelf will be lost for everybody and forever!')}}</span>
<p></p> <p></p>
<a id="confirm" href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('Ok')}}</a> <a id="confirm" href="{{ url_for('shelf.delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('Ok')}}</a>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
</div> </div>
</div> </div>
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
{% endif %} {% endif %}
<button type="submit" class="btn btn-default" id="submit">{{_('Submit')}}</button> <button type="submit" class="btn btn-default" id="submit">{{_('Submit')}}</button>
{% if shelf.id != None %} {% if shelf.id != None %}
<a href="{{ url_for('show_shelf', shelf_id=shelf.id) }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('shelf.show_shelf', shelf_id=shelf.id) }}" class="btn btn-default">{{_('Back')}}</a>
{% endif %} {% endif %}
</form> </form>
</div> </div>
......
...@@ -35,6 +35,8 @@ import cli ...@@ -35,6 +35,8 @@ import cli
engine = create_engine('sqlite:///{0}'.format(cli.settingspath), echo=False) engine = create_engine('sqlite:///{0}'.format(cli.settingspath), echo=False)
Base = declarative_base() Base = declarative_base()
session = None
ROLE_USER = 0 ROLE_USER = 0
ROLE_ADMIN = 1 ROLE_ADMIN = 1
ROLE_DOWNLOAD = 2 ROLE_DOWNLOAD = 2
...@@ -849,22 +851,23 @@ def create_admin_user(): ...@@ -849,22 +851,23 @@ def create_admin_user():
except Exception: except Exception:
session.rollback() session.rollback()
def init_db():
# Open session for database connection # Open session for database connection
Session = sessionmaker() global session
Session.configure(bind=engine) Session = sessionmaker()
session = Session() Session.configure(bind=engine)
session = Session()
if not os.path.exists(cli.settingspath):
try: if not os.path.exists(cli.settingspath):
try:
Base.metadata.create_all(engine)
create_default_config()
create_admin_user()
create_anonymous_user()
except Exception:
raise
else:
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
create_default_config() migrate_Database()
create_admin_user() clean_database()
create_anonymous_user()
except Exception:
raise
else:
Base.metadata.create_all(engine)
migrate_Database()
clean_database()
...@@ -17,26 +17,25 @@ ...@@ -17,26 +17,25 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from cps import config, get_locale
import threading import threading
import zipfile import zipfile
import requests import requests
import re
import logging import logging
import server
import time import time
from io import BytesIO from io import BytesIO
import os import os
import sys import sys
import shutil import shutil
from cps import config
from ub import UPDATE_STABLE from ub import UPDATE_STABLE
from tempfile import gettempdir from tempfile import gettempdir
import datetime import datetime
import json import json
from flask_babel import gettext as _ from flask_babel import gettext as _
from babel.dates import format_datetime from babel.dates import format_datetime
import web
import server
def is_sha1(sha1): def is_sha1(sha1):
if len(sha1) != 40: if len(sha1) != 40:
...@@ -288,7 +287,7 @@ class Updater(threading.Thread): ...@@ -288,7 +287,7 @@ class Updater(threading.Thread):
update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parents.append( parents.append(
[ [
format_datetime(new_commit_date, format='short', locale=web.get_locale()), format_datetime(new_commit_date, format='short', locale=get_locale()),
update_data['message'], update_data['message'],
update_data['sha'] update_data['sha']
] ]
...@@ -318,7 +317,7 @@ class Updater(threading.Thread): ...@@ -318,7 +317,7 @@ class Updater(threading.Thread):
parent_commit_date = datetime.datetime.strptime( parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime( parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=web.get_locale()) parent_commit_date, format='short', locale=get_locale())
parents.append([parent_commit_date, parents.append([parent_commit_date,
parent_data['message'].replace('\r\n','<p>').replace('\n','<p>')]) parent_data['message'].replace('\r\n','<p>').replace('\n','<p>')])
...@@ -346,7 +345,7 @@ class Updater(threading.Thread): ...@@ -346,7 +345,7 @@ class Updater(threading.Thread):
commit['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz commit['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parents.append( parents.append(
[ [
format_datetime(new_commit_date, format='short', locale=web.get_locale()), format_datetime(new_commit_date, format='short', locale=get_locale()),
commit['message'], commit['message'],
commit['sha'] commit['sha']
] ]
...@@ -376,7 +375,7 @@ class Updater(threading.Thread): ...@@ -376,7 +375,7 @@ class Updater(threading.Thread):
parent_commit_date = datetime.datetime.strptime( parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime( parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=web.get_locale()) parent_commit_date, format='short', locale=get_locale())
parents.append([parent_commit_date, parent_data['message'], parent_data['sha']]) parents.append([parent_commit_date, parent_data['message'], parent_data['sha']])
parent_commit = parent_data['parents'][0] parent_commit = parent_data['parents'][0]
...@@ -510,6 +509,3 @@ class Updater(threading.Thread): ...@@ -510,6 +509,3 @@ class Updater(threading.Thread):
status['message'] = _(u'General error') status['message'] = _(u'General error')
return status, commit return status, commit
updater_thread = Updater()
...@@ -17,11 +17,14 @@ ...@@ -17,11 +17,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os # import os
from tempfile import gettempdir from tempfile import gettempdir
import hashlib import hashlib
from collections import namedtuple from collections import namedtuple
import book_formats import logging
import os
from flask_babel import gettext as _
import comic
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id, languages') BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id, languages')
...@@ -29,6 +32,158 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d ...@@ -29,6 +32,158 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d
:rtype: BookMeta :rtype: BookMeta
""" """
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2016-2019 lemmsh cervinko Kennyl matthazinski OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
try:
from lxml.etree import LXML_VERSION as lxmlversion
except ImportError:
lxmlversion = None
__author__ = 'lemmsh'
logger = logging.getLogger("book_formats")
try:
from wand.image import Image
from wand import version as ImageVersion
use_generic_pdf_cover = False
except (ImportError, RuntimeError) as e:
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
use_generic_pdf_cover = True
try:
from PyPDF2 import PdfFileReader
from PyPDF2 import __version__ as PyPdfVersion
use_pdf_meta = True
except ImportError as e:
logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e)
use_pdf_meta = False
try:
import epub
use_epub_meta = True
except ImportError as e:
logger.warning('cannot import epub, extracting epub metadata will not work: %s', e)
use_epub_meta = False
try:
import fb2
use_fb2_meta = True
except ImportError as e:
logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e)
use_fb2_meta = False
def process(tmp_file_path, original_file_name, original_file_extension):
meta = None
try:
if ".PDF" == original_file_extension.upper():
meta = pdf_meta(tmp_file_path, original_file_name, original_file_extension)
if ".EPUB" == original_file_extension.upper() and use_epub_meta is True:
meta = epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension)
if ".FB2" == original_file_extension.upper() and use_fb2_meta is True:
meta = fb2.get_fb2_info(tmp_file_path, original_file_extension)
if original_file_extension.upper() in ['.CBZ', '.CBT']:
meta = comic.get_comic_info(tmp_file_path, original_file_name, original_file_extension)
except Exception as ex:
logger.warning('cannot parse metadata, using default: %s', ex)
if meta and meta.title.strip() and meta.author.strip():
return meta
else:
return default_meta(tmp_file_path, original_file_name, original_file_extension)
def default_meta(tmp_file_path, original_file_name, original_file_extension):
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=original_file_name,
author=u"Unknown",
cover=None,
description="",
tags="",
series="",
series_id="",
languages="")
def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
if use_pdf_meta:
pdf = PdfFileReader(open(tmp_file_path, 'rb'))
doc_info = pdf.getDocumentInfo()
else:
doc_info = None
if doc_info is not None:
author = doc_info.author if doc_info.author else u"Unknown"
title = doc_info.title if doc_info.title else original_file_name
subject = doc_info.subject
else:
author = u"Unknown"
title = original_file_name
subject = ""
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title,
author=author,
cover=pdf_preview(tmp_file_path, original_file_name),
description=subject,
tags="",
series="",
series_id="",
languages="")
def pdf_preview(tmp_file_path, tmp_dir):
if use_generic_pdf_cover:
return None
else:
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
img.compression_quality = 88
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
def get_versions():
if not use_generic_pdf_cover:
IVersion = ImageVersion.MAGICK_VERSION
WVersion = ImageVersion.VERSION
else:
IVersion = _(u'not installed')
WVersion = _(u'not installed')
if use_pdf_meta:
PVersion='v'+PyPdfVersion
else:
PVersion=_(u'not installed')
if lxmlversion:
XVersion = 'v'+'.'.join(map(str, lxmlversion))
else:
XVersion = _(u'not installed')
return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion, 'Wand Version': WVersion}
def upload(uploadfile): def upload(uploadfile):
tmp_dir = os.path.join(gettempdir(), 'calibre_web') tmp_dir = os.path.join(gettempdir(), 'calibre_web')
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment