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
|
||||
- User management with fine-grained per-user permissions
|
||||
- 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
|
||||
- Filter and search by titles, authors, tags, series and language
|
||||
- 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
|
||||
|
||||
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:
|
||||
|
||||
@@ -72,14 +72,7 @@ Optionally, to enable on-the-fly conversion from one ebook format to another whe
|
||||
|
||||
## Docker Images
|
||||
|
||||
Pre-built Docker images are available in these Docker Hub repositories:
|
||||
|
||||
#### **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`
|
||||
A pre-built Docker image is available in these Docker Hub repository (maintained by the LinuxServer team):
|
||||
|
||||
#### **LinuxServer - x64, armhf, aarch64**
|
||||
+ 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
|
||||
|
||||
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
|
||||
# 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 os
|
||||
|
||||
|
||||
# Insert local directories into path
|
||||
if sys.version_info < (3, 0):
|
||||
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__.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'))
|
||||
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
|
||||
@@ -42,13 +37,14 @@ from cps.admin import admi
|
||||
from cps.gdrive import gdrive
|
||||
from cps.editbooks import editbook
|
||||
from cps.remotelogin import remotelogin
|
||||
from cps.search_metadata import meta
|
||||
from cps.error_handler import init_errorhandler
|
||||
|
||||
try:
|
||||
from cps.kobo import kobo, get_kobo_activated
|
||||
from cps.kobo_auth import kobo_auth
|
||||
kobo_available = get_kobo_activated()
|
||||
except ImportError:
|
||||
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
|
||||
kobo_available = False
|
||||
|
||||
try:
|
||||
@@ -70,6 +66,7 @@ def main():
|
||||
app.register_blueprint(shelf)
|
||||
app.register_blueprint(admi)
|
||||
app.register_blueprint(remotelogin)
|
||||
app.register_blueprint(meta)
|
||||
app.register_blueprint(gdrive)
|
||||
app.register_blueprint(editbook)
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import mimetypes
|
||||
@@ -29,7 +28,7 @@ from babel import Locale as LC
|
||||
from babel import negotiate_locale
|
||||
from babel.core import UnknownLocaleError
|
||||
from flask import Flask, request, g
|
||||
from flask_login import LoginManager
|
||||
from .MyLoginManager import MyLoginManager
|
||||
from flask_babel import Babel
|
||||
from flask_principal import Principal
|
||||
|
||||
@@ -43,6 +42,12 @@ try:
|
||||
except ImportError:
|
||||
lxml_present = False
|
||||
|
||||
try:
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
wtf_present = True
|
||||
except ImportError:
|
||||
wtf_present = False
|
||||
|
||||
mimetypes.init()
|
||||
mimetypes.add_type('application/xhtml+xml', '.xhtml')
|
||||
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/ogg', '.ogg')
|
||||
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.config.update(
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
SESSION_COOKIE_SAMESITE='Lax',
|
||||
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.anonymous_user = ub.Anonymous
|
||||
lm.session_protection = 'strong'
|
||||
|
||||
if wtf_present:
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
else:
|
||||
csrf = None
|
||||
|
||||
ub.init_db(cli.settingspath)
|
||||
# pylint: disable=no-member
|
||||
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 ***')
|
||||
print(
|
||||
'*** 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)
|
||||
if not lxml_present:
|
||||
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" ***')
|
||||
web_server.stop(True)
|
||||
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)
|
||||
# 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'):
|
||||
cache_buster.init_cache_busting(app)
|
||||
@@ -139,7 +156,6 @@ def create_app():
|
||||
def get_locale():
|
||||
# if a user is logged in, use the locale from the user settings
|
||||
user = getattr(g, 'user', None)
|
||||
# user = None
|
||||
if user is not None and hasattr(user, "locale"):
|
||||
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
||||
return user.locale
|
||||
@@ -160,6 +176,7 @@ def get_timezone():
|
||||
user = getattr(g, 'user', None)
|
||||
return user.timezone if user else None
|
||||
|
||||
|
||||
from .updater import Updater
|
||||
updater_thread = Updater()
|
||||
updater_thread.start()
|
||||
|
||||
+13
-9
@@ -20,7 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import platform
|
||||
import sqlite3
|
||||
@@ -29,6 +28,10 @@ from collections import OrderedDict
|
||||
import babel, pytz, requests, sqlalchemy
|
||||
import werkzeug, flask, flask_login, flask_principal, jinja2
|
||||
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 .render_template import render_title_template
|
||||
@@ -75,6 +78,7 @@ _VERSIONS = OrderedDict(
|
||||
Flask=flask.__version__,
|
||||
Flask_Login=flask_loginVersion,
|
||||
Flask_Principal=flask_principal.__version__,
|
||||
Flask_WTF=flaskwtf_version,
|
||||
Werkzeug=werkzeug.__version__,
|
||||
Babel=babel.__version__,
|
||||
Jinja2=jinja2.__version__,
|
||||
@@ -84,14 +88,14 @@ _VERSIONS = OrderedDict(
|
||||
SQLite=sqlite3.sqlite_version,
|
||||
iso639=isoLanguages.__version__,
|
||||
pytz=pytz.__version__,
|
||||
Unidecode = unidecode_version,
|
||||
Scholarly = scholarly_version,
|
||||
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None,
|
||||
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
|
||||
Goodreads = u'installed' if bool(services.goodreads_support) else None,
|
||||
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||
flask_dance = flask_danceVersion,
|
||||
greenlet = greenlet_Version
|
||||
Unidecode=unidecode_version,
|
||||
Scholarly=scholarly_version,
|
||||
Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
|
||||
python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
|
||||
Goodreads=u'installed' if bool(services.goodreads_support) else None,
|
||||
jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||
flask_dance=flask_danceVersion,
|
||||
greenlet=greenlet_Version
|
||||
)
|
||||
_VERSIONS.update(uploader.get_versions())
|
||||
|
||||
|
||||
+24
-10
@@ -20,7 +20,6 @@
|
||||
# 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 os
|
||||
import re
|
||||
import base64
|
||||
@@ -41,7 +40,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
||||
from sqlalchemy.sql.expression import func, or_, text
|
||||
|
||||
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, \
|
||||
valid_email, check_username
|
||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||
@@ -146,7 +145,7 @@ def shutdown():
|
||||
else:
|
||||
showtext['text'] = _(u'Performing shutdown of server, please close window')
|
||||
# stop gevent/tornado server
|
||||
web_server.stop(task==0)
|
||||
web_server.stop(task == 0)
|
||||
return json.dumps(showtext)
|
||||
|
||||
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()
|
||||
restrict_columns = calibre_db.session.query(db.Custom_Columns)\
|
||||
.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,
|
||||
restrictColumns=restrict_columns,
|
||||
languages=languages,
|
||||
translations=translations,
|
||||
title=_(u"UI Configuration"), page="uiconfig")
|
||||
|
||||
@admi.route("/admin/usertable")
|
||||
@@ -515,16 +518,12 @@ def check_valid_restricted_column(column):
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@admi.route("/admin/viewconfig", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def update_view_configuration():
|
||||
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_columns_to_ignore")
|
||||
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_books_per_page")
|
||||
_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)
|
||||
@@ -1198,7 +1199,7 @@ def _configuration_update_helper():
|
||||
to_save = request.form.to_dict()
|
||||
try:
|
||||
reboot_required |= _config_int(to_save, "config_port")
|
||||
|
||||
reboot_required |= _config_string(to_save, "config_trustedhosts")
|
||||
reboot_required |= _config_string(to_save, "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'))
|
||||
@@ -1208,6 +1209,7 @@ def _configuration_update_helper():
|
||||
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_unicode_filename")
|
||||
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
||||
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
|
||||
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)
|
||||
config.load()
|
||||
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",
|
||||
config=config,
|
||||
@@ -1428,8 +1432,13 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||
else:
|
||||
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
|
||||
|
||||
# 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"):
|
||||
content.default_language = to_save["default_language"]
|
||||
if to_save.get("locale"):
|
||||
@@ -1485,6 +1494,8 @@ def new_user():
|
||||
else:
|
||||
content.role = config.config_default_role
|
||||
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,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||
@@ -1682,7 +1693,8 @@ def get_updater_status():
|
||||
"9": _(u'Update failed:') + u' ' + _(u'Connection error'),
|
||||
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
|
||||
"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
|
||||
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.email = useremail
|
||||
content.kindle_mail = kindlemail
|
||||
content.default_language = config.config_default_language
|
||||
content.locale = config.config_default_locale
|
||||
content.role = config.config_default_role
|
||||
content.sidebar_view = config.config_default_show
|
||||
content.allowed_tags = config.config_allowed_tags
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
|
||||
# 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 hashlib
|
||||
|
||||
|
||||
-14
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import 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')
|
||||
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")
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
|
||||
from . import logger, isoLanguages
|
||||
|
||||
+4
-7
@@ -16,8 +16,6 @@
|
||||
# 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 os
|
||||
import sys
|
||||
import json
|
||||
@@ -68,7 +66,7 @@ class _Settings(_Base):
|
||||
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
|
||||
config_certfile = Column(String)
|
||||
config_keyfile = Column(String)
|
||||
|
||||
config_trustedhosts = Column(String,default='')
|
||||
config_calibre_web_title = Column(String, default=u'Calibre-Web')
|
||||
config_books_per_page = Column(Integer, default=60)
|
||||
config_random_books = Column(Integer, default=4)
|
||||
@@ -91,6 +89,8 @@ class _Settings(_Base):
|
||||
|
||||
config_default_role = Column(SmallInteger, default=0)
|
||||
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_denied_tags = Column(String, default="")
|
||||
@@ -133,6 +133,7 @@ class _Settings(_Base):
|
||||
config_calibre = Column(String)
|
||||
config_rarfile_location = Column(String, default=None)
|
||||
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)
|
||||
|
||||
@@ -360,10 +361,6 @@ def _migrate_table(session, orm_class):
|
||||
session.query(column).first()
|
||||
except OperationalError as err:
|
||||
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:
|
||||
column_default = ""
|
||||
else:
|
||||
|
||||
+2
-8
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
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
|
||||
|
||||
# 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)).decode('utf-8')
|
||||
else:
|
||||
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))
|
||||
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
|
||||
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
|
||||
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, '
|
||||
'series_id, languages, publisher')
|
||||
|
||||
STABLE_VERSION = {'version': '0.6.13'}
|
||||
STABLE_VERSION = {'version': '0.6.14'}
|
||||
|
||||
NIGHTLY_VERSION = {}
|
||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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 os
|
||||
import re
|
||||
from flask_babel import gettext as _
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
@@ -387,8 +386,6 @@ class Custom_Columns(Base):
|
||||
|
||||
def get_display_dict(self):
|
||||
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
|
||||
|
||||
|
||||
@@ -398,7 +395,7 @@ class AlchemyEncoder(json.JSONEncoder):
|
||||
if isinstance(o.__class__, DeclarativeMeta):
|
||||
# an SQLAlchemy class
|
||||
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':
|
||||
continue
|
||||
data = o.__getattribute__(field)
|
||||
@@ -407,8 +404,11 @@ class AlchemyEncoder(json.JSONEncoder):
|
||||
data = data.replace("'", "\'")
|
||||
elif isinstance(data, InstrumentedList):
|
||||
el = list()
|
||||
# ele = None
|
||||
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())
|
||||
else:
|
||||
el.append(json.dumps(ele, cls=AlchemyEncoder))
|
||||
@@ -740,6 +740,7 @@ class CalibreDB():
|
||||
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||
entries = self.session.query(database).filter(tag_filter). \
|
||||
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])
|
||||
return json_dumps
|
||||
|
||||
@@ -805,11 +806,16 @@ class CalibreDB():
|
||||
.filter(self.common_filters()) \
|
||||
.group_by(text('books_languages_link.lang_code')).all()
|
||||
for lang in languages:
|
||||
try:
|
||||
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)
|
||||
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||
#try:
|
||||
# if lang.lang_code.lower() == "und":
|
||||
# lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||
# # 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
|
||||
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
from datetime import datetime
|
||||
import json
|
||||
@@ -40,14 +39,12 @@ try:
|
||||
except ImportError:
|
||||
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_babel import gettext as _
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.exc import OperationalError, IntegrityError
|
||||
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 calibre_db
|
||||
from .services.worker import WorkerThread
|
||||
@@ -462,6 +459,7 @@ def edit_book_series_index(series_index, book):
|
||||
modif_date = True
|
||||
return modif_date
|
||||
|
||||
|
||||
# Handle book comments/description
|
||||
def edit_book_comments(comments, book):
|
||||
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)
|
||||
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):
|
||||
changed = False
|
||||
def edit_all_cc_data(book_id, book, to_save):
|
||||
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:
|
||||
cc_string = "custom_column_" + str(c.id)
|
||||
if not c.is_multiple:
|
||||
@@ -814,7 +821,7 @@ def edit_book(book_id):
|
||||
# handle book ratings
|
||||
modif_date |= edit_book_ratings(to_save, book)
|
||||
# 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"]:
|
||||
try:
|
||||
@@ -826,6 +833,8 @@ def edit_book(book_id):
|
||||
|
||||
if modif_date:
|
||||
book.last_modified = datetime.utcnow()
|
||||
kobo_sync_status.remove_synced_book(edited_books_id)
|
||||
|
||||
calibre_db.session.merge(book)
|
||||
calibre_db.session.commit()
|
||||
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")
|
||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||
|
||||
@editbook.route("/scholarsearch/<query>",methods=['GET'])
|
||||
@login_required_if_no_ano
|
||||
@edit_required
|
||||
def scholar_search(query):
|
||||
if have_scholar:
|
||||
scholar_gen = scholarly.search_pubs(' '.join(query.split('+')))
|
||||
i=0
|
||||
result = []
|
||||
for publication in scholar_gen:
|
||||
del publication['source']
|
||||
result.append(publication)
|
||||
i+=1
|
||||
if(i>=10):
|
||||
break
|
||||
return Response(json.dumps(result),mimetype='application/json')
|
||||
else:
|
||||
return "[]"
|
||||
@editbook.route("/ajax/getcustomenum/<int:c_id>")
|
||||
@login_required
|
||||
def table_get_custom_enum(c_id):
|
||||
ret = list()
|
||||
cc = (calibre_db.session.query(db.Custom_Columns)
|
||||
.filter(db.Custom_Columns.id == c_id)
|
||||
.filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).one_or_none())
|
||||
ret.append({'value': "", 'text': ""})
|
||||
for idx, en in enumerate(cc.get_display_dict()['enum_values']):
|
||||
ret.append({'value': en, 'text': en})
|
||||
return json.dumps(ret)
|
||||
|
||||
|
||||
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
||||
@login_required_if_no_ano
|
||||
@@ -1131,10 +1135,7 @@ def edit_list_book(param):
|
||||
else:
|
||||
lang_names = list()
|
||||
for lang in book.languages:
|
||||
try:
|
||||
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))
|
||||
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
|
||||
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
|
||||
mimetype='application/json')
|
||||
elif param =='author_sort':
|
||||
@@ -1151,12 +1152,23 @@ def edit_list_book(param):
|
||||
book.sort = vals['value']
|
||||
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
|
||||
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':
|
||||
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])
|
||||
ret = Response(json.dumps({'success': True,
|
||||
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
|
||||
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()
|
||||
try:
|
||||
calibre_db.session.commit()
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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 os
|
||||
import zipfile
|
||||
from lxml import etree
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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
|
||||
from lxml import etree
|
||||
|
||||
from .constants import BookMeta
|
||||
|
||||
+38
-40
@@ -20,9 +20,7 @@
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import json
|
||||
import tempfile
|
||||
@@ -34,7 +32,7 @@ from flask import Blueprint, flash, request, redirect, url_for, abort
|
||||
from flask_babel import gettext as _
|
||||
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
|
||||
|
||||
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
|
||||
@@ -117,44 +115,44 @@ def revoke_watch_gdrive():
|
||||
config.save()
|
||||
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'])
|
||||
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 ''
|
||||
log.debug('%r', request.headers)
|
||||
log.debug('%r', request.data)
|
||||
log.info('Change received from gdrive')
|
||||
|
||||
log.debug('%r', request.headers)
|
||||
log.debug('%r', request.data)
|
||||
log.info('Change received from gdrive')
|
||||
|
||||
try:
|
||||
j = json.loads(request.data)
|
||||
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:
|
||||
try:
|
||||
j = json.loads(request.data)
|
||||
log.info('Getting change details')
|
||||
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
|
||||
log.debug('%r', response)
|
||||
if response:
|
||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||
if not os.path.isdir(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
|
||||
log.info('Database file updated')
|
||||
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
|
||||
log.info('Backing up existing and downloading updated metadata.db')
|
||||
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
|
||||
log.info('Setting up new DB')
|
||||
# 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)
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
return ''
|
||||
log.info('Database file updated')
|
||||
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
|
||||
log.info('Backing up existing and downloading updated metadata.db')
|
||||
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
|
||||
log.info('Setting up new DB')
|
||||
# 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)
|
||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
return ''
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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 os
|
||||
import json
|
||||
import shutil
|
||||
|
||||
+7
-18
@@ -17,7 +17,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import io
|
||||
@@ -234,16 +233,14 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||
value = value[:-1]+u'_'
|
||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||
if use_unidecode:
|
||||
value = (unidecode.unidecode(value))
|
||||
if not config.config_unicode_filename:
|
||||
value = (unidecode.unidecode(value))
|
||||
else:
|
||||
value = value.replace(u'§', u'SS')
|
||||
value = value.replace(u'ß', u'ss')
|
||||
value = unicodedata.normalize('NFKD', value)
|
||||
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
|
||||
if isinstance(value, str): # Python3 str, Python2 unicode
|
||||
value = re_slugify.sub('', value)
|
||||
else:
|
||||
value = unicode(re_slugify.sub('', value))
|
||||
value = re_slugify.sub('', value)
|
||||
if replace_whitespace:
|
||||
# *+:\"/<>? are replaced by _
|
||||
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()
|
||||
if not value:
|
||||
raise ValueError("Filename cannot be empty")
|
||||
if sys.version_info.major == 3:
|
||||
return value
|
||||
else:
|
||||
return value.decode('utf-8')
|
||||
return value
|
||||
|
||||
|
||||
def split_authors(values):
|
||||
@@ -495,10 +489,7 @@ def reset_password(user_id):
|
||||
def generate_random_password():
|
||||
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
|
||||
passlen = 8
|
||||
if sys.version_info < (3, 0):
|
||||
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))
|
||||
return "".join(s[c % len(s)] for c in os.urandom(passlen))
|
||||
|
||||
|
||||
def uniq(inpt):
|
||||
@@ -712,8 +703,6 @@ def check_unrar(unrarLocation):
|
||||
return _('Unrar binary file not found')
|
||||
|
||||
try:
|
||||
if sys.version_info < (3, 0):
|
||||
unrarLocation = unrarLocation.encode(sys.getfilesystemencoding())
|
||||
unrarLocation = [unrarLocation]
|
||||
value = process_wait(unrarLocation, pattern='UNRAR (.*) freeware')
|
||||
if value:
|
||||
@@ -842,8 +831,8 @@ def get_download_link(book_id, book_format, client):
|
||||
ub.update_download(book_id, int(current_user.id))
|
||||
file_name = book.title
|
||||
if len(book.authors) > 0:
|
||||
file_name = book.authors[0].name + '_' + file_name
|
||||
file_name = get_valid_filename(file_name)
|
||||
file_name = file_name + ' - ' + book.authors[0].name
|
||||
file_name = get_valid_filename(file_name, replace_whitespace=False)
|
||||
headers = Headers()
|
||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||
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
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
+373
-2
@@ -6,8 +6,6 @@
|
||||
|
||||
# pylint: disable=too-many-lines,bad-continuation
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# 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
|
||||
|
||||
@@ -7435,6 +7433,379 @@ LANGUAGE_NAMES = {
|
||||
"zxx": "No linguistic content",
|
||||
"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": {
|
||||
"aar": "Afar",
|
||||
"abk": "Abkhazian",
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
# custom jinja filters
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import datetime
|
||||
import mimetypes
|
||||
from uuid import uuid4
|
||||
|
||||
+41
-30
@@ -19,7 +19,6 @@
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import sys
|
||||
import os
|
||||
import uuid
|
||||
from time import gmtime, strftime
|
||||
@@ -47,7 +46,8 @@ from sqlalchemy.exc import StatementError
|
||||
from sqlalchemy.sql import select
|
||||
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 .helper import get_download_link
|
||||
from .services import SyncToken as SyncToken
|
||||
@@ -170,9 +170,12 @@ def HandleSyncRequest():
|
||||
ub.ArchivedBook.is_archived)
|
||||
changed_entries = (changed_entries
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.filter(or_(db.Books.last_modified > sync_token.books_last_modified,
|
||||
ub.BookShelf.date_added > sync_token.books_last_modified))
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS)).filter(calibre_db.common_filters())
|
||||
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
|
||||
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
|
||||
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(ub.ArchivedBook.last_modified)
|
||||
.join(ub.BookShelf, db.Books.id == ub.BookShelf.book_id)
|
||||
@@ -189,16 +192,16 @@ def HandleSyncRequest():
|
||||
ub.ArchivedBook.last_modified,
|
||||
ub.ArchivedBook.is_archived)
|
||||
changed_entries = (changed_entries
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.filter(db.Books.last_modified > sync_token.books_last_modified)
|
||||
.filter(calibre_db.common_filters())
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.order_by(db.Books.last_modified)
|
||||
.order_by(db.Books.id)
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
|
||||
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
|
||||
ub.KoboSyncedBooks.book_id == None))
|
||||
.filter(calibre_db.common_filters())
|
||||
.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 = []
|
||||
if sqlalchemy_version2:
|
||||
@@ -206,6 +209,7 @@ def HandleSyncRequest():
|
||||
else:
|
||||
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
||||
for book in books:
|
||||
kobo_sync_status.add_synced_books(book.Books.id)
|
||||
formats = [data.format for data in book.Books.data]
|
||||
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)
|
||||
@@ -263,11 +267,10 @@ def HandleSyncRequest():
|
||||
entries = calibre_db.session.execute(changed_entries).all()
|
||||
book_count = len(entries)
|
||||
else:
|
||||
entries = changed_entries.all()
|
||||
book_count = changed_entries.count()
|
||||
# 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
|
||||
changed_reading_states = ub.session.query(ub.KoboReadingState)
|
||||
|
||||
@@ -278,18 +281,18 @@ def HandleSyncRequest():
|
||||
.filter(current_user.id == ub.Shelf.user_id)\
|
||||
.filter(ub.Shelf.kobo_sync,
|
||||
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
|
||||
)).distinct()
|
||||
else:
|
||||
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(
|
||||
and_(ub.KoboReadingState.user_id == current_user.id,
|
||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))
|
||||
|
||||
for kobo_reading_state in changed_reading_states.all():
|
||||
cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT)
|
||||
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()
|
||||
if book:
|
||||
sync_results.append({
|
||||
@@ -305,9 +308,9 @@ def HandleSyncRequest():
|
||||
sync_token.books_last_modified = new_books_last_modified
|
||||
sync_token.archive_last_modified = new_archived_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):
|
||||
@@ -330,7 +333,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||
extra_headers["x-kobo-sync"] = "continue"
|
||||
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)
|
||||
|
||||
return response
|
||||
@@ -483,10 +486,7 @@ def get_metadata(book):
|
||||
metadata.update(get_author(book))
|
||||
|
||||
if get_series(book):
|
||||
if sys.version_info < (3, 0):
|
||||
name = get_series(book).encode("utf-8")
|
||||
else:
|
||||
name = get_series(book)
|
||||
name = get_series(book)
|
||||
metadata["Series"] = {
|
||||
"Name": get_series(book),
|
||||
"Number": get_seriesindex(book), # ToDo Check int() ?
|
||||
@@ -497,7 +497,7 @@ def get_metadata(book):
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags", methods=["POST", "DELETE"])
|
||||
@requires_kobo_auth
|
||||
# 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)
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
||||
@requires_kobo_auth
|
||||
def HandleTagUpdate(tag_id):
|
||||
@@ -587,6 +588,7 @@ def add_items_to_shelf(items, shelf):
|
||||
return items_unknown_to_calibre
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
def HandleTagAddItem(tag_id):
|
||||
@@ -616,6 +618,7 @@ def HandleTagAddItem(tag_id):
|
||||
return make_response('', 201)
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||
@requires_kobo_auth
|
||||
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 = []
|
||||
if only_kobo_shelves:
|
||||
@@ -757,7 +763,7 @@ def create_kobo_tag(shelf):
|
||||
)
|
||||
return {"Tag": tag}
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
||||
@requires_kobo_auth
|
||||
def HandleStateRequest(book_uuid):
|
||||
@@ -932,6 +938,7 @@ def TopLevelEndpoint():
|
||||
return make_response(jsonify({}))
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<book_uuid>", methods=["DELETE"])
|
||||
@requires_kobo_auth
|
||||
def HandleBookDeletionRequest(book_uuid):
|
||||
@@ -954,10 +961,11 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
|
||||
ub.session.merge(archived_book)
|
||||
ub.session_commit()
|
||||
return ("", 204)
|
||||
return "", 204
|
||||
|
||||
|
||||
# TODO: Implement the following routes
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||
def HandleUnimplementedRequest(dummy=None):
|
||||
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
||||
@@ -965,6 +973,7 @@ def HandleUnimplementedRequest(dummy=None):
|
||||
|
||||
|
||||
# TODO: Implement the following routes
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/user/loyalty/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/user/profile", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/user/wishlist", methods=["GET", "POST"])
|
||||
@@ -975,6 +984,7 @@ def HandleUserRequest(dummy=None):
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/recommendations", 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"])
|
||||
@requires_kobo_auth
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import os
|
||||
import sys
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
from flask import session
|
||||
|
||||
try:
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# 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 json
|
||||
from functools import wraps
|
||||
|
||||
|
||||
+13
-14
@@ -20,7 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import datetime
|
||||
from functools import wraps
|
||||
@@ -433,16 +432,17 @@ def feed_languagesindex():
|
||||
if current_user.filter_language() == u"all":
|
||||
languages = calibre_db.speaking_language()
|
||||
else:
|
||||
try:
|
||||
cur_l = LC.parse(current_user.filter_language())
|
||||
except UnknownLocaleError:
|
||||
cur_l = None
|
||||
#try:
|
||||
# cur_l = LC.parse(current_user.filter_language())
|
||||
#except UnknownLocaleError:
|
||||
# cur_l = None
|
||||
languages = calibre_db.session.query(db.Languages).filter(
|
||||
db.Languages.lang_code == current_user.filter_language()).all()
|
||||
if cur_l:
|
||||
languages[0].name = cur_l.get_language_name(get_locale())
|
||||
else:
|
||||
languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
|
||||
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
|
||||
#if cur_l:
|
||||
# languages[0].name = cur_l.get_language_name(get_locale())
|
||||
#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,
|
||||
len(languages))
|
||||
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):
|
||||
if sys.version_info.major == 3:
|
||||
try:
|
||||
username = username.encode('windows-1252')
|
||||
except UnicodeEncodeError:
|
||||
username = username.encode('utf-8')
|
||||
try:
|
||||
username = username.encode('windows-1252')
|
||||
except UnicodeEncodeError:
|
||||
username = username.encode('utf-8')
|
||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
|
||||
username.decode('utf-8').lower()).first()
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
from math import ceil
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
# http://flask.pocoo.org/snippets/62/
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
try:
|
||||
from urllib.parse import urlparse, urljoin
|
||||
except ImportError:
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
#
|
||||
# Inspired by http://flask.pocoo.org/snippets/35/
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
|
||||
class ReverseProxied(object):
|
||||
"""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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
import sys
|
||||
import os
|
||||
import 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):
|
||||
if sys.version_info < (3, 0):
|
||||
return b64encode(json.dumps(json_data))
|
||||
else:
|
||||
return b64encode(json.dumps(json_data).encode())
|
||||
return b64encode(json.dumps(json_data).encode())
|
||||
|
||||
|
||||
# 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"},
|
||||
"archive_last_modified": {"type": "string"},
|
||||
"reading_state_last_modified": {"type": "string"},
|
||||
"tags_last_modified": {"type": "string"},
|
||||
"books_last_id": {"type": "integer", "optional": True}
|
||||
"tags_last_modified": {"type": "string"}
|
||||
# "books_last_id": {"type": "integer", "optional": True}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -97,8 +94,8 @@ class SyncToken:
|
||||
books_last_modified=datetime.min,
|
||||
archive_last_modified=datetime.min,
|
||||
reading_state_last_modified=datetime.min,
|
||||
tags_last_modified=datetime.min,
|
||||
books_last_id=-1
|
||||
tags_last_modified=datetime.min
|
||||
# books_last_id=-1
|
||||
): # nosec
|
||||
self.raw_kobo_store_token = raw_kobo_store_token
|
||||
self.books_last_created = books_last_created
|
||||
@@ -106,7 +103,7 @@ class SyncToken:
|
||||
self.archive_last_modified = archive_last_modified
|
||||
self.reading_state_last_modified = reading_state_last_modified
|
||||
self.tags_last_modified = tags_last_modified
|
||||
self.books_last_id = books_last_id
|
||||
# self.books_last_id = books_last_id
|
||||
|
||||
@staticmethod
|
||||
def from_headers(headers):
|
||||
@@ -141,12 +138,12 @@ class SyncToken:
|
||||
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")
|
||||
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:
|
||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||
except KeyError:
|
||||
books_last_id = -1
|
||||
#except KeyError:
|
||||
# books_last_id = -1
|
||||
|
||||
return SyncToken(
|
||||
raw_kobo_store_token=raw_kobo_store_token,
|
||||
@@ -155,7 +152,7 @@ class SyncToken:
|
||||
archive_last_modified=archive_last_modified,
|
||||
reading_state_last_modified=reading_state_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):
|
||||
@@ -179,16 +176,16 @@ class SyncToken:
|
||||
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
||||
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_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)
|
||||
|
||||
def __str__(self):
|
||||
return "{},{},{},{},{},{},{}".format(self.raw_kobo_store_token,
|
||||
return "{},{},{},{},{},{}".format(self.raw_kobo_store_token,
|
||||
self.books_last_created,
|
||||
self.books_last_modified,
|
||||
self.archive_last_modified,
|
||||
self.reading_state_last_modified,
|
||||
self.tags_last_modified,
|
||||
self.books_last_id)
|
||||
self.tags_last_modified)
|
||||
#self.books_last_id)
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
# 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
|
||||
|
||||
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
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from google.auth.transport.requests import Request
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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 time
|
||||
from functools import reduce
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# 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 base64
|
||||
|
||||
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 abc
|
||||
import uuid
|
||||
@@ -205,10 +221,13 @@ class CalibreTask:
|
||||
# By default, we're good to clean a task if it's "Done"
|
||||
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
||||
|
||||
@progress.setter
|
||||
def progress(self, x):
|
||||
# todo: throw error if outside of [0,1]
|
||||
self._progress = x
|
||||
'''@progress.setter
|
||||
def progress(self, x):
|
||||
if x > 1:
|
||||
x = 1
|
||||
if x < 0:
|
||||
x = 0
|
||||
self._progress = x'''
|
||||
|
||||
def _handleError(self, error_message):
|
||||
self.stat = STAT_FAIL
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
+4
-4
@@ -35,7 +35,7 @@ body {
|
||||
float: left;
|
||||
width: 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-size: 500% 300%;
|
||||
}
|
||||
@@ -185,10 +185,10 @@ _:-ms-lang(x), .textLayer {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
background-color: #eeeeee !important;
|
||||
}
|
||||
.tt-dropdown-menu {
|
||||
.tt-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
@@ -166,7 +166,7 @@ fieldset[disabled] .twitter-typeahead .tt-input {
|
||||
*border-right-width: 2px;
|
||||
*border-bottom-width: 2px;
|
||||
}
|
||||
.tt-dropdown-menu .tt-suggestion {
|
||||
.tt-menu .tt-suggestion {
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
@@ -175,15 +175,15 @@ fieldset[disabled] .twitter-typeahead .tt-input {
|
||||
color: #333333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tt-dropdown-menu .tt-suggestion.tt-cursor {
|
||||
.tt-menu .tt-suggestion.tt-cursor {
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
background-color: #f5f5f5;
|
||||
color: #262626;
|
||||
}
|
||||
.tt-dropdown-menu .tt-suggestion.tt-cursor a {
|
||||
.tt-menu .tt-suggestion.tt-cursor a {
|
||||
color: #262626;
|
||||
}
|
||||
.tt-dropdown-menu .tt-suggestion p {
|
||||
.tt-menu .tt-suggestion p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
+13
-10
@@ -123,6 +123,10 @@ table .bg-dark-danger a { color: #fff; }
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row-fluid.text-center {
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
.container-fluid img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
@@ -151,11 +155,6 @@ table .bg-dark-danger a { color: #fff; }
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container-fluid .book .cover span.img {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
.author-bio img { margin: 0 1em 1em 0; }
|
||||
|
||||
.container-fluid .single .cover img {
|
||||
@@ -166,12 +165,16 @@ table .bg-dark-danger a { color: #fff; }
|
||||
box-shadow: 0 5px 8px -6px #777;
|
||||
}
|
||||
|
||||
.datepicker.form-control {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.container-fluid .book .cover span img {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
border: 1px solid #fff;
|
||||
bottom: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
box-sizing: border-box;
|
||||
-webkit-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; }
|
||||
.block-label { display: block; }
|
||||
|
||||
.fake-input {
|
||||
.form-control.fake-input {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
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 label = splitText.shift().trim();
|
||||
var value = splitText.join(':').trim();
|
||||
var class_value = ""
|
||||
// Preserve Links
|
||||
if ($(this).find('a').length) {
|
||||
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()
|
||||
|
||||
@@ -22,15 +22,17 @@ $(function() {
|
||||
});
|
||||
|
||||
$("#have_read_cb").on("change", function() {
|
||||
$.post({
|
||||
$.ajax({
|
||||
url: this.closest("form").action,
|
||||
method:"post",
|
||||
data: $(this).closest("form").serialize(),
|
||||
error: function(response) {
|
||||
var data = [{type:"danger", message:response.responseText}]
|
||||
$("#flash_success").remove();
|
||||
$("#flash_danger").remove();
|
||||
if (!jQuery.isEmptyObject(data)) {
|
||||
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>');
|
||||
});
|
||||
|
||||
+137
-140
@@ -6,6 +6,7 @@
|
||||
if ($("#description").length) {
|
||||
tinymce.init({
|
||||
selector: "#description",
|
||||
plugins: 'code',
|
||||
branding: false,
|
||||
menubar: "edit view format",
|
||||
language: language
|
||||
@@ -15,6 +16,7 @@ if ($("#description").length) {
|
||||
if ($(".tiny_editor").length) {
|
||||
tinymce.init({
|
||||
selector: ".tiny_editor",
|
||||
plugins: 'code',
|
||||
branding: false,
|
||||
menubar: "edit view format",
|
||||
language: language
|
||||
@@ -47,90 +49,20 @@ $(".datepicker_delete").click(function() {
|
||||
Takes a prefix, query typeahead callback, Bloodhound typeahead adapter
|
||||
and returns the completions it gets from the bloodhound engine prefixed.
|
||||
*/
|
||||
function prefixedSource(prefix, query, cb, bhAdapter) {
|
||||
bhAdapter(query, function(retArray) {
|
||||
function prefixedSource(prefix, query, cb, source) {
|
||||
function async(retArray) {
|
||||
retArray = retArray || [];
|
||||
var matches = [];
|
||||
for (var i = 0; i < retArray.length; i++) {
|
||||
var obj = {name : prefix + retArray[i].name};
|
||||
matches.push(obj);
|
||||
}
|
||||
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) {
|
||||
var bhAdapter = source.ttAdapter();
|
||||
|
||||
var tokens = query.split(split);
|
||||
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++) {
|
||||
prefix += tokens[i].trim() + newSplit;
|
||||
}
|
||||
prefixedSource(prefix, currentSource, cb, bhAdapter);
|
||||
prefixedSource(prefix, currentSource, cb, source);
|
||||
}
|
||||
|
||||
var promiseAuthors = authors.initialize();
|
||||
promiseAuthors.done(function() {
|
||||
$("#bookAuthor").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 1,
|
||||
hint: true
|
||||
}, {
|
||||
name: "authors",
|
||||
displayKey: "name",
|
||||
source: function source(query, cb) {
|
||||
return sourceSplit(query, cb, "&", authors); //sourceSplit //("&")
|
||||
}
|
||||
}
|
||||
);
|
||||
var authors = new Bloodhound({
|
||||
name: "authors",
|
||||
identify: function(obj) { return obj.name; },
|
||||
datumTokenizer: function datumTokenizer(datum) {
|
||||
return [datum.name];
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||
remote: {
|
||||
url: getPath() + "/get_authors_json?q=%QUERY",
|
||||
wildcard: '%QUERY',
|
||||
},
|
||||
});
|
||||
|
||||
var promiseSeries = series.initialize();
|
||||
promiseSeries.done(function() {
|
||||
$("#series").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "series",
|
||||
displayKey: "name",
|
||||
source: series.ttAdapter()
|
||||
$(".form-group #bookAuthor").typeahead(
|
||||
{
|
||||
highlight: true,
|
||||
minLength: 1,
|
||||
hint: true
|
||||
}, {
|
||||
name: "authors",
|
||||
display: 'name',
|
||||
source: function source(query, cb, asyncResults) {
|
||||
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();
|
||||
promiseTags.done(function() {
|
||||
$("#tags").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "tags",
|
||||
displayKey: "name",
|
||||
source: function source(query, cb) {
|
||||
return sourceSplit(query, cb, ",", tags);
|
||||
}
|
||||
$(".form-group #tags").typeahead(
|
||||
{
|
||||
highlight: true,
|
||||
minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "tags",
|
||||
display: "name",
|
||||
source: function source(query, cb, asyncResults) {
|
||||
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();
|
||||
promiseLanguages.done(function() {
|
||||
$("#languages").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "languages",
|
||||
displayKey: "name",
|
||||
source: function source(query, cb) {
|
||||
return sourceSplit(query, cb, ",", languages); //(",")
|
||||
}
|
||||
$(".form-group #languages").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "languages",
|
||||
display: "name",
|
||||
source: function source(query, cb, asyncResults) {
|
||||
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();
|
||||
promisePublishers.done(function() {
|
||||
$("#publisher").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "publishers",
|
||||
displayKey: "name",
|
||||
source: publishers.ttAdapter()
|
||||
}
|
||||
);
|
||||
});
|
||||
$(".form-group #publisher").typeahead(
|
||||
{
|
||||
highlight: true, minLength: 0,
|
||||
hint: true
|
||||
}, {
|
||||
name: "publishers",
|
||||
displayKey: "name",
|
||||
source: publishers
|
||||
}
|
||||
);
|
||||
|
||||
$("#search").on("change input.typeahead:selected", function(event) {
|
||||
if (event.target.type === "search" && event.target.tagName === "INPUT") {
|
||||
@@ -269,3 +265,4 @@ $("#xchange").click(function () {
|
||||
$("#book_title").val($("#bookAuthor").val());
|
||||
$("#bookAuthor").val(title);
|
||||
});
|
||||
|
||||
|
||||
+118
-364
@@ -14,380 +14,134 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* 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 = [];
|
||||
/* global _, i18nMsg, tinymce, getPath */
|
||||
|
||||
$(function () {
|
||||
var msg = i18nMsg;
|
||||
var douban = "https://api.douban.com";
|
||||
var dbSearch = "/v2/book/search";
|
||||
var dbDone = 0;
|
||||
var msg = i18nMsg;
|
||||
var keyword = ""
|
||||
|
||||
var google = "https://www.googleapis.com";
|
||||
var ggSearch = "/books/v1/volumes";
|
||||
var ggDone = 0;
|
||||
var templates = {
|
||||
bookResult: _.template(
|
||||
$("#template-book-result").html()
|
||||
)
|
||||
};
|
||||
|
||||
var comicvine = "https://comicvine.gamespot.com";
|
||||
var cvSearch = "/api/search/";
|
||||
var cvDone = 0;
|
||||
|
||||
var googlescholar = window.location.href.split('/admin/book')[0];
|
||||
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);
|
||||
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);
|
||||
});
|
||||
ggDone = 2;
|
||||
} else {
|
||||
ggDone = 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (gsResults.length > 0) {
|
||||
if (gsDone < 2) {
|
||||
gsResults.forEach(function(result) {
|
||||
var book = {
|
||||
id: generateID(result.bib.title),
|
||||
title: result.bib.title,
|
||||
authors: result.bib.author || [],
|
||||
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;
|
||||
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);
|
||||
}
|
||||
},
|
||||
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) {
|
||||
e.preventDefault();
|
||||
var keyword = $("#keyword").val();
|
||||
if (keyword) {
|
||||
doSearch(keyword);
|
||||
function doSearch (keyword) {
|
||||
if (keyword) {
|
||||
$("#meta-info").text(msg.loading);
|
||||
$.ajax({
|
||||
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 () {
|
||||
var bookTitle = $("#book_title").val();
|
||||
if (bookTitle) {
|
||||
$("#keyword").val(bookTitle);
|
||||
doSearch(bookTitle);
|
||||
function populate_provider() {
|
||||
$("#metadata_provider").empty();
|
||||
$.ajax({
|
||||
url: getPath() + "/metadata/provider",
|
||||
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) {
|
||||
var lastCompletion = 0;
|
||||
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
|
||||
loadArchiveFormats(['rar', 'zip', 'tar'], function() {
|
||||
// Open the file as an archive
|
||||
archiveOpenFile(ab, function (archive) {
|
||||
if (archive) {
|
||||
totalImages = archive.entries.length
|
||||
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);
|
||||
if (e.is_file) {
|
||||
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