Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da64ceb23 | ||
|
|
3077b854d7 | ||
|
|
a8317d900b | ||
|
|
668bf3a15e | ||
|
|
481e52b503 | ||
|
|
bbb65ec804 | ||
|
|
4b7b646692 | ||
|
|
65cfb1ccbc | ||
|
|
9e9d7b3642 | ||
|
|
1294672809 | ||
|
|
857584a929 | ||
|
|
b4772d9b66 | ||
|
|
382cd9458f | ||
|
|
be7ac7e163 | ||
|
|
9f260128cf | ||
|
|
cdd38350fe | ||
|
|
aefed40a2f | ||
|
|
bd0071354c | ||
|
|
c6bf62a6eb | ||
|
|
95544ef885 | ||
|
|
fe4db16a7e | ||
|
|
9b127a9f97 | ||
|
|
9e4aeac16d | ||
|
|
9653308300 | ||
|
|
7671a1d5c8 | ||
|
|
bd6b5ac873 | ||
|
|
00dc60da79 | ||
|
|
708861bcd5 | ||
|
|
24a2c0a5cf | ||
|
|
1e1d3a7c81 | ||
|
|
2ebddcfee3 | ||
|
|
4517f5b0cb | ||
|
|
666015e867 | ||
|
|
9d5e9b28ae | ||
|
|
6f1e78b9a3 | ||
|
|
d5aa345da6 | ||
|
|
e060c62742 | ||
|
|
5695268d1b | ||
|
|
cea10d3945 | ||
|
|
fe21d15194 | ||
|
|
369d5059c3 | ||
|
|
56a9c62421 | ||
|
|
50d703e2d8 | ||
|
|
5aefc893de | ||
|
|
6e5d9d7657 | ||
|
|
1be7dfcdca | ||
|
|
e58eb8dac1 | ||
|
|
d02eb842c7 | ||
|
|
f3efef1f60 | ||
|
|
659481c83b | ||
|
|
bad4c01474 | ||
|
|
cd53d57516 | ||
|
|
ac54899415 | ||
|
|
abe46e1862 | ||
|
|
3be47d6e57 | ||
|
|
25f608d109 | ||
|
|
aca5324914 | ||
|
|
d8f9e2feb2 | ||
|
|
ed26d34961 | ||
|
|
50919d4721 | ||
|
|
5edde53fed | ||
|
|
1c15e10ac0 | ||
|
|
52be2ad4a2 | ||
|
|
43fdef5e53 | ||
|
|
b699796236 | ||
|
|
b82d03c12c | ||
|
|
b8eb557761 | ||
|
|
ba40c6693e | ||
|
|
e064a3ec2b | ||
|
|
f6561456f7 | ||
|
|
deb91996a8 | ||
|
|
cd1fe6dde0 | ||
|
|
3f6a466ca7 | ||
|
|
50f4fe6546 | ||
|
|
861277460d | ||
|
|
83babf88fc | ||
|
|
58735caff3 | ||
|
|
844eb6c379 | ||
|
|
1b8410e786 | ||
|
|
fed9eff7b8 | ||
|
|
bf5de95fdc | ||
|
|
2e25e797dd | ||
|
|
c1ac68e2ae | ||
|
|
623a92ebec | ||
|
|
707556a36d | ||
|
|
781ca7a3f3 | ||
|
|
9ebde57fc3 | ||
|
|
8dc1e9bfa4 | ||
|
|
e3db2796c9 | ||
|
|
8930426f2a | ||
|
|
c0a06eec46 | ||
|
|
86ef1d47e8 | ||
|
|
b835aa660a | ||
|
|
4d92d7da3a | ||
|
|
d1e6a85803 | ||
|
|
10e212fcde | ||
|
|
abcf9a1808 | ||
|
|
3946ef8f0d | ||
|
|
6a2868c68d | ||
|
|
ff39fac50f | ||
|
|
a35c635987 | ||
|
|
aa9fdd2ada | ||
|
|
e68a3f18fa | ||
|
|
ceec1051d5 | ||
|
|
702e96ddd6 | ||
|
|
53603f79bd | ||
|
|
250cafe814 | ||
|
|
f0a5225524 | ||
|
|
302679719d | ||
|
|
a1a8a0cf29 | ||
|
|
259ac94b93 | ||
|
|
a27314464a | ||
|
|
ec7803fa76 | ||
|
|
aa2d3d2b36 | ||
|
|
d5e9cdc5b7 | ||
|
|
0d247fef6a | ||
|
|
94da61c57e | ||
|
|
430ccd9ab1 | ||
|
|
557296b7be | ||
|
|
67836006c5 | ||
|
|
1833e8fdb4 | ||
|
|
1aca1b9fdd | ||
|
|
9fea5a55f4 | ||
|
|
1a0bf45c34 | ||
|
|
67874e07b6 | ||
|
|
ced0bcfe33 | ||
|
|
8c0aa79f78 | ||
|
|
e9552fedef | ||
|
|
59987ed359 |
@@ -19,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
|||||||
- full graphical setup
|
- full graphical setup
|
||||||
- User management with fine-grained per-user permissions
|
- User management with fine-grained per-user permissions
|
||||||
- Admin interface
|
- Admin interface
|
||||||
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, turkish, ukrainian
|
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
|
||||||
- OPDS feed for eBook reader apps
|
- OPDS feed for eBook reader apps
|
||||||
- Filter and search by titles, authors, tags, series and language
|
- Filter and search by titles, authors, tags, series and language
|
||||||
- Create a custom book collection (shelves)
|
- Create a custom book collection (shelves)
|
||||||
@@ -62,7 +62,7 @@ Please note that running the above install command can fail on some versions of
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
python 3.x+
|
python 3.5+
|
||||||
|
|
||||||
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
|
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
|
||||||
|
|
||||||
@@ -72,14 +72,7 @@ Optionally, to enable on-the-fly conversion from one ebook format to another whe
|
|||||||
|
|
||||||
## Docker Images
|
## Docker Images
|
||||||
|
|
||||||
Pre-built Docker images are available in these Docker Hub repositories:
|
A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team):
|
||||||
|
|
||||||
#### **Technosoft2000 - x64**
|
|
||||||
+ Docker Hub - [https://hub.docker.com/r/technosoft2000/calibre-web](https://hub.docker.com/r/technosoft2000/calibre-web)
|
|
||||||
+ Github - [https://github.com/Technosoft2000/docker-calibre-web](https://github.com/Technosoft2000/docker-calibre-web)
|
|
||||||
|
|
||||||
Includes the Calibre `ebook-convert` binary.
|
|
||||||
+ The "path to convertertool" should be set to `/opt/calibre/ebook-convert`
|
|
||||||
|
|
||||||
#### **LinuxServer - x64, armhf, aarch64**
|
#### **LinuxServer - x64, armhf, aarch64**
|
||||||
+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
|
+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
|
||||||
|
|||||||
+24
@@ -3,3 +3,27 @@
|
|||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please report security issues to ozzie.fernandez.isaacs@googlemail.com
|
Please report security issues to ozzie.fernandez.isaacs@googlemail.com
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
To receive fixes for security vulnerabilities it is required to always upgrade to the latest version of Calibre-Web. See https://github.com/janeczku/calibre-web/releases/latest for the latest release.
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
| Fixed in | Description |CVE number |
|
||||||
|
| ---------- |---------|---------|
|
||||||
|
| 3rd July 2018 | Guest access acts as a backdoor||
|
||||||
|
| V 0.6.7 |Hardcoded secret key for sessions |CVE-2020-12627 |
|
||||||
|
| V 0.6.13|Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
||||||
|
| V 0.6.13|Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo||
|
||||||
|
| V 0.6.13|JavaScript could get executed in the description field. Thanks to @ranjit-git ||
|
||||||
|
| V 0.6.13|JavaScript could get executed in a custom column of type "comment" field ||
|
||||||
|
| V 0.6.13|JavaScript could get executed after converting a book to another format with a title containing javascript code||
|
||||||
|
| V 0.6.13|JavaScript could get executed after converting a book to another format with a username containing javascript code||
|
||||||
|
| V 0.6.13|JavaScript could get executed in the description series, categories or publishers title||
|
||||||
|
| V 0.6.13|JavaScript could get executed in the shelf title||
|
||||||
|
| V 0.6.13|Login with the old session cookie after logout. Thanks to @ibarrionuevo||
|
||||||
|
| V 0.6.14|CSRF was possible. Thanks to @mik317 ||
|
||||||
|
| V 0.6.14|Cross-Site Scripting vulnerability on typeahead inputs. Thanks to @notdodo||
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,18 +17,13 @@
|
|||||||
# 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 __future__ import absolute_import, division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
# Insert local directories into path
|
# Insert local directories into path
|
||||||
if sys.version_info < (3, 0):
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))))
|
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
|
||||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))), 'vendor'))
|
|
||||||
else:
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
|
|
||||||
|
|
||||||
|
|
||||||
from cps import create_app
|
from cps import create_app
|
||||||
@@ -42,13 +37,14 @@ from cps.admin import admi
|
|||||||
from cps.gdrive import gdrive
|
from cps.gdrive import gdrive
|
||||||
from cps.editbooks import editbook
|
from cps.editbooks import editbook
|
||||||
from cps.remotelogin import remotelogin
|
from cps.remotelogin import remotelogin
|
||||||
|
from cps.search_metadata import meta
|
||||||
from cps.error_handler import init_errorhandler
|
from cps.error_handler import init_errorhandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cps.kobo import kobo, get_kobo_activated
|
from cps.kobo import kobo, get_kobo_activated
|
||||||
from cps.kobo_auth import kobo_auth
|
from cps.kobo_auth import kobo_auth
|
||||||
kobo_available = get_kobo_activated()
|
kobo_available = get_kobo_activated()
|
||||||
except ImportError:
|
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
||||||
kobo_available = False
|
kobo_available = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -70,6 +66,7 @@ def main():
|
|||||||
app.register_blueprint(shelf)
|
app.register_blueprint(shelf)
|
||||||
app.register_blueprint(admi)
|
app.register_blueprint(admi)
|
||||||
app.register_blueprint(remotelogin)
|
app.register_blueprint(remotelogin)
|
||||||
|
app.register_blueprint(meta)
|
||||||
app.register_blueprint(gdrive)
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
if kobo_available:
|
if kobo_available:
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# -*- 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, GammaC0de, vuolter
|
||||||
|
#
|
||||||
|
# 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_login import LoginManager
|
||||||
|
from flask import session
|
||||||
|
|
||||||
|
|
||||||
|
class MyLoginManager(LoginManager):
|
||||||
|
def _session_protection_failed(self):
|
||||||
|
sess = session._get_current_object()
|
||||||
|
ident = self._session_identifier_generator()
|
||||||
|
if(sess and not (len(sess) == 1 and sess.get('csrf_token', None))) and ident != sess.get('_id', None):
|
||||||
|
return super(). _session_protection_failed()
|
||||||
|
return False
|
||||||
+26
-9
@@ -20,7 +20,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@@ -29,7 +28,7 @@ from babel import Locale as LC
|
|||||||
from babel import negotiate_locale
|
from babel import negotiate_locale
|
||||||
from babel.core import UnknownLocaleError
|
from babel.core import UnknownLocaleError
|
||||||
from flask import Flask, request, g
|
from flask import Flask, request, g
|
||||||
from flask_login import LoginManager
|
from .MyLoginManager import MyLoginManager
|
||||||
from flask_babel import Babel
|
from flask_babel import Babel
|
||||||
from flask_principal import Principal
|
from flask_principal import Principal
|
||||||
|
|
||||||
@@ -43,6 +42,12 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
lxml_present = False
|
lxml_present = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
|
wtf_present = True
|
||||||
|
except ImportError:
|
||||||
|
wtf_present = False
|
||||||
|
|
||||||
mimetypes.init()
|
mimetypes.init()
|
||||||
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
||||||
mimetypes.add_type('application/epub+zip', '.epub')
|
mimetypes.add_type('application/epub+zip', '.epub')
|
||||||
@@ -61,20 +66,29 @@ mimetypes.add_type('application/mp4', '.m4a')
|
|||||||
mimetypes.add_type('application/mp4', '.m4b')
|
mimetypes.add_type('application/mp4', '.m4b')
|
||||||
mimetypes.add_type('application/ogg', '.ogg')
|
mimetypes.add_type('application/ogg', '.ogg')
|
||||||
mimetypes.add_type('application/ogg', '.oga')
|
mimetypes.add_type('application/ogg', '.oga')
|
||||||
|
mimetypes.add_type('text/css', '.css')
|
||||||
|
mimetypes.add_type('text/javascript; charset=UTF-8', '.js')
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE='Lax',
|
SESSION_COOKIE_SAMESITE='Lax',
|
||||||
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
|
||||||
|
WTF_CSRF_SSL_STRICT=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
lm = LoginManager()
|
lm = MyLoginManager()
|
||||||
lm.login_view = 'web.login'
|
lm.login_view = 'web.login'
|
||||||
lm.anonymous_user = ub.Anonymous
|
lm.anonymous_user = ub.Anonymous
|
||||||
lm.session_protection = 'strong'
|
lm.session_protection = 'strong'
|
||||||
|
|
||||||
|
if wtf_present:
|
||||||
|
csrf = CSRFProtect()
|
||||||
|
csrf.init_app(app)
|
||||||
|
else:
|
||||||
|
csrf = None
|
||||||
|
|
||||||
ub.init_db(cli.settingspath)
|
ub.init_db(cli.settingspath)
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
config = config_sql.load_configuration(ub.session)
|
config = config_sql.load_configuration(ub.session)
|
||||||
@@ -100,17 +114,20 @@ def create_app():
|
|||||||
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
||||||
print(
|
print(
|
||||||
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, please update your installation to Python3 ***')
|
||||||
|
web_server.stop(True)
|
||||||
sys.exit(5)
|
sys.exit(5)
|
||||||
if not lxml_present:
|
if not lxml_present:
|
||||||
log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
log.info('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||||
print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
print('*** "lxml" is needed for calibre-web to run. Please install it using pip: "pip install lxml" ***')
|
||||||
|
web_server.stop(True)
|
||||||
sys.exit(6)
|
sys.exit(6)
|
||||||
|
if not wtf_present:
|
||||||
|
log.info('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
|
||||||
|
print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
|
||||||
|
web_server.stop(True)
|
||||||
|
sys.exit(7)
|
||||||
|
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
# For python2 convert path to unicode
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
app.static_folder = app.static_folder.decode('utf-8')
|
|
||||||
app.root_path = app.root_path.decode('utf-8')
|
|
||||||
app.instance_path = app.instance_path.decode('utf-8')
|
|
||||||
|
|
||||||
if os.environ.get('FLASK_DEBUG'):
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
cache_buster.init_cache_busting(app)
|
cache_buster.init_cache_busting(app)
|
||||||
@@ -139,7 +156,6 @@ def create_app():
|
|||||||
def get_locale():
|
def get_locale():
|
||||||
# if a user is logged in, use the locale from the user settings
|
# if a user is logged in, use the locale from the user settings
|
||||||
user = getattr(g, 'user', None)
|
user = getattr(g, 'user', None)
|
||||||
# user = None
|
|
||||||
if user is not None and hasattr(user, "locale"):
|
if user is not None and hasattr(user, "locale"):
|
||||||
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
||||||
return user.locale
|
return user.locale
|
||||||
@@ -160,6 +176,7 @@ def get_timezone():
|
|||||||
user = getattr(g, 'user', None)
|
user = getattr(g, 'user', None)
|
||||||
return user.timezone if user else None
|
return user.timezone if user else None
|
||||||
|
|
||||||
|
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
updater_thread.start()
|
updater_thread.start()
|
||||||
|
|||||||
+13
-9
@@ -20,7 +20,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -29,6 +28,10 @@ from collections import OrderedDict
|
|||||||
import babel, pytz, requests, sqlalchemy
|
import babel, pytz, requests, sqlalchemy
|
||||||
import werkzeug, flask, flask_login, flask_principal, jinja2
|
import werkzeug, flask, flask_login, flask_principal, jinja2
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
try:
|
||||||
|
from flask_wtf import __version__ as flaskwtf_version
|
||||||
|
except ImportError:
|
||||||
|
flaskwtf_version = _(u'not installed')
|
||||||
|
|
||||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
@@ -75,6 +78,7 @@ _VERSIONS = OrderedDict(
|
|||||||
Flask=flask.__version__,
|
Flask=flask.__version__,
|
||||||
Flask_Login=flask_loginVersion,
|
Flask_Login=flask_loginVersion,
|
||||||
Flask_Principal=flask_principal.__version__,
|
Flask_Principal=flask_principal.__version__,
|
||||||
|
Flask_WTF=flaskwtf_version,
|
||||||
Werkzeug=werkzeug.__version__,
|
Werkzeug=werkzeug.__version__,
|
||||||
Babel=babel.__version__,
|
Babel=babel.__version__,
|
||||||
Jinja2=jinja2.__version__,
|
Jinja2=jinja2.__version__,
|
||||||
@@ -84,14 +88,14 @@ _VERSIONS = OrderedDict(
|
|||||||
SQLite=sqlite3.sqlite_version,
|
SQLite=sqlite3.sqlite_version,
|
||||||
iso639=isoLanguages.__version__,
|
iso639=isoLanguages.__version__,
|
||||||
pytz=pytz.__version__,
|
pytz=pytz.__version__,
|
||||||
Unidecode = unidecode_version,
|
Unidecode=unidecode_version,
|
||||||
Scholarly = scholarly_version,
|
Scholarly=scholarly_version,
|
||||||
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None,
|
Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
|
||||||
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
|
python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
|
||||||
Goodreads = u'installed' if bool(services.goodreads_support) else None,
|
Goodreads=u'installed' if bool(services.goodreads_support) else None,
|
||||||
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||||
flask_dance = flask_danceVersion,
|
flask_dance=flask_danceVersion,
|
||||||
greenlet = greenlet_Version
|
greenlet=greenlet_Version
|
||||||
)
|
)
|
||||||
_VERSIONS.update(uploader.get_versions())
|
_VERSIONS.update(uploader.get_versions())
|
||||||
|
|
||||||
|
|||||||
+24
-10
@@ -20,7 +20,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import base64
|
import base64
|
||||||
@@ -41,7 +40,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
|||||||
from sqlalchemy.sql.expression import func, or_, text
|
from sqlalchemy.sql.expression import func, or_, text
|
||||||
|
|
||||||
from . import constants, logger, helper, services
|
from . import constants, logger, helper, services
|
||||||
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
|
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils, kobo_sync_status
|
||||||
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
|
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
|
||||||
valid_email, check_username
|
valid_email, check_username
|
||||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||||
@@ -146,7 +145,7 @@ def shutdown():
|
|||||||
else:
|
else:
|
||||||
showtext['text'] = _(u'Performing shutdown of server, please close window')
|
showtext['text'] = _(u'Performing shutdown of server, please close window')
|
||||||
# stop gevent/tornado server
|
# stop gevent/tornado server
|
||||||
web_server.stop(task==0)
|
web_server.stop(task == 0)
|
||||||
return json.dumps(showtext)
|
return json.dumps(showtext)
|
||||||
|
|
||||||
if task == 2:
|
if task == 2:
|
||||||
@@ -236,8 +235,12 @@ def view_configuration():
|
|||||||
.filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all()
|
.filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all()
|
||||||
restrict_columns = calibre_db.session.query(db.Custom_Columns)\
|
restrict_columns = calibre_db.session.query(db.Custom_Columns)\
|
||||||
.filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all()
|
.filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all()
|
||||||
|
languages = calibre_db.speaking_language()
|
||||||
|
translations = [LC('en')] + babel.list_translations()
|
||||||
return render_title_template("config_view_edit.html", conf=config, readColumns=read_column,
|
return render_title_template("config_view_edit.html", conf=config, readColumns=read_column,
|
||||||
restrictColumns=restrict_columns,
|
restrictColumns=restrict_columns,
|
||||||
|
languages=languages,
|
||||||
|
translations=translations,
|
||||||
title=_(u"UI Configuration"), page="uiconfig")
|
title=_(u"UI Configuration"), page="uiconfig")
|
||||||
|
|
||||||
@admi.route("/admin/usertable")
|
@admi.route("/admin/usertable")
|
||||||
@@ -515,16 +518,12 @@ def check_valid_restricted_column(column):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/viewconfig", methods=["POST"])
|
@admi.route("/admin/viewconfig", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_view_configuration():
|
def update_view_configuration():
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
|
|
||||||
# _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y)
|
|
||||||
# _config_int = lambda x: config.set_from_dictionary(to_save, x, int)
|
|
||||||
|
|
||||||
_config_string(to_save, "config_calibre_web_title")
|
_config_string(to_save, "config_calibre_web_title")
|
||||||
_config_string(to_save, "config_columns_to_ignore")
|
_config_string(to_save, "config_columns_to_ignore")
|
||||||
if _config_string(to_save, "config_title_regex"):
|
if _config_string(to_save, "config_title_regex"):
|
||||||
@@ -546,6 +545,8 @@ def update_view_configuration():
|
|||||||
_config_int(to_save, "config_random_books")
|
_config_int(to_save, "config_random_books")
|
||||||
_config_int(to_save, "config_books_per_page")
|
_config_int(to_save, "config_books_per_page")
|
||||||
_config_int(to_save, "config_authors_max")
|
_config_int(to_save, "config_authors_max")
|
||||||
|
_config_string(to_save, "config_default_language")
|
||||||
|
_config_string(to_save, "config_default_locale")
|
||||||
|
|
||||||
|
|
||||||
config.config_default_role = constants.selected_roles(to_save)
|
config.config_default_role = constants.selected_roles(to_save)
|
||||||
@@ -1198,7 +1199,7 @@ def _configuration_update_helper():
|
|||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
try:
|
try:
|
||||||
reboot_required |= _config_int(to_save, "config_port")
|
reboot_required |= _config_int(to_save, "config_port")
|
||||||
|
reboot_required |= _config_string(to_save, "config_trustedhosts")
|
||||||
reboot_required |= _config_string(to_save, "config_keyfile")
|
reboot_required |= _config_string(to_save, "config_keyfile")
|
||||||
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
|
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
|
||||||
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'))
|
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'))
|
||||||
@@ -1208,6 +1209,7 @@ def _configuration_update_helper():
|
|||||||
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'))
|
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'))
|
||||||
|
|
||||||
_config_checkbox_int(to_save, "config_uploading")
|
_config_checkbox_int(to_save, "config_uploading")
|
||||||
|
_config_checkbox_int(to_save, "config_unicode_filename")
|
||||||
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
||||||
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
|
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
|
||||||
and config.config_login_type == constants.LOGIN_LDAP)
|
and config.config_login_type == constants.LOGIN_LDAP)
|
||||||
@@ -1310,6 +1312,8 @@ def _db_configuration_result(error_flash=None, gdrive_error=None):
|
|||||||
log.error(error_flash)
|
log.error(error_flash)
|
||||||
config.load()
|
config.load()
|
||||||
flash(error_flash, category="error")
|
flash(error_flash, category="error")
|
||||||
|
elif request.method == "POST" and not gdrive_error:
|
||||||
|
flash(_("Database Settings updated"), category="success")
|
||||||
|
|
||||||
return render_title_template("config_db.html",
|
return render_title_template("config_db.html",
|
||||||
config=config,
|
config=config,
|
||||||
@@ -1428,8 +1432,13 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
|||||||
else:
|
else:
|
||||||
content.sidebar_view &= ~constants.DETAIL_RANDOM
|
content.sidebar_view &= ~constants.DETAIL_RANDOM
|
||||||
|
|
||||||
|
old_state = content.kobo_only_shelves_sync
|
||||||
content.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0
|
content.kobo_only_shelves_sync = int(to_save.get("kobo_only_shelves_sync") == "on") or 0
|
||||||
|
# 1 -> 0: nothing has to be done
|
||||||
|
# 0 -> 1: all synced books have to be added to archived books, + currently synced shelfs
|
||||||
|
# which don't have to be synced have to be removed (added to Shelf archive)
|
||||||
|
if old_state == 0 and content.kobo_only_shelves_sync == 1:
|
||||||
|
kobo_sync_status.update_on_sync_shelfs(content.id)
|
||||||
if to_save.get("default_language"):
|
if to_save.get("default_language"):
|
||||||
content.default_language = to_save["default_language"]
|
content.default_language = to_save["default_language"]
|
||||||
if to_save.get("locale"):
|
if to_save.get("locale"):
|
||||||
@@ -1485,6 +1494,8 @@ def new_user():
|
|||||||
else:
|
else:
|
||||||
content.role = config.config_default_role
|
content.role = config.config_default_role
|
||||||
content.sidebar_view = config.config_default_show
|
content.sidebar_view = config.config_default_show
|
||||||
|
content.locale = config.config_default_locale
|
||||||
|
content.default_language = config.config_default_language
|
||||||
return render_title_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"), page="newuser",
|
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||||
@@ -1682,7 +1693,8 @@ def get_updater_status():
|
|||||||
"9": _(u'Update failed:') + u' ' + _(u'Connection error'),
|
"9": _(u'Update failed:') + u' ' + _(u'Connection error'),
|
||||||
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
|
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
|
||||||
"11": _(u'Update failed:') + u' ' + _(u'General error'),
|
"11": _(u'Update failed:') + u' ' + _(u'General error'),
|
||||||
"12": _(u'Update failed:') + u' ' + _(u'Update File Could Not be Saved in Temp Dir')
|
"12": _(u'Update failed:') + u' ' + _(u'Update file could not be saved in temp dir'),
|
||||||
|
"13": _(u'Update failed:') + u' ' + _(u'Files could not be replaced during update')
|
||||||
}
|
}
|
||||||
status['text'] = text
|
status['text'] = text
|
||||||
updater_thread.status = 0
|
updater_thread.status = 0
|
||||||
@@ -1736,6 +1748,8 @@ def ldap_import_create_user(user, user_data):
|
|||||||
content.password = '' # dummy password which will be replaced by ldap one
|
content.password = '' # dummy password which will be replaced by ldap one
|
||||||
content.email = useremail
|
content.email = useremail
|
||||||
content.kindle_mail = kindlemail
|
content.kindle_mail = kindlemail
|
||||||
|
content.default_language = config.config_default_language
|
||||||
|
content.locale = config.config_default_locale
|
||||||
content.role = config.config_default_role
|
content.role = config.config_default_role
|
||||||
content.sidebar_view = config.config_default_show
|
content.sidebar_view = config.config_default_show
|
||||||
content.allowed_tags = config.config_allowed_tags
|
content.allowed_tags = config.config_allowed_tags
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
|
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
|
||||||
# Uses query strings so CSS font files are found without having to resort to absolute URLs
|
# Uses query strings so CSS font files are found without having to resort to absolute URLs
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
|||||||
-14
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
@@ -48,19 +47,6 @@ parser.add_argument('-s', metavar='user:pass', help='Sets specific username to n
|
|||||||
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
|
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
if args.p:
|
|
||||||
args.p = args.p.decode('utf-8')
|
|
||||||
if args.g:
|
|
||||||
args.g = args.g.decode('utf-8')
|
|
||||||
if args.k:
|
|
||||||
args.k = args.k.decode('utf-8')
|
|
||||||
if args.c:
|
|
||||||
args.c = args.c.decode('utf-8')
|
|
||||||
if args.s:
|
|
||||||
args.s = args.s.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
|
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
|
||||||
gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db")
|
gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db")
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from . import logger, isoLanguages
|
from . import logger, isoLanguages
|
||||||
|
|||||||
+4
-7
@@ -16,8 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
@@ -68,7 +66,7 @@ class _Settings(_Base):
|
|||||||
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
|
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
|
||||||
config_certfile = Column(String)
|
config_certfile = Column(String)
|
||||||
config_keyfile = Column(String)
|
config_keyfile = Column(String)
|
||||||
|
config_trustedhosts = Column(String,default='')
|
||||||
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
||||||
config_books_per_page = Column(Integer, default=60)
|
config_books_per_page = Column(Integer, default=60)
|
||||||
config_random_books = Column(Integer, default=4)
|
config_random_books = Column(Integer, default=4)
|
||||||
@@ -91,6 +89,8 @@ class _Settings(_Base):
|
|||||||
|
|
||||||
config_default_role = Column(SmallInteger, default=0)
|
config_default_role = Column(SmallInteger, default=0)
|
||||||
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
|
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
|
||||||
|
config_default_language = Column(String(3), default="all")
|
||||||
|
config_default_locale = Column(String(2), default="en")
|
||||||
config_columns_to_ignore = Column(String)
|
config_columns_to_ignore = Column(String)
|
||||||
|
|
||||||
config_denied_tags = Column(String, default="")
|
config_denied_tags = Column(String, default="")
|
||||||
@@ -133,6 +133,7 @@ class _Settings(_Base):
|
|||||||
config_calibre = Column(String)
|
config_calibre = Column(String)
|
||||||
config_rarfile_location = Column(String, default=None)
|
config_rarfile_location = Column(String, default=None)
|
||||||
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
|
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
|
||||||
|
config_unicode_filename =Column(Boolean, default=False)
|
||||||
|
|
||||||
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
||||||
|
|
||||||
@@ -360,10 +361,6 @@ def _migrate_table(session, orm_class):
|
|||||||
session.query(column).first()
|
session.query(column).first()
|
||||||
except OperationalError as err:
|
except OperationalError as err:
|
||||||
log.debug("%s: %s", column_name, err.args[0])
|
log.debug("%s: %s", column_name, err.args[0])
|
||||||
if column.default is not None:
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
if isinstance(column.default.arg, unicode):
|
|
||||||
column.default.arg = column.default.arg.encode('utf-8')
|
|
||||||
if column.default is None:
|
if column.default is None:
|
||||||
column_default = ""
|
column_default = ""
|
||||||
else:
|
else:
|
||||||
|
|||||||
+2
-8
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
@@ -31,12 +30,7 @@ HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file
|
|||||||
UPDATER_AVAILABLE = True
|
UPDATER_AVAILABLE = True
|
||||||
|
|
||||||
# Base dir is parent of current file, necessary if called from different folder
|
# Base dir is parent of current file, necessary if called from different folder
|
||||||
if sys.version_info < (3, 0):
|
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),os.pardir))
|
||||||
BASE_DIR = os.path.abspath(os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)),os.pardir)).decode('utf-8')
|
|
||||||
else:
|
|
||||||
BASE_DIR = os.path.abspath(os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)),os.pardir))
|
|
||||||
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
||||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
||||||
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
|
||||||
@@ -157,7 +151,7 @@ def selected_roles(dictionary):
|
|||||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||||
'series_id, languages, publisher')
|
'series_id, languages, publisher')
|
||||||
|
|
||||||
STABLE_VERSION = {'version': '0.6.13'}
|
STABLE_VERSION = {'version': '0.6.14'}
|
||||||
|
|
||||||
NIGHTLY_VERSION = {}
|
NIGHTLY_VERSION = {}
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -387,8 +386,6 @@ class Custom_Columns(Base):
|
|||||||
|
|
||||||
def get_display_dict(self):
|
def get_display_dict(self):
|
||||||
display_dict = ast.literal_eval(self.display)
|
display_dict = ast.literal_eval(self.display)
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']]
|
|
||||||
return display_dict
|
return display_dict
|
||||||
|
|
||||||
|
|
||||||
@@ -398,7 +395,7 @@ class AlchemyEncoder(json.JSONEncoder):
|
|||||||
if isinstance(o.__class__, DeclarativeMeta):
|
if isinstance(o.__class__, DeclarativeMeta):
|
||||||
# an SQLAlchemy class
|
# an SQLAlchemy class
|
||||||
fields = {}
|
fields = {}
|
||||||
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x!="password"]:
|
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x != "password"]:
|
||||||
if field == 'books':
|
if field == 'books':
|
||||||
continue
|
continue
|
||||||
data = o.__getattribute__(field)
|
data = o.__getattribute__(field)
|
||||||
@@ -407,8 +404,11 @@ class AlchemyEncoder(json.JSONEncoder):
|
|||||||
data = data.replace("'", "\'")
|
data = data.replace("'", "\'")
|
||||||
elif isinstance(data, InstrumentedList):
|
elif isinstance(data, InstrumentedList):
|
||||||
el = list()
|
el = list()
|
||||||
|
# ele = None
|
||||||
for ele in data:
|
for ele in data:
|
||||||
if ele.get:
|
if hasattr(ele, 'value'): # converter for custom_column values
|
||||||
|
el.append(str(ele.value))
|
||||||
|
elif ele.get:
|
||||||
el.append(ele.get())
|
el.append(ele.get())
|
||||||
else:
|
else:
|
||||||
el.append(json.dumps(ele, cls=AlchemyEncoder))
|
el.append(json.dumps(ele, cls=AlchemyEncoder))
|
||||||
@@ -740,6 +740,7 @@ class CalibreDB():
|
|||||||
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
entries = self.session.query(database).filter(tag_filter). \
|
entries = self.session.query(database).filter(tag_filter). \
|
||||||
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
filter(func.lower(database.name).ilike("%" + query + "%")).all()
|
||||||
|
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
|
||||||
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries])
|
||||||
return json_dumps
|
return json_dumps
|
||||||
|
|
||||||
@@ -805,11 +806,16 @@ class CalibreDB():
|
|||||||
.filter(self.common_filters()) \
|
.filter(self.common_filters()) \
|
||||||
.group_by(text('books_languages_link.lang_code')).all()
|
.group_by(text('books_languages_link.lang_code')).all()
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
try:
|
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||||
cur_l = LC.parse(lang.lang_code)
|
#try:
|
||||||
lang.name = cur_l.get_language_name(get_locale())
|
# if lang.lang_code.lower() == "und":
|
||||||
except UnknownLocaleError:
|
# lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||||
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
|
# # lang.name = _("Undetermined")
|
||||||
|
# else:
|
||||||
|
# cur_l = LC.parse(lang.lang_code)
|
||||||
|
# lang.name = cur_l.get_language_name(get_locale())
|
||||||
|
#except UnknownLocaleError:
|
||||||
|
# lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
|
||||||
return languages
|
return languages
|
||||||
|
|
||||||
def update_title_sort(self, config, conn=None):
|
def update_title_sort(self, config, conn=None):
|
||||||
|
|||||||
+40
-28
@@ -20,7 +20,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
@@ -40,14 +39,12 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
have_scholar = False
|
have_scholar = False
|
||||||
|
|
||||||
from babel import Locale as LC
|
|
||||||
from babel.core import UnknownLocaleError
|
|
||||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from sqlalchemy.exc import OperationalError, IntegrityError
|
from sqlalchemy.exc import OperationalError, IntegrityError
|
||||||
from sqlite3 import OperationalError as sqliteOperationalError
|
from sqlite3 import OperationalError as sqliteOperationalError
|
||||||
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper
|
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
|
||||||
from . import config, get_locale, ub, db
|
from . import config, get_locale, ub, db
|
||||||
from . import calibre_db
|
from . import calibre_db
|
||||||
from .services.worker import WorkerThread
|
from .services.worker import WorkerThread
|
||||||
@@ -462,6 +459,7 @@ def edit_book_series_index(series_index, book):
|
|||||||
modif_date = True
|
modif_date = True
|
||||||
return modif_date
|
return modif_date
|
||||||
|
|
||||||
|
|
||||||
# Handle book comments/description
|
# Handle book comments/description
|
||||||
def edit_book_comments(comments, book):
|
def edit_book_comments(comments, book):
|
||||||
modif_date = False
|
modif_date = False
|
||||||
@@ -576,10 +574,19 @@ def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
|
|||||||
getattr(book, cc_string).append(new_cc)
|
getattr(book, cc_string).append(new_cc)
|
||||||
return changed, to_save
|
return changed, to_save
|
||||||
|
|
||||||
|
def edit_single_cc_data(book_id, book, column_id, to_save):
|
||||||
|
cc = (calibre_db.session.query(db.Custom_Columns)
|
||||||
|
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions))
|
||||||
|
.filter(db.Custom_Columns.id == column_id)
|
||||||
|
.all())
|
||||||
|
return edit_cc_data(book_id, book, to_save, cc)
|
||||||
|
|
||||||
def edit_cc_data(book_id, book, to_save):
|
def edit_all_cc_data(book_id, book, to_save):
|
||||||
changed = False
|
|
||||||
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
|
return edit_cc_data(book_id, book, to_save, cc)
|
||||||
|
|
||||||
|
def edit_cc_data(book_id, book, to_save, cc):
|
||||||
|
changed = False
|
||||||
for c in cc:
|
for c in cc:
|
||||||
cc_string = "custom_column_" + str(c.id)
|
cc_string = "custom_column_" + str(c.id)
|
||||||
if not c.is_multiple:
|
if not c.is_multiple:
|
||||||
@@ -814,7 +821,7 @@ def edit_book(book_id):
|
|||||||
# handle book ratings
|
# handle book ratings
|
||||||
modif_date |= edit_book_ratings(to_save, book)
|
modif_date |= edit_book_ratings(to_save, book)
|
||||||
# handle cc data
|
# handle cc data
|
||||||
modif_date |= edit_cc_data(book_id, book, to_save)
|
modif_date |= edit_all_cc_data(book_id, book, to_save)
|
||||||
|
|
||||||
if to_save["pubdate"]:
|
if to_save["pubdate"]:
|
||||||
try:
|
try:
|
||||||
@@ -826,6 +833,8 @@ def edit_book(book_id):
|
|||||||
|
|
||||||
if modif_date:
|
if modif_date:
|
||||||
book.last_modified = datetime.utcnow()
|
book.last_modified = datetime.utcnow()
|
||||||
|
kobo_sync_status.remove_synced_book(edited_books_id)
|
||||||
|
|
||||||
calibre_db.session.merge(book)
|
calibre_db.session.merge(book)
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
@@ -1080,23 +1089,18 @@ def convert_bookformat(book_id):
|
|||||||
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
||||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
|
|
||||||
@editbook.route("/scholarsearch/<query>",methods=['GET'])
|
@editbook.route("/ajax/getcustomenum/<int:c_id>")
|
||||||
@login_required_if_no_ano
|
@login_required
|
||||||
@edit_required
|
def table_get_custom_enum(c_id):
|
||||||
def scholar_search(query):
|
ret = list()
|
||||||
if have_scholar:
|
cc = (calibre_db.session.query(db.Custom_Columns)
|
||||||
scholar_gen = scholarly.search_pubs(' '.join(query.split('+')))
|
.filter(db.Custom_Columns.id == c_id)
|
||||||
i=0
|
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).one_or_none())
|
||||||
result = []
|
ret.append({'value': "", 'text': ""})
|
||||||
for publication in scholar_gen:
|
for idx, en in enumerate(cc.get_display_dict()['enum_values']):
|
||||||
del publication['source']
|
ret.append({'value': en, 'text': en})
|
||||||
result.append(publication)
|
return json.dumps(ret)
|
||||||
i+=1
|
|
||||||
if(i>=10):
|
|
||||||
break
|
|
||||||
return Response(json.dumps(result),mimetype='application/json')
|
|
||||||
else:
|
|
||||||
return "[]"
|
|
||||||
|
|
||||||
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@@ -1131,10 +1135,7 @@ def edit_list_book(param):
|
|||||||
else:
|
else:
|
||||||
lang_names = list()
|
lang_names = list()
|
||||||
for lang in book.languages:
|
for lang in book.languages:
|
||||||
try:
|
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
|
||||||
lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale()))
|
|
||||||
except UnknownLocaleError:
|
|
||||||
lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name))
|
|
||||||
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
|
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
|
||||||
mimetype='application/json')
|
mimetype='application/json')
|
||||||
elif param =='author_sort':
|
elif param =='author_sort':
|
||||||
@@ -1151,12 +1152,23 @@ def edit_list_book(param):
|
|||||||
book.sort = vals['value']
|
book.sort = vals['value']
|
||||||
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
|
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
|
||||||
mimetype='application/json')
|
mimetype='application/json')
|
||||||
|
elif param =='comments':
|
||||||
|
edit_book_comments(vals['value'], book)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': book.comments[0].text}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='authors':
|
elif param =='authors':
|
||||||
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
|
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
|
||||||
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
|
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
|
||||||
ret = Response(json.dumps({'success': True,
|
ret = Response(json.dumps({'success': True,
|
||||||
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
|
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
|
||||||
mimetype='application/json')
|
mimetype='application/json')
|
||||||
|
elif param.startswith("custom_column_"):
|
||||||
|
new_val = dict()
|
||||||
|
new_val[param] = vals['value']
|
||||||
|
edit_single_cc_data(book.id, book, param[14:], new_val)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': vals['value']}),
|
||||||
|
mimetype='application/json')
|
||||||
|
|
||||||
book.last_modified = datetime.utcnow()
|
book.last_modified = datetime.utcnow()
|
||||||
try:
|
try:
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from .constants import BookMeta
|
from .constants import BookMeta
|
||||||
|
|||||||
+38
-40
@@ -20,9 +20,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/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -34,7 +32,7 @@ from flask import Blueprint, flash, request, redirect, url_for, abort
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
from . import logger, gdriveutils, config, ub, calibre_db
|
from . import logger, gdriveutils, config, ub, calibre_db, csrf
|
||||||
from .admin import admin_required
|
from .admin import admin_required
|
||||||
|
|
||||||
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||||
@@ -117,44 +115,44 @@ def revoke_watch_gdrive():
|
|||||||
config.save()
|
config.save()
|
||||||
return redirect(url_for('admin.db_configuration'))
|
return redirect(url_for('admin.db_configuration'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
@csrf.exempt
|
||||||
|
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
||||||
|
def on_received_watch_confirmation():
|
||||||
|
if not config.config_google_drive_watch_changes_response:
|
||||||
|
return ''
|
||||||
|
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
||||||
|
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
||||||
|
or not request.data:
|
||||||
|
return ''
|
||||||
|
|
||||||
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
|
log.debug('%r', request.headers)
|
||||||
def on_received_watch_confirmation():
|
log.debug('%r', request.data)
|
||||||
if not config.config_google_drive_watch_changes_response:
|
log.info('Change received from gdrive')
|
||||||
return ''
|
|
||||||
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
|
|
||||||
or request.headers.get('X-Goog-Resource-State') != 'change' \
|
|
||||||
or not request.data:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
log.debug('%r', request.headers)
|
try:
|
||||||
log.debug('%r', request.data)
|
j = json.loads(request.data)
|
||||||
log.info('Change received from gdrive')
|
log.info('Getting change details')
|
||||||
|
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
|
||||||
try:
|
log.debug('%r', response)
|
||||||
j = json.loads(request.data)
|
if response:
|
||||||
log.info('Getting change details')
|
|
||||||
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
|
|
||||||
log.debug('%r', response)
|
|
||||||
if response:
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
|
|
||||||
else:
|
|
||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
||||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||||
if not os.path.isdir(tmp_dir):
|
if not os.path.isdir(tmp_dir):
|
||||||
os.mkdir(tmp_dir)
|
os.mkdir(tmp_dir)
|
||||||
|
|
||||||
log.info('Database file updated')
|
log.info('Database file updated')
|
||||||
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
|
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
|
||||||
log.info('Backing up existing and downloading updated metadata.db')
|
log.info('Backing up existing and downloading updated metadata.db')
|
||||||
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
|
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
|
||||||
log.info('Setting up new DB')
|
log.info('Setting up new DB')
|
||||||
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
||||||
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
||||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log.debug_or_exception(ex)
|
log.debug_or_exception(ex)
|
||||||
return ''
|
return ''
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
|||||||
+7
-18
@@ -17,7 +17,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
@@ -234,16 +233,14 @@ def get_valid_filename(value, replace_whitespace=True):
|
|||||||
value = value[:-1]+u'_'
|
value = value[:-1]+u'_'
|
||||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||||
if use_unidecode:
|
if use_unidecode:
|
||||||
value = (unidecode.unidecode(value))
|
if not config.config_unicode_filename:
|
||||||
|
value = (unidecode.unidecode(value))
|
||||||
else:
|
else:
|
||||||
value = value.replace(u'§', u'SS')
|
value = value.replace(u'§', u'SS')
|
||||||
value = value.replace(u'ß', u'ss')
|
value = value.replace(u'ß', u'ss')
|
||||||
value = unicodedata.normalize('NFKD', value)
|
value = unicodedata.normalize('NFKD', value)
|
||||||
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
|
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
|
||||||
if isinstance(value, str): # Python3 str, Python2 unicode
|
value = re_slugify.sub('', value)
|
||||||
value = re_slugify.sub('', value)
|
|
||||||
else:
|
|
||||||
value = unicode(re_slugify.sub('', value))
|
|
||||||
if replace_whitespace:
|
if replace_whitespace:
|
||||||
# *+:\"/<>? are replaced by _
|
# *+:\"/<>? are replaced by _
|
||||||
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
||||||
@@ -252,10 +249,7 @@ def get_valid_filename(value, replace_whitespace=True):
|
|||||||
value = value[:128].strip()
|
value = value[:128].strip()
|
||||||
if not value:
|
if not value:
|
||||||
raise ValueError("Filename cannot be empty")
|
raise ValueError("Filename cannot be empty")
|
||||||
if sys.version_info.major == 3:
|
return value
|
||||||
return value
|
|
||||||
else:
|
|
||||||
return value.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
def split_authors(values):
|
def split_authors(values):
|
||||||
@@ -495,10 +489,7 @@ def reset_password(user_id):
|
|||||||
def generate_random_password():
|
def generate_random_password():
|
||||||
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
||||||
passlen = 8
|
passlen = 8
|
||||||
if sys.version_info < (3, 0):
|
return "".join(s[c % len(s)] for c in os.urandom(passlen))
|
||||||
return "".join(s[ord(c) % len(s)] for c in os.urandom(passlen))
|
|
||||||
else:
|
|
||||||
return "".join(s[c % len(s)] for c in os.urandom(passlen))
|
|
||||||
|
|
||||||
|
|
||||||
def uniq(inpt):
|
def uniq(inpt):
|
||||||
@@ -712,8 +703,6 @@ def check_unrar(unrarLocation):
|
|||||||
return _('Unrar binary file not found')
|
return _('Unrar binary file not found')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
unrarLocation = unrarLocation.encode(sys.getfilesystemencoding())
|
|
||||||
unrarLocation = [unrarLocation]
|
unrarLocation = [unrarLocation]
|
||||||
value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware')
|
value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware')
|
||||||
if value:
|
if value:
|
||||||
@@ -842,8 +831,8 @@ def get_download_link(book_id, book_format, client):
|
|||||||
ub.update_download(book_id, int(current_user.id))
|
ub.update_download(book_id, int(current_user.id))
|
||||||
file_name = book.title
|
file_name = book.title
|
||||||
if len(book.authors) > 0:
|
if len(book.authors) > 0:
|
||||||
file_name = book.authors[0].name + '_' + file_name
|
file_name = file_name + ' - ' + book.authors[0].name
|
||||||
file_name = get_valid_filename(file_name)
|
file_name = get_valid_filename(file_name, replace_whitespace=False)
|
||||||
headers = Headers()
|
headers = Headers()
|
||||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||||
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
|
|
||||||
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
|
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+373
-2
@@ -6,8 +6,6 @@
|
|||||||
|
|
||||||
# pylint: disable=too-many-lines,bad-continuation
|
# pylint: disable=too-many-lines,bad-continuation
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# This file is autogenerated, do NOT add, change, or delete ANY string
|
# This file is autogenerated, do NOT add, change, or delete ANY string
|
||||||
# If you need help or assistance for adding a new language, please contact the project team
|
# If you need help or assistance for adding a new language, please contact the project team
|
||||||
|
|
||||||
@@ -7435,6 +7433,379 @@ LANGUAGE_NAMES = {
|
|||||||
"zxx": "No linguistic content",
|
"zxx": "No linguistic content",
|
||||||
"zza": "扎扎其语"
|
"zza": "扎扎其语"
|
||||||
},
|
},
|
||||||
|
"zh_Hant_TW": {
|
||||||
|
"abk": "Abkhazian",
|
||||||
|
"ace": "亞齊語",
|
||||||
|
"ach": "Acoli",
|
||||||
|
"ada": "Adangme",
|
||||||
|
"ady": "Adyghe",
|
||||||
|
"aar": "Afar",
|
||||||
|
"afh": "Afrihili",
|
||||||
|
"afr": "南非荷蘭文",
|
||||||
|
"ain": "愛奴語 ( 日本 )",
|
||||||
|
"aka": "Akan",
|
||||||
|
"akk": "Akkadian",
|
||||||
|
"sqi": "阿爾巴尼亞文",
|
||||||
|
"ale": "Aleut",
|
||||||
|
"amh": "Amharic",
|
||||||
|
"anp": "Angika",
|
||||||
|
"ara": "阿拉伯",
|
||||||
|
"arg": "Aragonese",
|
||||||
|
"arp": "Arapaho",
|
||||||
|
"arw": "Arawak",
|
||||||
|
"hye": "Armenian",
|
||||||
|
"asm": "Assamese",
|
||||||
|
"ast": "Asturian",
|
||||||
|
"ava": "Avaric",
|
||||||
|
"ave": "Avestan",
|
||||||
|
"awa": "Awadhi",
|
||||||
|
"aym": "Aymara",
|
||||||
|
"aze": "亞塞拜疆文",
|
||||||
|
"ban": "Balinese",
|
||||||
|
"bal": "Baluchi",
|
||||||
|
"bam": "Bambara",
|
||||||
|
"bas": "Basa (Cameroon)",
|
||||||
|
"bak": "Bashkir",
|
||||||
|
"eus": "巴斯克文",
|
||||||
|
"bej": "Beja",
|
||||||
|
"bel": "白俄羅斯文",
|
||||||
|
"bem": "Bemba (Zambia)",
|
||||||
|
"ben": "Bengali",
|
||||||
|
"bho": "Bhojpuri",
|
||||||
|
"bik": "Bikol",
|
||||||
|
"byn": "Bilin",
|
||||||
|
"bin": "Bini",
|
||||||
|
"bis": "Bislama",
|
||||||
|
"zbl": "布利斯符號",
|
||||||
|
"bos": "Bosnian",
|
||||||
|
"bra": "Braj",
|
||||||
|
"bre": "布列塔尼文",
|
||||||
|
"bug": "Buginese",
|
||||||
|
"bul": "保加利亞文",
|
||||||
|
"bua": "Buriat",
|
||||||
|
"mya": "Burmese",
|
||||||
|
"cad": "Caddo",
|
||||||
|
"cat": "加泰隆文",
|
||||||
|
"ceb": "Cebuano",
|
||||||
|
"chg": "Chagatai",
|
||||||
|
"cha": "Chamorro",
|
||||||
|
"che": "Chechen",
|
||||||
|
"chr": "Cherokee",
|
||||||
|
"chy": "Cheyenne",
|
||||||
|
"chb": "Chibcha",
|
||||||
|
"zho": "中文",
|
||||||
|
"chn": "Chinook jargon",
|
||||||
|
"chp": "Chipewyan",
|
||||||
|
"cho": "Choctaw",
|
||||||
|
"chk": "Chuukese",
|
||||||
|
"chv": "Chuvash",
|
||||||
|
"cop": "Coptic",
|
||||||
|
"cor": "Cornish",
|
||||||
|
"cos": "Corsican",
|
||||||
|
"cre": "Cree",
|
||||||
|
"mus": "Creek",
|
||||||
|
"hrv": "克羅地亞文",
|
||||||
|
"ces": "捷克文",
|
||||||
|
"dak": "Dakota",
|
||||||
|
"dan": "丹麥文",
|
||||||
|
"dar": "Dargwa",
|
||||||
|
"del": "Delaware",
|
||||||
|
"div": "Dhivehi",
|
||||||
|
"din": "Dinka",
|
||||||
|
"doi": "Dogri (macrolanguage)",
|
||||||
|
"dgr": "Dogrib",
|
||||||
|
"dua": "Duala",
|
||||||
|
"nld": "荷蘭文",
|
||||||
|
"dyu": "Dyula",
|
||||||
|
"dzo": "Dzongkha",
|
||||||
|
"efi": "Efik",
|
||||||
|
"egy": "Egyptian (Ancient)",
|
||||||
|
"eka": "Ekajuk",
|
||||||
|
"elx": "Elamite",
|
||||||
|
"eng": "英文",
|
||||||
|
"myv": "Erzya",
|
||||||
|
"epo": "世界語",
|
||||||
|
"est": "愛沙尼亞文",
|
||||||
|
"ewe": "Ewe",
|
||||||
|
"ewo": "Ewondo",
|
||||||
|
"fan": "Fang (Equatorial Guinea)",
|
||||||
|
"fat": "Fanti",
|
||||||
|
"fao": "法羅文",
|
||||||
|
"fij": "Fijian",
|
||||||
|
"fil": "Filipino",
|
||||||
|
"fin": "芬蘭文",
|
||||||
|
"fon": "Fon",
|
||||||
|
"fra": "法文",
|
||||||
|
"fur": "Friulian",
|
||||||
|
"ful": "Fulah",
|
||||||
|
"gaa": "Ga",
|
||||||
|
"glg": "Galician",
|
||||||
|
"lug": "Ganda",
|
||||||
|
"gay": "Gayo",
|
||||||
|
"gba": "Gbaya (Central African Republic)",
|
||||||
|
"gez": "Geez",
|
||||||
|
"kat": "Georgian",
|
||||||
|
"deu": "德文",
|
||||||
|
"gil": "Gilbertese",
|
||||||
|
"gon": "Gondi",
|
||||||
|
"gor": "Gorontalo",
|
||||||
|
"got": "Gothic",
|
||||||
|
"grb": "Grebo",
|
||||||
|
"grn": "Guarani",
|
||||||
|
"guj": "古吉拉特語",
|
||||||
|
"gwi": "Gwichʼin",
|
||||||
|
"hai": "Haida",
|
||||||
|
"hau": "Hausa",
|
||||||
|
"haw": "Hawaiian",
|
||||||
|
"heb": "希伯來",
|
||||||
|
"her": "Herero",
|
||||||
|
"hil": "Hiligaynon",
|
||||||
|
"hin": "Hindi",
|
||||||
|
"hmo": "Hiri Motu",
|
||||||
|
"hit": "Hittite",
|
||||||
|
"hmn": "Hmong",
|
||||||
|
"hun": "匈牙利文",
|
||||||
|
"hup": "Hupa",
|
||||||
|
"iba": "Iban",
|
||||||
|
"isl": "冰島文",
|
||||||
|
"ido": "Ido",
|
||||||
|
"ibo": "Igbo",
|
||||||
|
"ilo": "Iloko",
|
||||||
|
"ind": "印尼文",
|
||||||
|
"inh": "Ingush",
|
||||||
|
"ina": "Interlingua (International Auxiliary Language Association)",
|
||||||
|
"ile": "Interlingue",
|
||||||
|
"iku": "Inuktitut",
|
||||||
|
"ipk": "Inupiaq",
|
||||||
|
"gle": "愛爾蘭文",
|
||||||
|
"ita": "意大利文",
|
||||||
|
"jpn": "日本",
|
||||||
|
"jav": "Javanese",
|
||||||
|
"jrb": "Judeo-Arabic",
|
||||||
|
"jpr": "Judeo-Persian",
|
||||||
|
"kbd": "Kabardian",
|
||||||
|
"kab": "Kabyle",
|
||||||
|
"kac": "Kachin",
|
||||||
|
"kal": "Kalaallisut",
|
||||||
|
"xal": "Kalmyk",
|
||||||
|
"kam": "Kamba (Kenya)",
|
||||||
|
"kan": "Kannada",
|
||||||
|
"kau": "Kanuri",
|
||||||
|
"kaa": "Kara-Kalpak",
|
||||||
|
"krc": "Karachay-Balkar",
|
||||||
|
"krl": "Karelian",
|
||||||
|
"kas": "Kashmiri",
|
||||||
|
"csb": "Kashubian",
|
||||||
|
"kaw": "Kawi",
|
||||||
|
"kaz": "Kazakh",
|
||||||
|
"kha": "Khasi",
|
||||||
|
"kho": "Khotanese",
|
||||||
|
"kik": "Kikuyu",
|
||||||
|
"kmb": "Kimbundu",
|
||||||
|
"kin": "Kinyarwanda",
|
||||||
|
"kir": "Kirghiz",
|
||||||
|
"tlh": "Klingon",
|
||||||
|
"kom": "Komi",
|
||||||
|
"kon": "Kongo",
|
||||||
|
"kok": "Konkani (macrolanguage)",
|
||||||
|
"kor": "韓國",
|
||||||
|
"kos": "Kosraean",
|
||||||
|
"kpe": "Kpelle",
|
||||||
|
"kua": "Kuanyama",
|
||||||
|
"kum": "Kumyk",
|
||||||
|
"kur": "Kurdish",
|
||||||
|
"kru": "Kurukh",
|
||||||
|
"kut": "Kutenai",
|
||||||
|
"lad": "Ladino",
|
||||||
|
"lah": "Lahnda",
|
||||||
|
"lam": "Lamba",
|
||||||
|
"lao": "Lao",
|
||||||
|
"lat": "Latin",
|
||||||
|
"lav": "拉脫維亞文",
|
||||||
|
"lez": "Lezghian",
|
||||||
|
"lim": "Limburgan",
|
||||||
|
"lin": "Lingala",
|
||||||
|
"lit": "立陶宛文",
|
||||||
|
"jbo": "Lojban",
|
||||||
|
"loz": "Lozi",
|
||||||
|
"lub": "Luba-Katanga",
|
||||||
|
"lua": "Luba-Lulua",
|
||||||
|
"lui": "Luiseno",
|
||||||
|
"smj": "Lule Sami",
|
||||||
|
"lun": "Lunda",
|
||||||
|
"luo": "Luo (Kenya and Tanzania)",
|
||||||
|
"lus": "Lushai",
|
||||||
|
"ltz": "Luxembourgish",
|
||||||
|
"mkd": "馬其頓文",
|
||||||
|
"mad": "Madurese",
|
||||||
|
"mag": "Magahi",
|
||||||
|
"mai": "Maithili",
|
||||||
|
"mak": "Makasar",
|
||||||
|
"mlg": "Malagasy",
|
||||||
|
"msa": "Malay (macrolanguage)",
|
||||||
|
"mal": "Malayalam",
|
||||||
|
"mlt": "Maltese",
|
||||||
|
"mnc": "Manchu",
|
||||||
|
"mdr": "Mandar",
|
||||||
|
"man": "Mandingo",
|
||||||
|
"mni": "Manipuri",
|
||||||
|
"glv": "Manx",
|
||||||
|
"mri": "Maori",
|
||||||
|
"arn": "Mapudungun",
|
||||||
|
"mar": "Marathi",
|
||||||
|
"chm": "Mari (Russia)",
|
||||||
|
"mah": "Marshallese",
|
||||||
|
"mwr": "Marwari",
|
||||||
|
"mas": "Masai",
|
||||||
|
"men": "Mende (Sierra Leone)",
|
||||||
|
"mic": "Mi'kmaq",
|
||||||
|
"min": "Minangkabau",
|
||||||
|
"mwl": "Mirandese",
|
||||||
|
"moh": "Mohawk",
|
||||||
|
"mdf": "Moksha",
|
||||||
|
"lol": "Mongo",
|
||||||
|
"mon": "Mongolian",
|
||||||
|
"mos": "Mossi",
|
||||||
|
"mul": "Multiple languages",
|
||||||
|
"nqo": "N'Ko",
|
||||||
|
"nau": "Nauru",
|
||||||
|
"nav": "Navajo",
|
||||||
|
"ndo": "Ndonga",
|
||||||
|
"nap": "Neapolitan",
|
||||||
|
"nia": "Nias",
|
||||||
|
"niu": "Niuean",
|
||||||
|
"zxx": "非語言內容",
|
||||||
|
"nog": "Nogai",
|
||||||
|
"nor": "挪威文",
|
||||||
|
"nob": "Norwegian Bokmål",
|
||||||
|
"nno": "Norwegian Nynorsk",
|
||||||
|
"nym": "Nyamwezi",
|
||||||
|
"nya": "Nyanja",
|
||||||
|
"nyn": "Nyankole",
|
||||||
|
"nyo": "Nyoro",
|
||||||
|
"nzi": "Nzima",
|
||||||
|
"oci": "Occitan (post 1500)",
|
||||||
|
"oji": "Ojibwa",
|
||||||
|
"orm": "Oromo",
|
||||||
|
"osa": "Osage",
|
||||||
|
"oss": "Ossetian",
|
||||||
|
"pal": "Pahlavi",
|
||||||
|
"pau": "Palauan",
|
||||||
|
"pli": "Pali",
|
||||||
|
"pam": "Pampanga",
|
||||||
|
"pag": "Pangasinan",
|
||||||
|
"pan": "Panjabi",
|
||||||
|
"pap": "Papiamento",
|
||||||
|
"fas": "Persian",
|
||||||
|
"phn": "Phoenician",
|
||||||
|
"pon": "Pohnpeian",
|
||||||
|
"pol": "波蘭文",
|
||||||
|
"por": "葡萄牙文",
|
||||||
|
"pus": "Pashto",
|
||||||
|
"que": "Quechua",
|
||||||
|
"raj": "Rajasthani",
|
||||||
|
"rap": "Rapanui",
|
||||||
|
"ron": "羅馬尼亞文",
|
||||||
|
"roh": "Romansh",
|
||||||
|
"rom": "Romany",
|
||||||
|
"run": "Rundi",
|
||||||
|
"rus": "俄羅斯文",
|
||||||
|
"smo": "Samoan",
|
||||||
|
"sad": "Sandawe",
|
||||||
|
"sag": "Sango",
|
||||||
|
"san": "Sanskrit",
|
||||||
|
"sat": "Santali",
|
||||||
|
"srd": "Sardinian",
|
||||||
|
"sas": "Sasak",
|
||||||
|
"sco": "Scots",
|
||||||
|
"sel": "Selkup",
|
||||||
|
"srp": "塞爾維亞文",
|
||||||
|
"srr": "Serer",
|
||||||
|
"shn": "Shan",
|
||||||
|
"sna": "Shona",
|
||||||
|
"scn": "Sicilian",
|
||||||
|
"sid": "Sidamo",
|
||||||
|
"bla": "Siksika",
|
||||||
|
"snd": "Sindhi",
|
||||||
|
"sin": "Sinhala",
|
||||||
|
"den": "Slave (Athapascan)",
|
||||||
|
"slk": "斯洛伐克文",
|
||||||
|
"slv": "斯洛文尼亞文",
|
||||||
|
"sog": "Sogdian",
|
||||||
|
"som": "Somali",
|
||||||
|
"snk": "Soninke",
|
||||||
|
"spa": "西班牙文",
|
||||||
|
"srn": "Sranan Tongo",
|
||||||
|
"suk": "Sukuma",
|
||||||
|
"sux": "Sumerian",
|
||||||
|
"sun": "Sundanese",
|
||||||
|
"sus": "Susu",
|
||||||
|
"swa": "Swahili (macrolanguage)",
|
||||||
|
"ssw": "Swati",
|
||||||
|
"swe": "瑞典文",
|
||||||
|
"syr": "Syriac",
|
||||||
|
"tgl": "Tagalog",
|
||||||
|
"tah": "Tahitian",
|
||||||
|
"tgk": "Tajik",
|
||||||
|
"tmh": "Tamashek",
|
||||||
|
"tam": "Tamil",
|
||||||
|
"tat": "Tatar",
|
||||||
|
"tel": "Telugu",
|
||||||
|
"ter": "Tereno",
|
||||||
|
"tet": "Tetum",
|
||||||
|
"tha": "泰國",
|
||||||
|
"bod": "Tibetan",
|
||||||
|
"tig": "Tigre",
|
||||||
|
"tir": "Tigrinya",
|
||||||
|
"tem": "Timne",
|
||||||
|
"tiv": "Tiv",
|
||||||
|
"tli": "Tlingit",
|
||||||
|
"tpi": "Tok Pisin",
|
||||||
|
"tkl": "Tokelau",
|
||||||
|
"tog": "Tonga (Nyasa)",
|
||||||
|
"ton": "Tonga (Tonga Islands)",
|
||||||
|
"tsi": "Tsimshian",
|
||||||
|
"tso": "Tsonga",
|
||||||
|
"tsn": "Tswana",
|
||||||
|
"tum": "Tumbuka",
|
||||||
|
"tur": "土耳其",
|
||||||
|
"tuk": "Turkmen",
|
||||||
|
"tvl": "Tuvalu",
|
||||||
|
"tyv": "Tuvinian",
|
||||||
|
"twi": "Twi",
|
||||||
|
"udm": "Udmurt",
|
||||||
|
"uga": "Ugaritic",
|
||||||
|
"uig": "Uighur",
|
||||||
|
"ukr": "烏克蘭語",
|
||||||
|
"umb": "Umbundu",
|
||||||
|
"mis": "Uncoded languages",
|
||||||
|
"und": "Undetermined",
|
||||||
|
"urd": "烏爾都語",
|
||||||
|
"uzb": "烏茲別克語",
|
||||||
|
"vai": "Vai",
|
||||||
|
"ven": "文達語",
|
||||||
|
"vie": "越南語",
|
||||||
|
"vol": "沃拉普克語",
|
||||||
|
"vot": "Votic",
|
||||||
|
"wln": "瓦隆語",
|
||||||
|
"war": "Waray (Philippines)",
|
||||||
|
"was": "Washo",
|
||||||
|
"cym": "威爾斯語",
|
||||||
|
"wal": "Wolaytta",
|
||||||
|
"wol": "渥魯夫語",
|
||||||
|
"xho": " 科薩語",
|
||||||
|
"sah": "Yakut",
|
||||||
|
"yao": "Yao",
|
||||||
|
"yap": "雅浦語",
|
||||||
|
"yid": "意第緒語",
|
||||||
|
"yor": "約魯巴語",
|
||||||
|
"zap": "薩波特克語",
|
||||||
|
"zza": "扎扎其語",
|
||||||
|
"zen": " 哲納加語",
|
||||||
|
"zha": " 壯語",
|
||||||
|
"zul": "祖魯語",
|
||||||
|
"zun": "祖尼語"
|
||||||
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"aar": "Afar",
|
"aar": "Afar",
|
||||||
"abk": "Abkhazian",
|
"abk": "Abkhazian",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
# custom jinja filters
|
# custom jinja filters
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import datetime
|
import datetime
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|||||||
+41
-30
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
@@ -47,7 +46,8 @@ from sqlalchemy.exc import StatementError
|
|||||||
from sqlalchemy.sql import select
|
from sqlalchemy.sql import select
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub
|
|
||||||
|
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
|
||||||
from .constants import sqlalchemy_version2
|
from .constants import sqlalchemy_version2
|
||||||
from .helper import get_download_link
|
from .helper import get_download_link
|
||||||
from .services import SyncToken as SyncToken
|
from .services import SyncToken as SyncToken
|
||||||
@@ -170,9 +170,12 @@ def HandleSyncRequest():
|
|||||||
ub.ArchivedBook.is_archived)
|
ub.ArchivedBook.is_archived)
|
||||||
changed_entries = (changed_entries
|
changed_entries = (changed_entries
|
||||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||||
.filter(or_(db.Books.last_modified > sync_token.books_last_modified,
|
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
|
||||||
ub.BookShelf.date_added > sync_token.books_last_modified))
|
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS)).filter(calibre_db.common_filters())
|
ub.KoboSyncedBooks.book_id == None))
|
||||||
|
.filter(ub.BookShelf.date_added > sync_token.books_last_modified)
|
||||||
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
|
.filter(calibre_db.common_filters(allow_show_archived=True))
|
||||||
.order_by(db.Books.id)
|
.order_by(db.Books.id)
|
||||||
.order_by(ub.ArchivedBook.last_modified)
|
.order_by(ub.ArchivedBook.last_modified)
|
||||||
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
|
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
|
||||||
@@ -189,16 +192,16 @@ def HandleSyncRequest():
|
|||||||
ub.ArchivedBook.last_modified,
|
ub.ArchivedBook.last_modified,
|
||||||
ub.ArchivedBook.is_archived)
|
ub.ArchivedBook.is_archived)
|
||||||
changed_entries = (changed_entries
|
changed_entries = (changed_entries
|
||||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||||
.filter(db.Books.last_modified > sync_token.books_last_modified)
|
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
|
||||||
.filter(calibre_db.common_filters())
|
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
ub.KoboSyncedBooks.book_id == None))
|
||||||
.order_by(db.Books.last_modified)
|
.filter(calibre_db.common_filters())
|
||||||
.order_by(db.Books.id)
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
|
.order_by(db.Books.last_modified)
|
||||||
|
.order_by(db.Books.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
if sync_token.books_last_id > -1:
|
|
||||||
changed_entries = changed_entries.filter(db.Books.id > sync_token.books_last_id)
|
|
||||||
|
|
||||||
reading_states_in_new_entitlements = []
|
reading_states_in_new_entitlements = []
|
||||||
if sqlalchemy_version2:
|
if sqlalchemy_version2:
|
||||||
@@ -206,6 +209,7 @@ def HandleSyncRequest():
|
|||||||
else:
|
else:
|
||||||
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
||||||
for book in books:
|
for book in books:
|
||||||
|
kobo_sync_status.add_synced_books(book.Books.id)
|
||||||
formats = [data.format for data in book.Books.data]
|
formats = [data.format for data in book.Books.data]
|
||||||
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||||
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||||
@@ -263,11 +267,10 @@ def HandleSyncRequest():
|
|||||||
entries = calibre_db.session.execute(changed_entries).all()
|
entries = calibre_db.session.execute(changed_entries).all()
|
||||||
book_count = len(entries)
|
book_count = len(entries)
|
||||||
else:
|
else:
|
||||||
entries = changed_entries.all()
|
|
||||||
book_count = changed_entries.count()
|
book_count = changed_entries.count()
|
||||||
# last entry:
|
# last entry:
|
||||||
books_last_id = entries[-1].Books.id or -1 if book_count else -1
|
cont_sync = bool(book_count)
|
||||||
|
log.debug("Remaining books to Sync: {}".format(book_count))
|
||||||
# generate reading state data
|
# generate reading state data
|
||||||
changed_reading_states = ub.session.query(ub.KoboReadingState)
|
changed_reading_states = ub.session.query(ub.KoboReadingState)
|
||||||
|
|
||||||
@@ -278,18 +281,18 @@ def HandleSyncRequest():
|
|||||||
.filter(current_user.id == ub.Shelf.user_id)\
|
.filter(current_user.id == ub.Shelf.user_id)\
|
||||||
.filter(ub.Shelf.kobo_sync,
|
.filter(ub.Shelf.kobo_sync,
|
||||||
or_(
|
or_(
|
||||||
func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified,
|
ub.KoboReadingState.last_modified > sync_token.reading_state_last_modified,
|
||||||
func.datetime(ub.BookShelf.date_added) > sync_token.books_last_modified
|
func.datetime(ub.BookShelf.date_added) > sync_token.books_last_modified
|
||||||
)).distinct()
|
)).distinct()
|
||||||
else:
|
else:
|
||||||
changed_reading_states = changed_reading_states.filter(
|
changed_reading_states = changed_reading_states.filter(
|
||||||
func.datetime(ub.KoboReadingState.last_modified) > sync_token.reading_state_last_modified)
|
ub.KoboReadingState.last_modified > sync_token.reading_state_last_modified)
|
||||||
|
|
||||||
changed_reading_states = changed_reading_states.filter(
|
changed_reading_states = changed_reading_states.filter(
|
||||||
and_(ub.KoboReadingState.user_id == current_user.id,
|
and_(ub.KoboReadingState.user_id == current_user.id,
|
||||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))
|
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))
|
||||||
|
cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT)
|
||||||
for kobo_reading_state in changed_reading_states.all():
|
for kobo_reading_state in changed_reading_states.limit(SYNC_ITEM_LIMIT).all():
|
||||||
book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
|
book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
|
||||||
if book:
|
if book:
|
||||||
sync_results.append({
|
sync_results.append({
|
||||||
@@ -305,9 +308,9 @@ def HandleSyncRequest():
|
|||||||
sync_token.books_last_modified = new_books_last_modified
|
sync_token.books_last_modified = new_books_last_modified
|
||||||
sync_token.archive_last_modified = new_archived_last_modified
|
sync_token.archive_last_modified = new_archived_last_modified
|
||||||
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
||||||
sync_token.books_last_id = books_last_id
|
# sync_token.books_last_id = books_last_id
|
||||||
|
|
||||||
return generate_sync_response(sync_token, sync_results, book_count)
|
return generate_sync_response(sync_token, sync_results, cont_sync)
|
||||||
|
|
||||||
|
|
||||||
def generate_sync_response(sync_token, sync_results, set_cont=False):
|
def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||||
@@ -330,7 +333,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
|||||||
extra_headers["x-kobo-sync"] = "continue"
|
extra_headers["x-kobo-sync"] = "continue"
|
||||||
sync_token.to_headers(extra_headers)
|
sync_token.to_headers(extra_headers)
|
||||||
|
|
||||||
log.debug("Kobo Sync Content: {}".format(sync_results))
|
# log.debug("Kobo Sync Content: {}".format(sync_results))
|
||||||
response = make_response(jsonify(sync_results), extra_headers)
|
response = make_response(jsonify(sync_results), extra_headers)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -483,10 +486,7 @@ def get_metadata(book):
|
|||||||
metadata.update(get_author(book))
|
metadata.update(get_author(book))
|
||||||
|
|
||||||
if get_series(book):
|
if get_series(book):
|
||||||
if sys.version_info < (3, 0):
|
name = get_series(book)
|
||||||
name = get_series(book).encode("utf-8")
|
|
||||||
else:
|
|
||||||
name = get_series(book)
|
|
||||||
metadata["Series"] = {
|
metadata["Series"] = {
|
||||||
"Name": get_series(book),
|
"Name": get_series(book),
|
||||||
"Number": get_seriesindex(book), # ToDo Check int() ?
|
"Number": get_seriesindex(book), # ToDo Check int() ?
|
||||||
@@ -497,7 +497,7 @@ def get_metadata(book):
|
|||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||||
@@ -532,6 +532,7 @@ def HandleTagCreate():
|
|||||||
return make_response(jsonify(str(shelf.uuid)), 201)
|
return make_response(jsonify(str(shelf.uuid)), 201)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleTagUpdate(tag_id):
|
def HandleTagUpdate(tag_id):
|
||||||
@@ -587,6 +588,7 @@ def add_items_to_shelf(items, shelf):
|
|||||||
return items_unknown_to_calibre
|
return items_unknown_to_calibre
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleTagAddItem(tag_id):
|
def HandleTagAddItem(tag_id):
|
||||||
@@ -616,6 +618,7 @@ def HandleTagAddItem(tag_id):
|
|||||||
return make_response('', 201)
|
return make_response('', 201)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleTagRemoveItem(tag_id):
|
def HandleTagRemoveItem(tag_id):
|
||||||
@@ -678,6 +681,9 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
ub.session.delete(shelf)
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
|
||||||
extra_filters = []
|
extra_filters = []
|
||||||
if only_kobo_shelves:
|
if only_kobo_shelves:
|
||||||
@@ -757,7 +763,7 @@ def create_kobo_tag(shelf):
|
|||||||
)
|
)
|
||||||
return {"Tag": tag}
|
return {"Tag": tag}
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleStateRequest(book_uuid):
|
def HandleStateRequest(book_uuid):
|
||||||
@@ -932,6 +938,7 @@ def TopLevelEndpoint():
|
|||||||
return make_response(jsonify({}))
|
return make_response(jsonify({}))
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleBookDeletionRequest(book_uuid):
|
def HandleBookDeletionRequest(book_uuid):
|
||||||
@@ -954,10 +961,11 @@ def HandleBookDeletionRequest(book_uuid):
|
|||||||
|
|
||||||
ub.session.merge(archived_book)
|
ub.session.merge(archived_book)
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
return ("", 204)
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
# TODO: Implement the following routes
|
# TODO: Implement the following routes
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||||
def HandleUnimplementedRequest(dummy=None):
|
def HandleUnimplementedRequest(dummy=None):
|
||||||
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
||||||
@@ -965,6 +973,7 @@ def HandleUnimplementedRequest(dummy=None):
|
|||||||
|
|
||||||
|
|
||||||
# TODO: Implement the following routes
|
# TODO: Implement the following routes
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
||||||
@@ -975,6 +984,7 @@ def HandleUserRequest(dummy=None):
|
|||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||||
@@ -1008,6 +1018,7 @@ def make_calibre_web_auth_response():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf.exempt
|
||||||
@kobo.route("/v1/auth/device", methods=["POST"])
|
@kobo.route("/v1/auth/device", methods=["POST"])
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
def HandleAuthRequest():
|
def HandleAuthRequest():
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
from flask_login import current_user
|
||||||
|
from . import ub
|
||||||
|
import datetime
|
||||||
|
from sqlalchemy.sql.expression import or_
|
||||||
|
|
||||||
|
|
||||||
|
def add_synced_books(book_id):
|
||||||
|
synced_book = ub.KoboSyncedBooks()
|
||||||
|
synced_book.user_id = current_user.id
|
||||||
|
synced_book.book_id = book_id
|
||||||
|
ub.session.add(synced_book)
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_synced_book(book_id):
|
||||||
|
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).delete()
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
def add_archived_books(book_id):
|
||||||
|
archived_book = (
|
||||||
|
ub.session.query(ub.ArchivedBook)
|
||||||
|
.filter(ub.ArchivedBook.book_id == book_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not archived_book:
|
||||||
|
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
||||||
|
archived_book.is_archived = True
|
||||||
|
archived_book.last_modified = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
ub.session.merge(archived_book)
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# select all books which are synced by the current user and do not belong to a synced shelf and them to archive
|
||||||
|
# select all shelfs from current user which are synced and do not belong to the "only sync" shelfs
|
||||||
|
def update_on_sync_shelfs(content_id):
|
||||||
|
books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
|
||||||
|
.join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True)
|
||||||
|
.join(ub.Shelf, ub.Shelf.user_id == content_id, isouter=True)
|
||||||
|
.filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None))
|
||||||
|
.filter(ub.KoboSyncedBooks.user_id == content_id).all())
|
||||||
|
for b in books_to_archive:
|
||||||
|
add_archived_books(b.book_id)
|
||||||
|
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == b.book_id).filter(ub.KoboSyncedBooks.user_id == content_id).delete()
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
shelfs_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == content_id).filter(
|
||||||
|
ub.Shelf.kobo_sync == 0).all()
|
||||||
|
for a in shelfs_to_archive:
|
||||||
|
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=content_id))
|
||||||
|
ub.session_commit()
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 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/>.
|
||||||
|
|
||||||
|
# ComicVine api document: https://comicvine.gamespot.com/api/documentation
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from cps.services.Metadata import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
class ComicVine(Metadata):
|
||||||
|
__name__ = "ComicVine"
|
||||||
|
__id__ = "comicvine"
|
||||||
|
|
||||||
|
def search(self, query, __):
|
||||||
|
val = list()
|
||||||
|
apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6"
|
||||||
|
if self.active:
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Not Evil Browser'
|
||||||
|
}
|
||||||
|
|
||||||
|
result = requests.get("https://comicvine.gamespot.com/api/search?api_key="
|
||||||
|
+ apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers)
|
||||||
|
for r in result.json()['results']:
|
||||||
|
seriesTitle = r['volume'].get('name', "")
|
||||||
|
if r.get('store_date'):
|
||||||
|
dateFomers = r.get('store_date')
|
||||||
|
else:
|
||||||
|
dateFomers = r.get('date_added')
|
||||||
|
v = dict()
|
||||||
|
v['id'] = r['id']
|
||||||
|
v['title'] = seriesTitle + " #" + r.get('issue_number', "0") + " - " + ( r.get('name', "") or "")
|
||||||
|
v['authors'] = r.get('authors', [])
|
||||||
|
v['description'] = r.get('description', "")
|
||||||
|
v['publisher'] = ""
|
||||||
|
v['publishedDate'] = dateFomers
|
||||||
|
v['tags'] = ["Comics", seriesTitle]
|
||||||
|
v['rating'] = 0
|
||||||
|
v['series'] = seriesTitle
|
||||||
|
v['cover'] = r['image'].get('original_url')
|
||||||
|
v['source'] = {
|
||||||
|
"id": self.__id__,
|
||||||
|
"description": "ComicVine Books",
|
||||||
|
"link": "https://comicvine.gamespot.com/"
|
||||||
|
}
|
||||||
|
v['url'] = r.get('site_detail_url', "")
|
||||||
|
val.append(v)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 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/>.
|
||||||
|
|
||||||
|
# Google Books api document: https://developers.google.com/books/docs/v1/using
|
||||||
|
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from cps.services.Metadata import Metadata
|
||||||
|
|
||||||
|
class Google(Metadata):
|
||||||
|
__name__ = "Google"
|
||||||
|
__id__ = "google"
|
||||||
|
|
||||||
|
def search(self, query, __):
|
||||||
|
if self.active:
|
||||||
|
val = list()
|
||||||
|
result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+"))
|
||||||
|
for r in result.json()['items']:
|
||||||
|
v = dict()
|
||||||
|
v['id'] = r['id']
|
||||||
|
v['title'] = r['volumeInfo']['title']
|
||||||
|
v['authors'] = r['volumeInfo'].get('authors', [])
|
||||||
|
v['description'] = r['volumeInfo'].get('description', "")
|
||||||
|
v['publisher'] = r['volumeInfo'].get('publisher', "")
|
||||||
|
v['publishedDate'] = r['volumeInfo'].get('publishedDate', "")
|
||||||
|
v['tags'] = r['volumeInfo'].get('categories', [])
|
||||||
|
v['rating'] = r['volumeInfo'].get('averageRating', 0)
|
||||||
|
if r['volumeInfo'].get('imageLinks'):
|
||||||
|
v['cover'] = r['volumeInfo']['imageLinks']['thumbnail'].replace("http://", "https://")
|
||||||
|
else:
|
||||||
|
v['cover'] = "/../../../static/generic_cover.jpg"
|
||||||
|
v['source'] = {
|
||||||
|
"id": self.__id__,
|
||||||
|
"description": "Google Books",
|
||||||
|
"link": "https://books.google.com/"}
|
||||||
|
v['url'] = "https://books.google.com/books?id=" + r['id']
|
||||||
|
val.append(v)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 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/>.
|
||||||
|
|
||||||
|
from scholarly import scholarly
|
||||||
|
|
||||||
|
from cps.services.Metadata import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
class scholar(Metadata):
|
||||||
|
__name__ = "Google Scholar"
|
||||||
|
__id__ = "googlescholar"
|
||||||
|
|
||||||
|
def search(self, query, generic_cover=""):
|
||||||
|
val = list()
|
||||||
|
if self.active:
|
||||||
|
scholar_gen = scholarly.search_pubs(' '.join(query.split('+')))
|
||||||
|
i = 0
|
||||||
|
for publication in scholar_gen:
|
||||||
|
v = dict()
|
||||||
|
v['id'] = "1234" # publication['bib'].get('title')
|
||||||
|
v['title'] = publication['bib'].get('title')
|
||||||
|
v['authors'] = publication['bib'].get('author', [])
|
||||||
|
v['description'] = publication['bib'].get('abstract', "")
|
||||||
|
v['publisher'] = publication['bib'].get('venue', "")
|
||||||
|
if publication['bib'].get('pub_year'):
|
||||||
|
v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01"
|
||||||
|
else:
|
||||||
|
v['publishedDate'] = ""
|
||||||
|
v['tags'] = ""
|
||||||
|
v['ratings'] = 0
|
||||||
|
v['series'] = ""
|
||||||
|
v['cover'] = generic_cover
|
||||||
|
v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "",
|
||||||
|
v['source'] = {
|
||||||
|
"id": self.__id__,
|
||||||
|
"description": "Google Scholar",
|
||||||
|
"link": "https://scholar.google.com/"
|
||||||
|
}
|
||||||
|
val.append(v)
|
||||||
|
i += 1
|
||||||
|
if (i >= 10):
|
||||||
|
break
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
from flask import session
|
from flask import session
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import json
|
import json
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
|||||||
+13
-14
@@ -20,7 +20,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import datetime
|
import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -433,16 +432,17 @@ def feed_languagesindex():
|
|||||||
if current_user.filter_language() == u"all":
|
if current_user.filter_language() == u"all":
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
else:
|
else:
|
||||||
try:
|
#try:
|
||||||
cur_l = LC.parse(current_user.filter_language())
|
# cur_l = LC.parse(current_user.filter_language())
|
||||||
except UnknownLocaleError:
|
#except UnknownLocaleError:
|
||||||
cur_l = None
|
# cur_l = None
|
||||||
languages = calibre_db.session.query(db.Languages).filter(
|
languages = calibre_db.session.query(db.Languages).filter(
|
||||||
db.Languages.lang_code == current_user.filter_language()).all()
|
db.Languages.lang_code == current_user.filter_language()).all()
|
||||||
if cur_l:
|
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
|
||||||
languages[0].name = cur_l.get_language_name(get_locale())
|
#if cur_l:
|
||||||
else:
|
# languages[0].name = cur_l.get_language_name(get_locale())
|
||||||
languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
|
#else:
|
||||||
|
# languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
|
||||||
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(languages))
|
len(languages))
|
||||||
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination)
|
||||||
@@ -536,11 +536,10 @@ def feed_search(term):
|
|||||||
|
|
||||||
|
|
||||||
def check_auth(username, password):
|
def check_auth(username, password):
|
||||||
if sys.version_info.major == 3:
|
try:
|
||||||
try:
|
username = username.encode('windows-1252')
|
||||||
username = username.encode('windows-1252')
|
except UnicodeEncodeError:
|
||||||
except UnicodeEncodeError:
|
username = username.encode('utf-8')
|
||||||
username = username.encode('utf-8')
|
|
||||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
|
||||||
username.decode('utf-8').lower()).first()
|
username.decode('utf-8').lower()).first()
|
||||||
if bool(user and check_password_hash(str(user.password), password)):
|
if bool(user and check_password_hash(str(user.password), password)):
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
# http://flask.pocoo.org/snippets/62/
|
# http://flask.pocoo.org/snippets/62/
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import urlparse, urljoin
|
from urllib.parse import urlparse, urljoin
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -36,8 +36,6 @@
|
|||||||
#
|
#
|
||||||
# Inspired by http://flask.pocoo.org/snippets/35/
|
# Inspired by http://flask.pocoo.org/snippets/35/
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
class ReverseProxied(object):
|
class ReverseProxied(object):
|
||||||
"""Wrap the application in this middleware and configure the
|
"""Wrap the application in this middleware and configure the
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 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 json
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
import inspect
|
||||||
|
import datetime
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
from flask import Blueprint, request, Response, url_for
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask_login import login_required
|
||||||
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||||
|
|
||||||
|
from . import constants, logger, ub
|
||||||
|
from cps.services.Metadata import Metadata
|
||||||
|
|
||||||
|
|
||||||
|
meta = Blueprint('metadata', __name__)
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
new_list = list()
|
||||||
|
meta_dir = os.path.join(constants.BASE_DIR, "cps", "metadata_provider")
|
||||||
|
modules = os.listdir(os.path.join(constants.BASE_DIR, "cps", "metadata_provider"))
|
||||||
|
for f in modules:
|
||||||
|
if os.path.isfile(os.path.join(meta_dir, f)) and not f.endswith('__init__.py'):
|
||||||
|
a = os.path.basename(f)[:-3]
|
||||||
|
try:
|
||||||
|
importlib.import_module("cps.metadata_provider." + a)
|
||||||
|
new_list.append(a)
|
||||||
|
except ImportError:
|
||||||
|
log.error("Import error for metadata source: {}".format(a))
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_classes(provider_list):
|
||||||
|
classes = list()
|
||||||
|
for element in provider_list:
|
||||||
|
for name, obj in inspect.getmembers(sys.modules["cps.metadata_provider." + element]):
|
||||||
|
if inspect.isclass(obj) and name != "Metadata" and issubclass(obj, Metadata):
|
||||||
|
classes.append(obj())
|
||||||
|
return classes
|
||||||
|
|
||||||
|
cl = list_classes(new_list)
|
||||||
|
|
||||||
|
@meta.route("/metadata/provider")
|
||||||
|
@login_required
|
||||||
|
def metadata_provider():
|
||||||
|
active = current_user.view_settings.get('metadata', {})
|
||||||
|
provider = list()
|
||||||
|
for c in cl:
|
||||||
|
ac = active.get(c.__id__, True)
|
||||||
|
provider.append({"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__})
|
||||||
|
return Response(json.dumps(provider), mimetype='application/json')
|
||||||
|
|
||||||
|
@meta.route("/metadata/provider", methods=['POST'])
|
||||||
|
@meta.route("/metadata/provider/<prov_name>", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def metadata_change_active_provider(prov_name):
|
||||||
|
new_state = request.get_json()
|
||||||
|
active = current_user.view_settings.get('metadata', {})
|
||||||
|
active[new_state['id']] = new_state['value']
|
||||||
|
current_user.view_settings['metadata'] = active
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
flag_modified(current_user, "view_settings")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
ub.session.commit()
|
||||||
|
except (InvalidRequestError, OperationalError):
|
||||||
|
log.error("Invalid request received: {}".format(request))
|
||||||
|
return "Invalid request", 400
|
||||||
|
if "initial" in new_state and prov_name:
|
||||||
|
for c in cl:
|
||||||
|
if c.__id__ == prov_name:
|
||||||
|
data = c.search(new_state.get('query', ""))
|
||||||
|
break
|
||||||
|
return Response(json.dumps(data), mimetype='application/json')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@meta.route("/metadata/search", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def metadata_search():
|
||||||
|
query = request.form.to_dict().get('query')
|
||||||
|
data = list()
|
||||||
|
active = current_user.view_settings.get('metadata', {})
|
||||||
|
if query:
|
||||||
|
static_cover = url_for('static', filename='generic_cover.jpg')
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
|
meta = {executor.submit(c.search, query, static_cover): c for c in cl if active.get(c.__id__, True)}
|
||||||
|
for future in concurrent.futures.as_completed(meta):
|
||||||
|
data.extend(future.result())
|
||||||
|
return Response(json.dumps(data), mimetype='application/json')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import errno
|
import errno
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
class Metadata():
|
||||||
|
__name__ = "Generic"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.active = True
|
||||||
|
|
||||||
|
def set_status(self, state):
|
||||||
|
self.active = state
|
||||||
+14
-17
@@ -35,10 +35,7 @@ log = logger.create()
|
|||||||
|
|
||||||
|
|
||||||
def b64encode_json(json_data):
|
def b64encode_json(json_data):
|
||||||
if sys.version_info < (3, 0):
|
return b64encode(json.dumps(json_data).encode())
|
||||||
return b64encode(json.dumps(json_data))
|
|
||||||
else:
|
|
||||||
return b64encode(json.dumps(json_data).encode())
|
|
||||||
|
|
||||||
|
|
||||||
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
|
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
|
||||||
@@ -85,8 +82,8 @@ class SyncToken:
|
|||||||
"books_last_created": {"type": "string"},
|
"books_last_created": {"type": "string"},
|
||||||
"archive_last_modified": {"type": "string"},
|
"archive_last_modified": {"type": "string"},
|
||||||
"reading_state_last_modified": {"type": "string"},
|
"reading_state_last_modified": {"type": "string"},
|
||||||
"tags_last_modified": {"type": "string"},
|
"tags_last_modified": {"type": "string"}
|
||||||
"books_last_id": {"type": "integer", "optional": True}
|
# "books_last_id": {"type": "integer", "optional": True}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +94,8 @@ class SyncToken:
|
|||||||
books_last_modified=datetime.min,
|
books_last_modified=datetime.min,
|
||||||
archive_last_modified=datetime.min,
|
archive_last_modified=datetime.min,
|
||||||
reading_state_last_modified=datetime.min,
|
reading_state_last_modified=datetime.min,
|
||||||
tags_last_modified=datetime.min,
|
tags_last_modified=datetime.min
|
||||||
books_last_id=-1
|
# books_last_id=-1
|
||||||
): # nosec
|
): # nosec
|
||||||
self.raw_kobo_store_token = raw_kobo_store_token
|
self.raw_kobo_store_token = raw_kobo_store_token
|
||||||
self.books_last_created = books_last_created
|
self.books_last_created = books_last_created
|
||||||
@@ -106,7 +103,7 @@ class SyncToken:
|
|||||||
self.archive_last_modified = archive_last_modified
|
self.archive_last_modified = archive_last_modified
|
||||||
self.reading_state_last_modified = reading_state_last_modified
|
self.reading_state_last_modified = reading_state_last_modified
|
||||||
self.tags_last_modified = tags_last_modified
|
self.tags_last_modified = tags_last_modified
|
||||||
self.books_last_id = books_last_id
|
# self.books_last_id = books_last_id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_headers(headers):
|
def from_headers(headers):
|
||||||
@@ -141,12 +138,12 @@ class SyncToken:
|
|||||||
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
||||||
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
||||||
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
||||||
books_last_id = data_json["books_last_id"]
|
# books_last_id = data_json["books_last_id"]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||||
except KeyError:
|
#except KeyError:
|
||||||
books_last_id = -1
|
# books_last_id = -1
|
||||||
|
|
||||||
return SyncToken(
|
return SyncToken(
|
||||||
raw_kobo_store_token=raw_kobo_store_token,
|
raw_kobo_store_token=raw_kobo_store_token,
|
||||||
@@ -155,7 +152,7 @@ class SyncToken:
|
|||||||
archive_last_modified=archive_last_modified,
|
archive_last_modified=archive_last_modified,
|
||||||
reading_state_last_modified=reading_state_last_modified,
|
reading_state_last_modified=reading_state_last_modified,
|
||||||
tags_last_modified=tags_last_modified,
|
tags_last_modified=tags_last_modified,
|
||||||
books_last_id=books_last_id
|
#books_last_id=books_last_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_kobo_store_header(self, store_headers):
|
def set_kobo_store_header(self, store_headers):
|
||||||
@@ -179,16 +176,16 @@ class SyncToken:
|
|||||||
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
||||||
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
||||||
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
|
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
|
||||||
"books_last_id":self.books_last_id
|
#"books_last_id":self.books_last_id
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return b64encode_json(token)
|
return b64encode_json(token)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{},{},{},{},{},{},{}".format(self.raw_kobo_store_token,
|
return "{},{},{},{},{},{}".format(self.raw_kobo_store_token,
|
||||||
self.books_last_created,
|
self.books_last_created,
|
||||||
self.books_last_modified,
|
self.books_last_modified,
|
||||||
self.archive_last_modified,
|
self.archive_last_modified,
|
||||||
self.reading_state_last_modified,
|
self.reading_state_last_modified,
|
||||||
self.tags_last_modified,
|
self.tags_last_modified)
|
||||||
self.books_last_id)
|
#self.books_last_id)
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
|
|
||||||
from .. import logger
|
from .. import logger
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -1,4 +1,21 @@
|
|||||||
from __future__ import print_function
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2021 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.path
|
import os.path
|
||||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
from google.auth.transport.requests import Request
|
from google.auth.transport.requests import Request
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import time
|
import time
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from flask_simpleldap import LDAP, LDAPException
|
from flask_simpleldap import LDAP, LDAPException
|
||||||
|
|||||||
+24
-5
@@ -1,5 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
||||||
|
# Copyright (C) 2020 pwr
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import threading
|
import threading
|
||||||
import abc
|
import abc
|
||||||
import uuid
|
import uuid
|
||||||
@@ -205,10 +221,13 @@ class CalibreTask:
|
|||||||
# By default, we're good to clean a task if it's "Done"
|
# By default, we're good to clean a task if it's "Done"
|
||||||
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
||||||
|
|
||||||
@progress.setter
|
'''@progress.setter
|
||||||
def progress(self, x):
|
def progress(self, x):
|
||||||
# todo: throw error if outside of [0,1]
|
if x > 1:
|
||||||
self._progress = x
|
x = 1
|
||||||
|
if x < 0:
|
||||||
|
x = 0
|
||||||
|
self._progress = x'''
|
||||||
|
|
||||||
def _handleError(self, error_message):
|
def _handleError(self, error_message):
|
||||||
self.stat = STAT_FAIL
|
self.stat = STAT_FAIL
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
# 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 __future__ import division, print_function, unicode_literals
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -35,7 +35,7 @@ body {
|
|||||||
float: left;
|
float: left;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-image: url("img/toolbar-buttons.png");
|
background-image: url("../../js/libs/djvu_html5/img/toolbar-buttons.png");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 500% 300%;
|
background-size: 500% 300%;
|
||||||
}
|
}
|
||||||
@@ -185,10 +185,10 @@ _:-ms-lang(x), .textLayer {
|
|||||||
width: 128px;
|
width: 128px;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
margin: -72px 0 0 -64px;
|
margin: -72px 0 0 -64px;
|
||||||
background-image: url("img/status.png");
|
background-image: url("../../js/libs/djvu_html5/img/status.png");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blankImage {
|
.blankImage {
|
||||||
background-image: url("img/blank.jpg");
|
background-image: url("../../js/libs/djvu_html5/img/blank.jpg");
|
||||||
}
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
ul.wysihtml5-toolbar {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar::after {
|
||||||
|
clear: both;
|
||||||
|
display: table;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar > li {
|
||||||
|
float: left;
|
||||||
|
display: list-item;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0 5px 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar a[data-wysihtml5-command=bold] {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar a[data-wysihtml5-command=italic] {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar a[data-wysihtml5-command=underline] {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar a.btn.wysihtml5-command-active {
|
||||||
|
background-image: none;
|
||||||
|
-webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
-moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
background-color: #E6E6E6;
|
||||||
|
background-color: #D9D9D9;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-commands-disabled .dropdown-menu {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div.wysihtml5-colors {
|
||||||
|
display:block;
|
||||||
|
width: 50px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-left: 5px;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar a.wysihtml5-colors-title {
|
||||||
|
padding-left: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="black"] {
|
||||||
|
background: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="silver"] {
|
||||||
|
background: silver !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="gray"] {
|
||||||
|
background: gray !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="maroon"] {
|
||||||
|
background: maroon !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="red"] {
|
||||||
|
background: red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="purple"] {
|
||||||
|
background: purple !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="green"] {
|
||||||
|
background: green !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="olive"] {
|
||||||
|
background: olive !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="navy"] {
|
||||||
|
background: navy !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="blue"] {
|
||||||
|
background: blue !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.wysihtml5-toolbar div[data-wysihtml5-command-value="orange"] {
|
||||||
|
background: orange !important;
|
||||||
|
}
|
||||||
|
|
||||||
Vendored
+5
-5
@@ -145,7 +145,7 @@ fieldset[disabled] .twitter-typeahead .tt-input {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
background-color: #eeeeee !important;
|
background-color: #eeeeee !important;
|
||||||
}
|
}
|
||||||
.tt-dropdown-menu {
|
.tt-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -166,7 +166,7 @@ fieldset[disabled] .twitter-typeahead .tt-input {
|
|||||||
*border-right-width: 2px;
|
*border-right-width: 2px;
|
||||||
*border-bottom-width: 2px;
|
*border-bottom-width: 2px;
|
||||||
}
|
}
|
||||||
.tt-dropdown-menu .tt-suggestion {
|
.tt-menu .tt-suggestion {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 3px 20px;
|
padding: 3px 20px;
|
||||||
clear: both;
|
clear: both;
|
||||||
@@ -175,15 +175,15 @@ fieldset[disabled] .twitter-typeahead .tt-input {
|
|||||||
color: #333333;
|
color: #333333;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.tt-dropdown-menu .tt-suggestion.tt-cursor {
|
.tt-menu .tt-suggestion.tt-cursor {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
color: #262626;
|
color: #262626;
|
||||||
}
|
}
|
||||||
.tt-dropdown-menu .tt-suggestion.tt-cursor a {
|
.tt-menu .tt-suggestion.tt-cursor a {
|
||||||
color: #262626;
|
color: #262626;
|
||||||
}
|
}
|
||||||
.tt-dropdown-menu .tt-suggestion p {
|
.tt-menu .tt-suggestion p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-10
@@ -123,6 +123,10 @@ table .bg-dark-danger a { color: #fff; }
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-fluid.text-center {
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
.container-fluid img {
|
.container-fluid img {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -151,11 +155,6 @@ table .bg-dark-danger a { color: #fff; }
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-fluid .book .cover span.img {
|
|
||||||
bottom: 0;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.author-bio img { margin: 0 1em 1em 0; }
|
.author-bio img { margin: 0 1em 1em 0; }
|
||||||
|
|
||||||
.container-fluid .single .cover img {
|
.container-fluid .single .cover img {
|
||||||
@@ -166,12 +165,16 @@ table .bg-dark-danger a { color: #fff; }
|
|||||||
box-shadow: 0 5px 8px -6px #777;
|
box-shadow: 0 5px 8px -6px #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.datepicker.form-control {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
.container-fluid .book .cover span img {
|
.container-fluid .book .cover span img {
|
||||||
position: relative;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
|
bottom: 0;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
-webkit-box-shadow: 0 5px 8px -6px #777;
|
-webkit-box-shadow: 0 5px 8px -6px #777;
|
||||||
-moz-box-shadow: 0 5px 8px -6px #777;
|
-moz-box-shadow: 0 5px 8px -6px #777;
|
||||||
@@ -322,7 +325,7 @@ table .bg-dark-danger:hover { background-color: #c9302c; }
|
|||||||
table .bg-primary:hover { background-color: #1c5484; }
|
table .bg-primary:hover { background-color: #1c5484; }
|
||||||
.block-label { display: block; }
|
.block-label { display: block; }
|
||||||
|
|
||||||
.fake-input {
|
.form-control.fake-input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#readmain {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#area {
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
margin: 5% auto;
|
||||||
|
max-width: 1250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#area iframe {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
xmp, pre, plaintext {
|
||||||
|
display: block;
|
||||||
|
font-family: -moz-fixed;
|
||||||
|
white-space: pre;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#area{
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-family: -moz-fixed;
|
||||||
|
column-count:2;
|
||||||
|
-webkit-columns:2;
|
||||||
|
-moz-columns:2;
|
||||||
|
column-gap:20px;
|
||||||
|
-moz-column-gap:20px;
|
||||||
|
-webkit-column-gap:20px;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
@@ -150,11 +150,16 @@ if ($("body.book").length > 0) {
|
|||||||
var splitText = $(this).text().split(':');
|
var splitText = $(this).text().split(':');
|
||||||
var label = splitText.shift().trim();
|
var label = splitText.shift().trim();
|
||||||
var value = splitText.join(':').trim();
|
var value = splitText.join(':').trim();
|
||||||
|
var class_value = ""
|
||||||
// Preserve Links
|
// Preserve Links
|
||||||
if ($(this).find('a').length) {
|
if ($(this).find('a').length) {
|
||||||
value = $(this).find('a').first().removeClass();
|
value = $(this).find('a').first().removeClass();
|
||||||
}
|
}
|
||||||
$(this).html('<span>' + label + '</span><span></span>').find('span').last().append(value);
|
// Preserve glyphicons
|
||||||
|
if ($(this).find('span').length) {
|
||||||
|
class_value = $(this).find('span').first().attr('class');
|
||||||
|
}
|
||||||
|
$(this).html('<span>' + label + '</span><span class="' + class_value + '"></span>').find('span').last().append(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".book-meta h2:first").clone()
|
$(".book-meta h2:first").clone()
|
||||||
|
|||||||
@@ -22,15 +22,17 @@ $(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#have_read_cb").on("change", function() {
|
$("#have_read_cb").on("change", function() {
|
||||||
$.post({
|
$.ajax({
|
||||||
url: this.closest("form").action,
|
url: this.closest("form").action,
|
||||||
|
method:"post",
|
||||||
|
data: $(this).closest("form").serialize(),
|
||||||
error: function(response) {
|
error: function(response) {
|
||||||
var data = [{type:"danger", message:response.responseText}]
|
var data = [{type:"danger", message:response.responseText}]
|
||||||
$("#flash_success").remove();
|
$("#flash_success").remove();
|
||||||
$("#flash_danger").remove();
|
$("#flash_danger").remove();
|
||||||
if (!jQuery.isEmptyObject(data)) {
|
if (!jQuery.isEmptyObject(data)) {
|
||||||
data.forEach(function (item) {
|
data.forEach(function (item) {
|
||||||
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' +
|
$(".navbar").after('<div class="row-fluid text-center" >' +
|
||||||
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
});
|
});
|
||||||
|
|||||||
+137
-140
@@ -6,6 +6,7 @@
|
|||||||
if ($("#description").length) {
|
if ($("#description").length) {
|
||||||
tinymce.init({
|
tinymce.init({
|
||||||
selector: "#description",
|
selector: "#description",
|
||||||
|
plugins: 'code',
|
||||||
branding: false,
|
branding: false,
|
||||||
menubar: "edit view format",
|
menubar: "edit view format",
|
||||||
language: language
|
language: language
|
||||||
@@ -15,6 +16,7 @@ if ($("#description").length) {
|
|||||||
if ($(".tiny_editor").length) {
|
if ($(".tiny_editor").length) {
|
||||||
tinymce.init({
|
tinymce.init({
|
||||||
selector: ".tiny_editor",
|
selector: ".tiny_editor",
|
||||||
|
plugins: 'code',
|
||||||
branding: false,
|
branding: false,
|
||||||
menubar: "edit view format",
|
menubar: "edit view format",
|
||||||
language: language
|
language: language
|
||||||
@@ -47,90 +49,20 @@ $(".datepicker_delete").click(function() {
|
|||||||
Takes a prefix, query typeahead callback, Bloodhound typeahead adapter
|
Takes a prefix, query typeahead callback, Bloodhound typeahead adapter
|
||||||
and returns the completions it gets from the bloodhound engine prefixed.
|
and returns the completions it gets from the bloodhound engine prefixed.
|
||||||
*/
|
*/
|
||||||
function prefixedSource(prefix, query, cb, bhAdapter) {
|
function prefixedSource(prefix, query, cb, source) {
|
||||||
bhAdapter(query, function(retArray) {
|
function async(retArray) {
|
||||||
|
retArray = retArray || [];
|
||||||
var matches = [];
|
var matches = [];
|
||||||
for (var i = 0; i < retArray.length; i++) {
|
for (var i = 0; i < retArray.length; i++) {
|
||||||
var obj = {name : prefix + retArray[i].name};
|
var obj = {name : prefix + retArray[i].name};
|
||||||
matches.push(obj);
|
matches.push(obj);
|
||||||
}
|
}
|
||||||
cb(matches);
|
cb(matches);
|
||||||
});
|
}
|
||||||
|
source.search(query, cb, async);
|
||||||
}
|
}
|
||||||
|
|
||||||
var authors = new Bloodhound({
|
|
||||||
name: "authors",
|
|
||||||
datumTokenizer: function datumTokenizer(datum) {
|
|
||||||
return [datum.name];
|
|
||||||
},
|
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
|
||||||
remote: {
|
|
||||||
url: getPath() + "/get_authors_json?q=%QUERY"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var series = new Bloodhound({
|
|
||||||
name: "series",
|
|
||||||
datumTokenizer: function datumTokenizer(datum) {
|
|
||||||
return [datum.name];
|
|
||||||
},
|
|
||||||
queryTokenizer: function queryTokenizer(query) {
|
|
||||||
return [query];
|
|
||||||
},
|
|
||||||
remote: {
|
|
||||||
url: getPath() + "/get_series_json?q=",
|
|
||||||
replace: function replace(url, query) {
|
|
||||||
return url + encodeURIComponent(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var tags = new Bloodhound({
|
|
||||||
name: "tags",
|
|
||||||
datumTokenizer: function datumTokenizer(datum) {
|
|
||||||
return [datum.name];
|
|
||||||
},
|
|
||||||
queryTokenizer: function queryTokenizer(query) {
|
|
||||||
var tokens = query.split(",");
|
|
||||||
tokens = [tokens[tokens.length - 1].trim()];
|
|
||||||
return tokens;
|
|
||||||
},
|
|
||||||
remote: {
|
|
||||||
url: getPath() + "/get_tags_json?q=%QUERY"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var languages = new Bloodhound({
|
|
||||||
name: "languages",
|
|
||||||
datumTokenizer: function datumTokenizer(datum) {
|
|
||||||
return [datum.name];
|
|
||||||
},
|
|
||||||
queryTokenizer: function queryTokenizer(query) {
|
|
||||||
return [query];
|
|
||||||
},
|
|
||||||
remote: {
|
|
||||||
url: getPath() + "/get_languages_json?q=",
|
|
||||||
replace: function replace(url, query) {
|
|
||||||
return url + encodeURIComponent(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var publishers = new Bloodhound({
|
|
||||||
name: "publisher",
|
|
||||||
datumTokenizer: function datumTokenizer(datum) {
|
|
||||||
return [datum.name];
|
|
||||||
},
|
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
|
||||||
remote: {
|
|
||||||
url: getPath() + "/get_publishers_json?q=%QUERY"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function sourceSplit(query, cb, split, source) {
|
function sourceSplit(query, cb, split, source) {
|
||||||
var bhAdapter = source.ttAdapter();
|
|
||||||
|
|
||||||
var tokens = query.split(split);
|
var tokens = query.split(split);
|
||||||
var currentSource = tokens[tokens.length - 1].trim();
|
var currentSource = tokens[tokens.length - 1].trim();
|
||||||
|
|
||||||
@@ -145,84 +77,148 @@ function sourceSplit(query, cb, split, source) {
|
|||||||
for (var i = 0; i < tokens.length; i++) {
|
for (var i = 0; i < tokens.length; i++) {
|
||||||
prefix += tokens[i].trim() + newSplit;
|
prefix += tokens[i].trim() + newSplit;
|
||||||
}
|
}
|
||||||
prefixedSource(prefix, currentSource, cb, bhAdapter);
|
prefixedSource(prefix, currentSource, cb, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
var promiseAuthors = authors.initialize();
|
var authors = new Bloodhound({
|
||||||
promiseAuthors.done(function() {
|
name: "authors",
|
||||||
$("#bookAuthor").typeahead(
|
identify: function(obj) { return obj.name; },
|
||||||
{
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
highlight: true, minLength: 1,
|
return [datum.name];
|
||||||
hint: true
|
},
|
||||||
}, {
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
name: "authors",
|
remote: {
|
||||||
displayKey: "name",
|
url: getPath() + "/get_authors_json?q=%QUERY",
|
||||||
source: function source(query, cb) {
|
wildcard: '%QUERY',
|
||||||
return sourceSplit(query, cb, "&", authors); //sourceSplit //("&")
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var promiseSeries = series.initialize();
|
$(".form-group #bookAuthor").typeahead(
|
||||||
promiseSeries.done(function() {
|
{
|
||||||
$("#series").typeahead(
|
highlight: true,
|
||||||
{
|
minLength: 1,
|
||||||
highlight: true, minLength: 0,
|
hint: true
|
||||||
hint: true
|
}, {
|
||||||
}, {
|
name: "authors",
|
||||||
name: "series",
|
display: 'name',
|
||||||
displayKey: "name",
|
source: function source(query, cb, asyncResults) {
|
||||||
source: series.ttAdapter()
|
return sourceSplit(query, cb, "&", authors);
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
var series = new Bloodhound({
|
||||||
|
name: "series",
|
||||||
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
|
return [datum.name];
|
||||||
|
},
|
||||||
|
// queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
|
queryTokenizer: function queryTokenizer(query) {
|
||||||
|
return [query];
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
url: getPath() + "/get_series_json?q=%QUERY",
|
||||||
|
wildcard: '%QUERY',
|
||||||
|
/*replace: function replace(url, query) {
|
||||||
|
return url + encodeURIComponent(query);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(".form-group #series").typeahead(
|
||||||
|
{
|
||||||
|
highlight: true,
|
||||||
|
minLength: 0,
|
||||||
|
hint: true
|
||||||
|
}, {
|
||||||
|
name: "series",
|
||||||
|
displayKey: "name",
|
||||||
|
source: series
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var tags = new Bloodhound({
|
||||||
|
name: "tags",
|
||||||
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
|
return [datum.name];
|
||||||
|
},
|
||||||
|
queryTokenizer: function queryTokenizer(query) {
|
||||||
|
var tokens = query.split(",");
|
||||||
|
tokens = [tokens[tokens.length - 1].trim()];
|
||||||
|
return tokens;
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
url: getPath() + "/get_tags_json?q=%QUERY",
|
||||||
|
wildcard: '%QUERY'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var promiseTags = tags.initialize();
|
$(".form-group #tags").typeahead(
|
||||||
promiseTags.done(function() {
|
{
|
||||||
$("#tags").typeahead(
|
highlight: true,
|
||||||
{
|
minLength: 0,
|
||||||
highlight: true, minLength: 0,
|
hint: true
|
||||||
hint: true
|
}, {
|
||||||
}, {
|
name: "tags",
|
||||||
name: "tags",
|
display: "name",
|
||||||
displayKey: "name",
|
source: function source(query, cb, asyncResults) {
|
||||||
source: function source(query, cb) {
|
return sourceSplit(query, cb, ",", tags);
|
||||||
return sourceSplit(query, cb, ",", tags);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var languages = new Bloodhound({
|
||||||
|
name: "languages",
|
||||||
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
|
return [datum.name];
|
||||||
|
},
|
||||||
|
queryTokenizer: function queryTokenizer(query) {
|
||||||
|
return [query];
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
url: getPath() + "/get_languages_json?q=%QUERY",
|
||||||
|
wildcard: '%QUERY'
|
||||||
|
/*replace: function replace(url, query) {
|
||||||
|
return url + encodeURIComponent(query);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var promiseLanguages = languages.initialize();
|
$(".form-group #languages").typeahead(
|
||||||
promiseLanguages.done(function() {
|
{
|
||||||
$("#languages").typeahead(
|
highlight: true, minLength: 0,
|
||||||
{
|
hint: true
|
||||||
highlight: true, minLength: 0,
|
}, {
|
||||||
hint: true
|
name: "languages",
|
||||||
}, {
|
display: "name",
|
||||||
name: "languages",
|
source: function source(query, cb, asyncResults) {
|
||||||
displayKey: "name",
|
return sourceSplit(query, cb, ",", languages);
|
||||||
source: function source(query, cb) {
|
|
||||||
return sourceSplit(query, cb, ",", languages); //(",")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var publishers = new Bloodhound({
|
||||||
|
name: "publisher",
|
||||||
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
|
return [datum.name];
|
||||||
|
},
|
||||||
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
|
remote: {
|
||||||
|
url: getPath() + "/get_publishers_json?q=%QUERY",
|
||||||
|
wildcard: '%QUERY'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var promisePublishers = publishers.initialize();
|
$(".form-group #publisher").typeahead(
|
||||||
promisePublishers.done(function() {
|
{
|
||||||
$("#publisher").typeahead(
|
highlight: true, minLength: 0,
|
||||||
{
|
hint: true
|
||||||
highlight: true, minLength: 0,
|
}, {
|
||||||
hint: true
|
name: "publishers",
|
||||||
}, {
|
displayKey: "name",
|
||||||
name: "publishers",
|
source: publishers
|
||||||
displayKey: "name",
|
}
|
||||||
source: publishers.ttAdapter()
|
);
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#search").on("change input.typeahead:selected", function(event) {
|
$("#search").on("change input.typeahead:selected", function(event) {
|
||||||
if (event.target.type === "search" && event.target.tagName === "INPUT") {
|
if (event.target.type === "search" && event.target.tagName === "INPUT") {
|
||||||
@@ -269,3 +265,4 @@ $("#xchange").click(function () {
|
|||||||
$("#book_title").val($("#bookAuthor").val());
|
$("#book_title").val($("#bookAuthor").val());
|
||||||
$("#bookAuthor").val(title);
|
$("#bookAuthor").val(title);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+118
-364
@@ -14,380 +14,134 @@
|
|||||||
* 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/>.
|
||||||
*/
|
*/
|
||||||
/*
|
/* global _, i18nMsg, tinymce, getPath */
|
||||||
* Get Metadata from Douban Books api and Google Books api and ComicVine
|
|
||||||
* Google Books api document: https://developers.google.com/books/docs/v1/using
|
|
||||||
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
|
|
||||||
* ComicVine api document: https://comicvine.gamespot.com/api/documentation
|
|
||||||
*/
|
|
||||||
/* global _, i18nMsg, tinymce */
|
|
||||||
var dbResults = [];
|
|
||||||
var ggResults = [];
|
|
||||||
var cvResults = [];
|
|
||||||
var gsResults = [];
|
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
var msg = i18nMsg;
|
var msg = i18nMsg;
|
||||||
var douban = "https://api.douban.com";
|
var keyword = ""
|
||||||
var dbSearch = "/v2/book/search";
|
|
||||||
var dbDone = 0;
|
|
||||||
|
|
||||||
var google = "https://www.googleapis.com";
|
var templates = {
|
||||||
var ggSearch = "/books/v1/volumes";
|
bookResult: _.template(
|
||||||
var ggDone = 0;
|
$("#template-book-result").html()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
var comicvine = "https://comicvine.gamespot.com";
|
function populateForm (book) {
|
||||||
var cvSearch = "/api/search/";
|
tinymce.get("description").setContent(book.description);
|
||||||
var cvDone = 0;
|
var uniqueTags = [];
|
||||||
|
$.each(book.tags, function(i, el) {
|
||||||
var googlescholar = window.location.href.split('/admin/book')[0];
|
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
|
||||||
var gsSearch = "/scholarsearch/"
|
|
||||||
var gsDone = 0;
|
|
||||||
|
|
||||||
var showFlag = 0;
|
|
||||||
|
|
||||||
var templates = {
|
|
||||||
bookResult: _.template(
|
|
||||||
$("#template-book-result").html()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
function populateForm (book) {
|
|
||||||
tinymce.get("description").setContent(book.description);
|
|
||||||
var uniqueTags = [];
|
|
||||||
$.each(book.tags, function(i, el) {
|
|
||||||
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
var ampSeparatedAuthors = (book.authors || []).join(" & ");
|
|
||||||
$("#bookAuthor").val(ampSeparatedAuthors);
|
|
||||||
$("#book_title").val(book.title);
|
|
||||||
$("#tags").val(uniqueTags.join(","));
|
|
||||||
$("#rating").data("rating").setValue(Math.round(book.rating));
|
|
||||||
if(book.cover !== null){
|
|
||||||
$(".cover img").attr("src", book.cover);
|
|
||||||
$("#cover_url").val(book.cover);
|
|
||||||
}
|
|
||||||
$("#pubdate").val(book.publishedDate);
|
|
||||||
$("#publisher").val(book.publisher);
|
|
||||||
if (typeof book.series !== "undefined") {
|
|
||||||
$("#series").val(book.series);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showResult () {
|
|
||||||
showFlag++;
|
|
||||||
if (showFlag === 1) {
|
|
||||||
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
|
||||||
}
|
|
||||||
if ((ggDone === 3 || (ggDone === 1 && ggResults.length === 0)) &&
|
|
||||||
(dbDone === 3 || (dbDone === 1 && dbResults.length === 0)) &&
|
|
||||||
(cvDone === 3 || (cvDone === 1 && cvResults.length === 0)) &&
|
|
||||||
(gsDone === 3 || (gsDone === 1 && gsResults.length === 0))) {
|
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
function formatDate (date) {
|
|
||||||
var d = new Date(date),
|
|
||||||
month = "" + (d.getMonth() + 1),
|
|
||||||
day = "" + d.getDate(),
|
|
||||||
year = d.getFullYear();
|
|
||||||
|
|
||||||
if (month.length < 2) {
|
|
||||||
month = "0" + month;
|
|
||||||
}
|
|
||||||
if (day.length < 2) {
|
|
||||||
day = "0" + day;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [year, month, day].join("-");
|
|
||||||
}
|
|
||||||
function generateID (title) {
|
|
||||||
return title.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0).toString().substr(0,12);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ggResults.length > 0) {
|
|
||||||
if (ggDone < 2) {
|
|
||||||
ggResults.forEach(function(result) {
|
|
||||||
var book = {
|
|
||||||
id: result.id,
|
|
||||||
title: result.volumeInfo.title,
|
|
||||||
authors: result.volumeInfo.authors || [],
|
|
||||||
description: result.volumeInfo.description || "",
|
|
||||||
publisher: result.volumeInfo.publisher || "",
|
|
||||||
publishedDate: result.volumeInfo.publishedDate || "",
|
|
||||||
tags: result.volumeInfo.categories || [],
|
|
||||||
rating: result.volumeInfo.averageRating || 0,
|
|
||||||
cover: result.volumeInfo.imageLinks ?
|
|
||||||
result.volumeInfo.imageLinks.thumbnail : location + "/../../../static/generic_cover.jpg",
|
|
||||||
url: "https://books.google.com/books?id=" + result.id,
|
|
||||||
source: {
|
|
||||||
id: "google",
|
|
||||||
description: "Google Books",
|
|
||||||
url: "https://books.google.com/"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var $book = $(templates.bookResult(book));
|
|
||||||
$book.find("img").on("click", function () {
|
|
||||||
populateForm(book);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#book-list").append($book);
|
|
||||||
});
|
});
|
||||||
ggDone = 2;
|
var ampSeparatedAuthors = (book.authors || []).join(" & ");
|
||||||
} else {
|
$("#bookAuthor").val(ampSeparatedAuthors);
|
||||||
ggDone = 3;
|
$("#book_title").val(book.title);
|
||||||
}
|
$("#tags").val(uniqueTags.join(","));
|
||||||
}
|
$("#rating").data("rating").setValue(Math.round(book.rating));
|
||||||
|
if(book.cover !== null){
|
||||||
if (gsResults.length > 0) {
|
$(".cover img").attr("src", book.cover);
|
||||||
if (gsDone < 2) {
|
$("#cover_url").val(book.cover);
|
||||||
gsResults.forEach(function(result) {
|
}
|
||||||
var book = {
|
$("#pubdate").val(book.publishedDate);
|
||||||
id: generateID(result.bib.title),
|
$("#publisher").val(book.publisher);
|
||||||
title: result.bib.title,
|
if (typeof book.series !== "undefined") {
|
||||||
authors: result.bib.author || [],
|
$("#series").val(book.series);
|
||||||
description: result.bib.abstract || "",
|
|
||||||
publisher: result.bib.venue || "",
|
|
||||||
publishedDate: result.bib.pub_year ? result.bib.pub_year+"-01-01" : "",
|
|
||||||
tags: [],
|
|
||||||
rating: 0,
|
|
||||||
series: "",
|
|
||||||
cover: null,
|
|
||||||
url: result.pub_url || result.eprint_url || "",
|
|
||||||
source: {
|
|
||||||
id: "googlescholar",
|
|
||||||
description: "Google Scholar",
|
|
||||||
link: "https://scholar.google.com/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var $book = $(templates.bookResult(book));
|
|
||||||
$book.find("img").on("click", function () {
|
|
||||||
populateForm(book);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#book-list").append($book);
|
|
||||||
|
|
||||||
});
|
|
||||||
gsDone = 2;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
gsDone = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dbResults.length > 0) {
|
|
||||||
if (dbDone < 2) {
|
|
||||||
dbResults.forEach(function(result) {
|
|
||||||
var seriesTitle = "";
|
|
||||||
if (result.series) {
|
|
||||||
seriesTitle = result.series.title;
|
|
||||||
}
|
|
||||||
var dateFomers = result.pubdate.split("-");
|
|
||||||
var publishedYear = parseInt(dateFomers[0], 10);
|
|
||||||
var publishedMonth = parseInt(dateFomers[1], 10);
|
|
||||||
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
|
|
||||||
|
|
||||||
publishedDate = formatDate(publishedDate);
|
|
||||||
|
|
||||||
var book = {
|
|
||||||
id: result.id,
|
|
||||||
title: result.title,
|
|
||||||
authors: result.author || [],
|
|
||||||
description: result.summary,
|
|
||||||
publisher: result.publisher || "",
|
|
||||||
publishedDate: publishedDate || "",
|
|
||||||
tags: result.tags.map(function(tag) {
|
|
||||||
return tag.title.toLowerCase().replace(/,/g, "_");
|
|
||||||
}),
|
|
||||||
rating: result.rating.average || 0,
|
|
||||||
series: seriesTitle || "",
|
|
||||||
cover: result.image,
|
|
||||||
url: "https://book.douban.com/subject/" + result.id,
|
|
||||||
source: {
|
|
||||||
id: "douban",
|
|
||||||
description: "Douban Books",
|
|
||||||
url: "https://book.douban.com/"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (book.rating > 0) {
|
|
||||||
book.rating /= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $book = $(templates.bookResult(book));
|
|
||||||
$book.find("img").on("click", function () {
|
|
||||||
populateForm(book);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#book-list").append($book);
|
|
||||||
});
|
|
||||||
dbDone = 2;
|
|
||||||
} else {
|
|
||||||
dbDone = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (cvResults.length > 0) {
|
|
||||||
if (cvDone < 2) {
|
|
||||||
cvResults.forEach(function(result) {
|
|
||||||
var seriesTitle = "";
|
|
||||||
if (result.volume.name) {
|
|
||||||
seriesTitle = result.volume.name;
|
|
||||||
}
|
|
||||||
var dateFomers = "";
|
|
||||||
if (result.store_date) {
|
|
||||||
dateFomers = result.store_date.split("-");
|
|
||||||
} else {
|
|
||||||
dateFomers = result.date_added.split("-");
|
|
||||||
}
|
|
||||||
var publishedYear = parseInt(dateFomers[0], 10);
|
|
||||||
var publishedMonth = parseInt(dateFomers[1], 10);
|
|
||||||
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
|
|
||||||
|
|
||||||
publishedDate = formatDate(publishedDate);
|
|
||||||
|
|
||||||
var book = {
|
|
||||||
id: result.id,
|
|
||||||
title: seriesTitle + " #" + ("00" + result.issue_number).slice(-3) + " - " + result.name,
|
|
||||||
authors: result.author || [],
|
|
||||||
description: result.description,
|
|
||||||
publisher: "",
|
|
||||||
publishedDate: publishedDate || "",
|
|
||||||
tags: ["Comics", seriesTitle],
|
|
||||||
rating: 0,
|
|
||||||
series: seriesTitle || "",
|
|
||||||
cover: result.image.original_url,
|
|
||||||
url: result.site_detail_url,
|
|
||||||
source: {
|
|
||||||
id: "comicvine",
|
|
||||||
description: "ComicVine Books",
|
|
||||||
url: "https://comicvine.gamespot.com/"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var $book = $(templates.bookResult(book));
|
|
||||||
$book.find("img").on("click", function () {
|
|
||||||
populateForm(book);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#book-list").append($book);
|
|
||||||
});
|
|
||||||
cvDone = 2;
|
|
||||||
} else {
|
|
||||||
cvDone = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ggSearchBook (title) {
|
|
||||||
$.ajax({
|
|
||||||
url: google + ggSearch + "?q=" + title.replace(/\s+/gm, "+"),
|
|
||||||
type: "GET",
|
|
||||||
dataType: "jsonp",
|
|
||||||
jsonp: "callback",
|
|
||||||
success: function success(data) {
|
|
||||||
if ("items" in data) {
|
|
||||||
ggResults = data.items;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
complete: function complete() {
|
|
||||||
ggDone = 1;
|
|
||||||
showResult();
|
|
||||||
$("#show-google").trigger("change");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function dbSearchBook (title) {
|
|
||||||
var apikey = "054022eaeae0b00e0fc068c0c0a2102a";
|
|
||||||
$.ajax({
|
|
||||||
url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10",
|
|
||||||
type: "GET",
|
|
||||||
dataType: "jsonp",
|
|
||||||
jsonp: "callback",
|
|
||||||
success: function success(data) {
|
|
||||||
dbResults = data.books;
|
|
||||||
},
|
|
||||||
error: function error() {
|
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
|
||||||
},
|
|
||||||
complete: function complete() {
|
|
||||||
dbDone = 1;
|
|
||||||
showResult();
|
|
||||||
$("#show-douban").trigger("change");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function cvSearchBook (title) {
|
|
||||||
var apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6";
|
|
||||||
title = encodeURIComponent(title);
|
|
||||||
$.ajax({
|
|
||||||
url: comicvine + cvSearch + "?api_key=" + apikey + "&resources=issue&query=" + title + "&sort=name:desc&format=jsonp",
|
|
||||||
type: "GET",
|
|
||||||
dataType: "jsonp",
|
|
||||||
jsonp: "json_callback",
|
|
||||||
success: function success(data) {
|
|
||||||
cvResults = data.results;
|
|
||||||
},
|
|
||||||
error: function error() {
|
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
|
||||||
},
|
|
||||||
complete: function complete() {
|
|
||||||
cvDone = 1;
|
|
||||||
showResult();
|
|
||||||
$("#show-comics").trigger("change");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function gsSearchBook (title) {
|
|
||||||
$.ajax({
|
|
||||||
url: googlescholar + gsSearch + title.replace(/\s+/gm,'+'),
|
|
||||||
type: "GET",
|
|
||||||
dataType: "json",
|
|
||||||
success: function success(data) {
|
|
||||||
gsResults = data;
|
|
||||||
},
|
|
||||||
complete: function complete() {
|
|
||||||
gsDone = 1;
|
|
||||||
showResult();
|
|
||||||
$("#show-googlescholar").trigger("change");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch (keyword) {
|
|
||||||
showFlag = 0;
|
|
||||||
dbDone = ggDone = cvDone = 0;
|
|
||||||
dbResults = [];
|
|
||||||
ggResults = [];
|
|
||||||
cvResults = [];
|
|
||||||
gsResults = [];
|
|
||||||
$("#meta-info").text(msg.loading);
|
|
||||||
if (keyword) {
|
|
||||||
dbSearchBook(keyword);
|
|
||||||
ggSearchBook(keyword);
|
|
||||||
cvSearchBook(keyword);
|
|
||||||
gsSearchBook(keyword);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$("#meta-search").on("submit", function (e) {
|
function doSearch (keyword) {
|
||||||
e.preventDefault();
|
if (keyword) {
|
||||||
var keyword = $("#keyword").val();
|
$("#meta-info").text(msg.loading);
|
||||||
if (keyword) {
|
$.ajax({
|
||||||
doSearch(keyword);
|
url: getPath() + "/metadata/search",
|
||||||
|
type: "POST",
|
||||||
|
data: {"query": keyword},
|
||||||
|
dataType: "json",
|
||||||
|
success: function success(data) {
|
||||||
|
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
||||||
|
data.forEach(function(book) {
|
||||||
|
var $book = $(templates.bookResult(book));
|
||||||
|
$book.find("img").on("click", function () {
|
||||||
|
populateForm(book);
|
||||||
|
});
|
||||||
|
$("#book-list").append($book);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function error() {
|
||||||
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$("#get_meta").click(function () {
|
function populate_provider() {
|
||||||
var bookTitle = $("#book_title").val();
|
$("#metadata_provider").empty();
|
||||||
if (bookTitle) {
|
$.ajax({
|
||||||
$("#keyword").val(bookTitle);
|
url: getPath() + "/metadata/provider",
|
||||||
doSearch(bookTitle);
|
type: "get",
|
||||||
|
dataType: "json",
|
||||||
|
success: function success(data) {
|
||||||
|
data.forEach(function(provider) {
|
||||||
|
var checked = "";
|
||||||
|
if (provider.active) {
|
||||||
|
checked = "checked";
|
||||||
|
}
|
||||||
|
var $provider_button = '<input type="checkbox" id="show-' + provider.name + '" class="pill" data-initial="' + provider.initial + '" data-control="' + provider.id + '" ' + checked + '><label for="show-' + provider.name + '">' + provider.name + ' <span class="glyphicon glyphicon-ok"></span></label>'
|
||||||
|
$("#metadata_provider").append($provider_button);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
$(document).on("change", ".pill", function () {
|
||||||
|
var element = $(this);
|
||||||
|
var id = element.data("control");
|
||||||
|
var initial = element.data("initial");
|
||||||
|
var val = element.prop('checked');
|
||||||
|
var params = {id : id, value: val};
|
||||||
|
if (!initial) {
|
||||||
|
params['initial'] = initial;
|
||||||
|
params['query'] = keyword;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/metadata/provider/" + id,
|
||||||
|
data: JSON.stringify(params),
|
||||||
|
success: function success(data) {
|
||||||
|
element.data("initial", "true");
|
||||||
|
data.forEach(function(book) {
|
||||||
|
var $book = $(templates.bookResult(book));
|
||||||
|
$book.find("img").on("click", function () {
|
||||||
|
populateForm(book);
|
||||||
|
});
|
||||||
|
$("#book-list").append($book);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#meta-search").on("submit", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
keyword = $("#keyword").val();
|
||||||
|
$('.pill').each(function(){
|
||||||
|
// console.log($(this).data('control'));
|
||||||
|
$(this).data("initial", $(this).prop('checked'));
|
||||||
|
// console.log($(this).data('initial'));
|
||||||
|
});
|
||||||
|
doSearch(keyword);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#get_meta").click(function () {
|
||||||
|
populate_provider();
|
||||||
|
var bookTitle = $("#book_title").val();
|
||||||
|
$("#keyword").val(bookTitle);
|
||||||
|
keyword = bookTitle;
|
||||||
|
doSearch(bookTitle);
|
||||||
|
});
|
||||||
|
$("#metaModal").on("show.bs.modal", function(e) {
|
||||||
|
$(e.relatedTarget).one('focus', function (e) {
|
||||||
|
$(this).blur();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -180,13 +180,15 @@ function initProgressClick() {
|
|||||||
|
|
||||||
function loadFromArrayBuffer(ab) {
|
function loadFromArrayBuffer(ab) {
|
||||||
var lastCompletion = 0;
|
var lastCompletion = 0;
|
||||||
|
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
|
||||||
loadArchiveFormats(['rar', 'zip', 'tar'], function() {
|
loadArchiveFormats(['rar', 'zip', 'tar'], function() {
|
||||||
// Open the file as an archive
|
// Open the file as an archive
|
||||||
archiveOpenFile(ab, function (archive) {
|
archiveOpenFile(ab, function (archive) {
|
||||||
if (archive) {
|
if (archive) {
|
||||||
totalImages = archive.entries.length
|
totalImages = archive.entries.length
|
||||||
console.info('Uncompressing ' + archive.archive_type + ' ...');
|
console.info('Uncompressing ' + archive.archive_type + ' ...');
|
||||||
archive.entries.forEach(function(e, i) {
|
entries = archive.entries.sort((a,b) => collator.compare(a.name, b.name));
|
||||||
|
entries.forEach(function(e, i) {
|
||||||
updateProgress( (i + 1)/ totalImages * 100);
|
updateProgress( (i + 1)/ totalImages * 100);
|
||||||
if (e.is_file) {
|
if (e.is_file) {
|
||||||
e.readData(function(d) {
|
e.readData(function(d) {
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
!function(a){a.fn.datepicker.dates["zh-TW"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["週日","週一","週二","週三","週四","週五","週六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",format:"yyyy年mm月dd日",weekStart:1,clear:"清除"}}(jQuery);
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user