Version Bump
Merge branch 'Develop'
This commit is contained in:
commit
623a92ebec
|
@ -62,7 +62,7 @@ Please note that running the above install command can fail on some versions of
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
python 3.x+
|
python 3.5+
|
||||||
|
|
||||||
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
|
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
|
||||||
|
|
||||||
|
|
2
cps.py
2
cps.py
|
@ -42,6 +42,7 @@ from cps.admin import admi
|
||||||
from cps.gdrive import gdrive
|
from cps.gdrive import gdrive
|
||||||
from cps.editbooks import editbook
|
from cps.editbooks import editbook
|
||||||
from cps.remotelogin import remotelogin
|
from cps.remotelogin import remotelogin
|
||||||
|
from cps.search_metadata import meta
|
||||||
from cps.error_handler import init_errorhandler
|
from cps.error_handler import init_errorhandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -70,6 +71,7 @@ def main():
|
||||||
app.register_blueprint(shelf)
|
app.register_blueprint(shelf)
|
||||||
app.register_blueprint(admi)
|
app.register_blueprint(admi)
|
||||||
app.register_blueprint(remotelogin)
|
app.register_blueprint(remotelogin)
|
||||||
|
app.register_blueprint(meta)
|
||||||
app.register_blueprint(gdrive)
|
app.register_blueprint(gdrive)
|
||||||
app.register_blueprint(editbook)
|
app.register_blueprint(editbook)
|
||||||
if kobo_available:
|
if kobo_available:
|
||||||
|
|
|
@ -139,7 +139,6 @@ def create_app():
|
||||||
def get_locale():
|
def get_locale():
|
||||||
# if a user is logged in, use the locale from the user settings
|
# if a user is logged in, use the locale from the user settings
|
||||||
user = getattr(g, 'user', None)
|
user = getattr(g, 'user', None)
|
||||||
# user = None
|
|
||||||
if user is not None and hasattr(user, "locale"):
|
if user is not None and hasattr(user, "locale"):
|
||||||
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
||||||
return user.locale
|
return user.locale
|
||||||
|
@ -160,6 +159,7 @@ def get_timezone():
|
||||||
user = getattr(g, 'user', None)
|
user = getattr(g, 'user', None)
|
||||||
return user.timezone if user else None
|
return user.timezone if user else None
|
||||||
|
|
||||||
|
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
updater_thread = Updater()
|
updater_thread = Updater()
|
||||||
updater_thread.start()
|
updater_thread.start()
|
||||||
|
|
|
@ -1208,6 +1208,7 @@ def _configuration_update_helper():
|
||||||
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'))
|
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'))
|
||||||
|
|
||||||
_config_checkbox_int(to_save, "config_uploading")
|
_config_checkbox_int(to_save, "config_uploading")
|
||||||
|
_config_checkbox_int(to_save, "config_unicode_filename")
|
||||||
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
|
||||||
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
|
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
|
||||||
and config.config_login_type == constants.LOGIN_LDAP)
|
and config.config_login_type == constants.LOGIN_LDAP)
|
||||||
|
@ -1310,6 +1311,8 @@ def _db_configuration_result(error_flash=None, gdrive_error=None):
|
||||||
log.error(error_flash)
|
log.error(error_flash)
|
||||||
config.load()
|
config.load()
|
||||||
flash(error_flash, category="error")
|
flash(error_flash, category="error")
|
||||||
|
elif request.method == "POST" and not gdrive_error:
|
||||||
|
flash(_("Database Settings updated"), category="success")
|
||||||
|
|
||||||
return render_title_template("config_db.html",
|
return render_title_template("config_db.html",
|
||||||
config=config,
|
config=config,
|
||||||
|
@ -1682,7 +1685,8 @@ def get_updater_status():
|
||||||
"9": _(u'Update failed:') + u' ' + _(u'Connection error'),
|
"9": _(u'Update failed:') + u' ' + _(u'Connection error'),
|
||||||
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
|
"10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'),
|
||||||
"11": _(u'Update failed:') + u' ' + _(u'General error'),
|
"11": _(u'Update failed:') + u' ' + _(u'General error'),
|
||||||
"12": _(u'Update failed:') + u' ' + _(u'Update File Could Not be Saved in Temp Dir')
|
"12": _(u'Update failed:') + u' ' + _(u'Update file could not be saved in temp dir'),
|
||||||
|
"13": _(u'Update failed:') + u' ' + _(u'Files could not be replaced during update')
|
||||||
}
|
}
|
||||||
status['text'] = text
|
status['text'] = text
|
||||||
updater_thread.status = 0
|
updater_thread.status = 0
|
||||||
|
|
|
@ -133,6 +133,7 @@ class _Settings(_Base):
|
||||||
config_calibre = Column(String)
|
config_calibre = Column(String)
|
||||||
config_rarfile_location = Column(String, default=None)
|
config_rarfile_location = Column(String, default=None)
|
||||||
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
|
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
|
||||||
|
config_unicode_filename =Column(Boolean, default=False)
|
||||||
|
|
||||||
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ def selected_roles(dictionary):
|
||||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||||
'series_id, languages, publisher')
|
'series_id, languages, publisher')
|
||||||
|
|
||||||
STABLE_VERSION = {'version': '0.6.13'}
|
STABLE_VERSION = {'version': '0.6.14 Beta'}
|
||||||
|
|
||||||
NIGHTLY_VERSION = {}
|
NIGHTLY_VERSION = {}
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
|
|
|
@ -234,16 +234,14 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||||
value = value[:-1]+u'_'
|
value = value[:-1]+u'_'
|
||||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||||
if use_unidecode:
|
if use_unidecode:
|
||||||
value = (unidecode.unidecode(value))
|
if not config.config_unicode_filename:
|
||||||
|
value = (unidecode.unidecode(value))
|
||||||
else:
|
else:
|
||||||
value = value.replace(u'§', u'SS')
|
value = value.replace(u'§', u'SS')
|
||||||
value = value.replace(u'ß', u'ss')
|
value = value.replace(u'ß', u'ss')
|
||||||
value = unicodedata.normalize('NFKD', value)
|
value = unicodedata.normalize('NFKD', value)
|
||||||
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
|
re_slugify = re.compile(r'[\W\s-]', re.UNICODE)
|
||||||
if isinstance(value, str): # Python3 str, Python2 unicode
|
value = re_slugify.sub('', value)
|
||||||
value = re_slugify.sub('', value)
|
|
||||||
else:
|
|
||||||
value = unicode(re_slugify.sub('', value))
|
|
||||||
if replace_whitespace:
|
if replace_whitespace:
|
||||||
# *+:\"/<>? are replaced by _
|
# *+:\"/<>? are replaced by _
|
||||||
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
value = re.sub(r'[*+:\\\"/<>?]+', u'_', value, flags=re.U)
|
||||||
|
@ -252,10 +250,7 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||||
value = value[:128].strip()
|
value = value[:128].strip()
|
||||||
if not value:
|
if not value:
|
||||||
raise ValueError("Filename cannot be empty")
|
raise ValueError("Filename cannot be empty")
|
||||||
if sys.version_info.major == 3:
|
return value
|
||||||
return value
|
|
||||||
else:
|
|
||||||
return value.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
def split_authors(values):
|
def split_authors(values):
|
||||||
|
@ -842,8 +837,8 @@ def get_download_link(book_id, book_format, client):
|
||||||
ub.update_download(book_id, int(current_user.id))
|
ub.update_download(book_id, int(current_user.id))
|
||||||
file_name = book.title
|
file_name = book.title
|
||||||
if len(book.authors) > 0:
|
if len(book.authors) > 0:
|
||||||
file_name = book.authors[0].name + '_' + file_name
|
file_name = file_name + ' - ' + book.authors[0].name
|
||||||
file_name = get_valid_filename(file_name)
|
file_name = get_valid_filename(file_name, replace_whitespace=False)
|
||||||
headers = Headers()
|
headers = Headers()
|
||||||
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
|
||||||
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
headers["Content-Disposition"] = "attachment; filename=%s.%s; filename*=UTF-8''%s.%s" % (
|
||||||
|
|
65
cps/metadata_provider/comicvine.py
Normal file
65
cps/metadata_provider/comicvine.py
Normal file
|
@ -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
|
||||||
|
|
||||||
|
|
55
cps/metadata_provider/google.py
Normal file
55
cps/metadata_provider/google.py
Normal file
|
@ -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
|
||||||
|
|
||||||
|
|
61
cps/metadata_provider/scholar.py
Normal file
61
cps/metadata_provider/scholar.py
Normal file
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
118
cps/search_metadata.py
Normal file
118
cps/search_metadata.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
# -*- 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 __future__ import division, print_function, unicode_literals
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
27
cps/services/Metadata.py
Normal file
27
cps/services/Metadata.py
Normal file
|
@ -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
|
|
@ -205,10 +205,13 @@ class CalibreTask:
|
||||||
# By default, we're good to clean a task if it's "Done"
|
# By default, we're good to clean a task if it's "Done"
|
||||||
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
||||||
|
|
||||||
@progress.setter
|
'''@progress.setter
|
||||||
def progress(self, x):
|
def progress(self, x):
|
||||||
# todo: throw error if outside of [0,1]
|
if x > 1:
|
||||||
self._progress = x
|
x = 1
|
||||||
|
if x < 0:
|
||||||
|
x = 0
|
||||||
|
self._progress = x'''
|
||||||
|
|
||||||
def _handleError(self, error_message):
|
def _handleError(self, error_message):
|
||||||
self.stat = STAT_FAIL
|
self.stat = STAT_FAIL
|
||||||
|
|
|
@ -123,6 +123,10 @@ table .bg-dark-danger a { color: #fff; }
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-fluid.text-center {
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
.container-fluid img {
|
.container-fluid img {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -166,6 +170,10 @@ table .bg-dark-danger a { color: #fff; }
|
||||||
box-shadow: 0 5px 8px -6px #777;
|
box-shadow: 0 5px 8px -6px #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.datepicker.form-control {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
.container-fluid .book .cover span img {
|
.container-fluid .book .cover span img {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -322,7 +330,7 @@ table .bg-dark-danger:hover { background-color: #c9302c; }
|
||||||
table .bg-primary:hover { background-color: #1c5484; }
|
table .bg-primary:hover { background-color: #1c5484; }
|
||||||
.block-label { display: block; }
|
.block-label { display: block; }
|
||||||
|
|
||||||
.fake-input {
|
.form-control.fake-input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
@ -30,7 +30,7 @@ $("#have_read_cb").on("change", function() {
|
||||||
$("#flash_danger").remove();
|
$("#flash_danger").remove();
|
||||||
if (!jQuery.isEmptyObject(data)) {
|
if (!jQuery.isEmptyObject(data)) {
|
||||||
data.forEach(function (item) {
|
data.forEach(function (item) {
|
||||||
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' +
|
$(".navbar").after('<div class="row-fluid text-center" >' +
|
||||||
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
});
|
});
|
||||||
|
|
|
@ -269,3 +269,4 @@ $("#xchange").click(function () {
|
||||||
$("#book_title").val($("#bookAuthor").val());
|
$("#book_title").val($("#bookAuthor").val());
|
||||||
$("#bookAuthor").val(title);
|
$("#bookAuthor").val(title);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,380 +14,134 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
/*
|
/* global _, i18nMsg, tinymce, getPath */
|
||||||
* Get Metadata from Douban Books api and Google Books api and ComicVine
|
|
||||||
* Google Books api document: https://developers.google.com/books/docs/v1/using
|
|
||||||
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
|
|
||||||
* ComicVine api document: https://comicvine.gamespot.com/api/documentation
|
|
||||||
*/
|
|
||||||
/* global _, i18nMsg, tinymce */
|
|
||||||
var dbResults = [];
|
|
||||||
var ggResults = [];
|
|
||||||
var cvResults = [];
|
|
||||||
var gsResults = [];
|
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
var msg = i18nMsg;
|
var msg = i18nMsg;
|
||||||
var douban = "https://api.douban.com";
|
var keyword = ""
|
||||||
var dbSearch = "/v2/book/search";
|
|
||||||
var dbDone = 0;
|
|
||||||
|
|
||||||
var google = "https://www.googleapis.com";
|
var templates = {
|
||||||
var ggSearch = "/books/v1/volumes";
|
bookResult: _.template(
|
||||||
var ggDone = 0;
|
$("#template-book-result").html()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
var comicvine = "https://comicvine.gamespot.com";
|
function populateForm (book) {
|
||||||
var cvSearch = "/api/search/";
|
tinymce.get("description").setContent(book.description);
|
||||||
var cvDone = 0;
|
var uniqueTags = [];
|
||||||
|
$.each(book.tags, function(i, el) {
|
||||||
var googlescholar = window.location.href.split('/admin/book')[0];
|
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
|
||||||
var gsSearch = "/scholarsearch/"
|
|
||||||
var gsDone = 0;
|
|
||||||
|
|
||||||
var showFlag = 0;
|
|
||||||
|
|
||||||
var templates = {
|
|
||||||
bookResult: _.template(
|
|
||||||
$("#template-book-result").html()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
function populateForm (book) {
|
|
||||||
tinymce.get("description").setContent(book.description);
|
|
||||||
var uniqueTags = [];
|
|
||||||
$.each(book.tags, function(i, el) {
|
|
||||||
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
var ampSeparatedAuthors = (book.authors || []).join(" & ");
|
|
||||||
$("#bookAuthor").val(ampSeparatedAuthors);
|
|
||||||
$("#book_title").val(book.title);
|
|
||||||
$("#tags").val(uniqueTags.join(","));
|
|
||||||
$("#rating").data("rating").setValue(Math.round(book.rating));
|
|
||||||
if(book.cover !== null){
|
|
||||||
$(".cover img").attr("src", book.cover);
|
|
||||||
$("#cover_url").val(book.cover);
|
|
||||||
}
|
|
||||||
$("#pubdate").val(book.publishedDate);
|
|
||||||
$("#publisher").val(book.publisher);
|
|
||||||
if (typeof book.series !== "undefined") {
|
|
||||||
$("#series").val(book.series);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showResult () {
|
|
||||||
showFlag++;
|
|
||||||
if (showFlag === 1) {
|
|
||||||
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
|
||||||
}
|
|
||||||
if ((ggDone === 3 || (ggDone === 1 && ggResults.length === 0)) &&
|
|
||||||
(dbDone === 3 || (dbDone === 1 && dbResults.length === 0)) &&
|
|
||||||
(cvDone === 3 || (cvDone === 1 && cvResults.length === 0)) &&
|
|
||||||
(gsDone === 3 || (gsDone === 1 && gsResults.length === 0))) {
|
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
function formatDate (date) {
|
|
||||||
var d = new Date(date),
|
|
||||||
month = "" + (d.getMonth() + 1),
|
|
||||||
day = "" + d.getDate(),
|
|
||||||
year = d.getFullYear();
|
|
||||||
|
|
||||||
if (month.length < 2) {
|
|
||||||
month = "0" + month;
|
|
||||||
}
|
|
||||||
if (day.length < 2) {
|
|
||||||
day = "0" + day;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [year, month, day].join("-");
|
|
||||||
}
|
|
||||||
function generateID (title) {
|
|
||||||
return title.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0).toString().substr(0,12);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ggResults.length > 0) {
|
|
||||||
if (ggDone < 2) {
|
|
||||||
ggResults.forEach(function(result) {
|
|
||||||
var book = {
|
|
||||||
id: result.id,
|
|
||||||
title: result.volumeInfo.title,
|
|
||||||
authors: result.volumeInfo.authors || [],
|
|
||||||
description: result.volumeInfo.description || "",
|
|
||||||
publisher: result.volumeInfo.publisher || "",
|
|
||||||
publishedDate: result.volumeInfo.publishedDate || "",
|
|
||||||
tags: result.volumeInfo.categories || [],
|
|
||||||
rating: result.volumeInfo.averageRating || 0,
|
|
||||||
cover: result.volumeInfo.imageLinks ?
|
|
||||||
result.volumeInfo.imageLinks.thumbnail : location + "/../../../static/generic_cover.jpg",
|
|
||||||
url: "https://books.google.com/books?id=" + result.id,
|
|
||||||
source: {
|
|
||||||
id: "google",
|
|
||||||
description: "Google Books",
|
|
||||||
url: "https://books.google.com/"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var $book = $(templates.bookResult(book));
|
|
||||||
$book.find("img").on("click", function () {
|
|
||||||
populateForm(book);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#book-list").append($book);
|
|
||||||
});
|
});
|
||||||
ggDone = 2;
|
var ampSeparatedAuthors = (book.authors || []).join(" & ");
|
||||||
} else {
|
$("#bookAuthor").val(ampSeparatedAuthors);
|
||||||
ggDone = 3;
|
$("#book_title").val(book.title);
|
||||||
}
|
$("#tags").val(uniqueTags.join(","));
|
||||||
}
|
$("#rating").data("rating").setValue(Math.round(book.rating));
|
||||||
|
if(book.cover !== null){
|
||||||
if (gsResults.length > 0) {
|
$(".cover img").attr("src", book.cover);
|
||||||
if (gsDone < 2) {
|
$("#cover_url").val(book.cover);
|
||||||
gsResults.forEach(function(result) {
|
}
|
||||||
var book = {
|
$("#pubdate").val(book.publishedDate);
|
||||||
id: generateID(result.bib.title),
|
$("#publisher").val(book.publisher);
|
||||||
title: result.bib.title,
|
if (typeof book.series !== "undefined") {
|
||||||
authors: result.bib.author || [],
|
$("#series").val(book.series);
|
||||||
description: result.bib.abstract || "",
|
|
||||||
publisher: result.bib.venue || "",
|
|
||||||
publishedDate: result.bib.pub_year ? result.bib.pub_year+"-01-01" : "",
|
|
||||||
tags: [],
|
|
||||||
rating: 0,
|
|
||||||
series: "",
|
|
||||||
cover: null,
|
|
||||||
url: result.pub_url || result.eprint_url || "",
|
|
||||||
source: {
|
|
||||||
id: "googlescholar",
|
|
||||||
description: "Google Scholar",
|
|
||||||
link: "https://scholar.google.com/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var $book = $(templates.bookResult(book));
|
|
||||||
$book.find("img").on("click", function () {
|
|
||||||
populateForm(book);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#book-list").append($book);
|
|
||||||
|
|
||||||
});
|
|
||||||
gsDone = 2;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
gsDone = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dbResults.length > 0) {
|
|
||||||
if (dbDone < 2) {
|
|
||||||
dbResults.forEach(function(result) {
|
|
||||||
var seriesTitle = "";
|
|
||||||
if (result.series) {
|
|
||||||
seriesTitle = result.series.title;
|
|
||||||
}
|
|
||||||
var dateFomers = result.pubdate.split("-");
|
|
||||||
var publishedYear = parseInt(dateFomers[0], 10);
|
|
||||||
var publishedMonth = parseInt(dateFomers[1], 10);
|
|
||||||
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
|
|
||||||
|
|
||||||
publishedDate = formatDate(publishedDate);
|
|
||||||
|
|
||||||
var book = {
|
|
||||||
id: result.id,
|
|
||||||
title: result.title,
|
|
||||||
authors: result.author || [],
|
|
||||||
description: result.summary,
|
|
||||||
publisher: result.publisher || "",
|
|
||||||
publishedDate: publishedDate || "",
|
|
||||||
tags: result.tags.map(function(tag) {
|
|
||||||
return tag.title.toLowerCase().replace(/,/g, "_");
|
|
||||||
}),
|
|
||||||
rating: result.rating.average || 0,
|
|
||||||
series: seriesTitle || "",
|
|
||||||
cover: result.image,
|
|
||||||
url: "https://book.douban.com/subject/" + result.id,
|
|
||||||
source: {
|
|
||||||
id: "douban",
|
|
||||||
description: "Douban Books",
|
|
||||||
url: "https://book.douban.com/"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (book.rating > 0) {
|
|
||||||
book.rating /= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $book = $(templates.bookResult(book));
|
|
||||||
$book.find("img").on("click", function () {
|
|
||||||
populateForm(book);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#book-list").append($book);
|
|
||||||
});
|
|
||||||
dbDone = 2;
|
|
||||||
} else {
|
|
||||||
dbDone = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (cvResults.length > 0) {
|
|
||||||
if (cvDone < 2) {
|
|
||||||
cvResults.forEach(function(result) {
|
|
||||||
var seriesTitle = "";
|
|
||||||
if (result.volume.name) {
|
|
||||||
seriesTitle = result.volume.name;
|
|
||||||
}
|
|
||||||
var dateFomers = "";
|
|
||||||
if (result.store_date) {
|
|
||||||
dateFomers = result.store_date.split("-");
|
|
||||||
} else {
|
|
||||||
dateFomers = result.date_added.split("-");
|
|
||||||
}
|
|
||||||
var publishedYear = parseInt(dateFomers[0], 10);
|
|
||||||
var publishedMonth = parseInt(dateFomers[1], 10);
|
|
||||||
var publishedDate = new Date(publishedYear, publishedMonth - 1, 1);
|
|
||||||
|
|
||||||
publishedDate = formatDate(publishedDate);
|
|
||||||
|
|
||||||
var book = {
|
|
||||||
id: result.id,
|
|
||||||
title: seriesTitle + " #" + ("00" + result.issue_number).slice(-3) + " - " + result.name,
|
|
||||||
authors: result.author || [],
|
|
||||||
description: result.description,
|
|
||||||
publisher: "",
|
|
||||||
publishedDate: publishedDate || "",
|
|
||||||
tags: ["Comics", seriesTitle],
|
|
||||||
rating: 0,
|
|
||||||
series: seriesTitle || "",
|
|
||||||
cover: result.image.original_url,
|
|
||||||
url: result.site_detail_url,
|
|
||||||
source: {
|
|
||||||
id: "comicvine",
|
|
||||||
description: "ComicVine Books",
|
|
||||||
url: "https://comicvine.gamespot.com/"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var $book = $(templates.bookResult(book));
|
|
||||||
$book.find("img").on("click", function () {
|
|
||||||
populateForm(book);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#book-list").append($book);
|
|
||||||
});
|
|
||||||
cvDone = 2;
|
|
||||||
} else {
|
|
||||||
cvDone = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ggSearchBook (title) {
|
|
||||||
$.ajax({
|
|
||||||
url: google + ggSearch + "?q=" + title.replace(/\s+/gm, "+"),
|
|
||||||
type: "GET",
|
|
||||||
dataType: "jsonp",
|
|
||||||
jsonp: "callback",
|
|
||||||
success: function success(data) {
|
|
||||||
if ("items" in data) {
|
|
||||||
ggResults = data.items;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
complete: function complete() {
|
|
||||||
ggDone = 1;
|
|
||||||
showResult();
|
|
||||||
$("#show-google").trigger("change");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function dbSearchBook (title) {
|
|
||||||
var apikey = "054022eaeae0b00e0fc068c0c0a2102a";
|
|
||||||
$.ajax({
|
|
||||||
url: douban + dbSearch + "?apikey=" + apikey + "&q=" + title + "&fields=all&count=10",
|
|
||||||
type: "GET",
|
|
||||||
dataType: "jsonp",
|
|
||||||
jsonp: "callback",
|
|
||||||
success: function success(data) {
|
|
||||||
dbResults = data.books;
|
|
||||||
},
|
|
||||||
error: function error() {
|
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
|
||||||
},
|
|
||||||
complete: function complete() {
|
|
||||||
dbDone = 1;
|
|
||||||
showResult();
|
|
||||||
$("#show-douban").trigger("change");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function cvSearchBook (title) {
|
|
||||||
var apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6";
|
|
||||||
title = encodeURIComponent(title);
|
|
||||||
$.ajax({
|
|
||||||
url: comicvine + cvSearch + "?api_key=" + apikey + "&resources=issue&query=" + title + "&sort=name:desc&format=jsonp",
|
|
||||||
type: "GET",
|
|
||||||
dataType: "jsonp",
|
|
||||||
jsonp: "json_callback",
|
|
||||||
success: function success(data) {
|
|
||||||
cvResults = data.results;
|
|
||||||
},
|
|
||||||
error: function error() {
|
|
||||||
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
|
||||||
},
|
|
||||||
complete: function complete() {
|
|
||||||
cvDone = 1;
|
|
||||||
showResult();
|
|
||||||
$("#show-comics").trigger("change");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function gsSearchBook (title) {
|
|
||||||
$.ajax({
|
|
||||||
url: googlescholar + gsSearch + title.replace(/\s+/gm,'+'),
|
|
||||||
type: "GET",
|
|
||||||
dataType: "json",
|
|
||||||
success: function success(data) {
|
|
||||||
gsResults = data;
|
|
||||||
},
|
|
||||||
complete: function complete() {
|
|
||||||
gsDone = 1;
|
|
||||||
showResult();
|
|
||||||
$("#show-googlescholar").trigger("change");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch (keyword) {
|
|
||||||
showFlag = 0;
|
|
||||||
dbDone = ggDone = cvDone = 0;
|
|
||||||
dbResults = [];
|
|
||||||
ggResults = [];
|
|
||||||
cvResults = [];
|
|
||||||
gsResults = [];
|
|
||||||
$("#meta-info").text(msg.loading);
|
|
||||||
if (keyword) {
|
|
||||||
dbSearchBook(keyword);
|
|
||||||
ggSearchBook(keyword);
|
|
||||||
cvSearchBook(keyword);
|
|
||||||
gsSearchBook(keyword);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$("#meta-search").on("submit", function (e) {
|
function doSearch (keyword) {
|
||||||
e.preventDefault();
|
if (keyword) {
|
||||||
var keyword = $("#keyword").val();
|
$("#meta-info").text(msg.loading);
|
||||||
if (keyword) {
|
$.ajax({
|
||||||
doSearch(keyword);
|
url: getPath() + "/metadata/search",
|
||||||
|
type: "POST",
|
||||||
|
data: {"query": keyword},
|
||||||
|
dataType: "json",
|
||||||
|
success: function success(data) {
|
||||||
|
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
||||||
|
data.forEach(function(book) {
|
||||||
|
var $book = $(templates.bookResult(book));
|
||||||
|
$book.find("img").on("click", function () {
|
||||||
|
populateForm(book);
|
||||||
|
});
|
||||||
|
$("#book-list").append($book);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function error() {
|
||||||
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$("#get_meta").click(function () {
|
function populate_provider() {
|
||||||
var bookTitle = $("#book_title").val();
|
$("#metadata_provider").empty();
|
||||||
if (bookTitle) {
|
$.ajax({
|
||||||
$("#keyword").val(bookTitle);
|
url: getPath() + "/metadata/provider",
|
||||||
doSearch(bookTitle);
|
type: "get",
|
||||||
|
dataType: "json",
|
||||||
|
success: function success(data) {
|
||||||
|
data.forEach(function(provider) {
|
||||||
|
var checked = "";
|
||||||
|
if (provider.active) {
|
||||||
|
checked = "checked";
|
||||||
|
}
|
||||||
|
var $provider_button = '<input type="checkbox" id="show-' + provider.name + '" class="pill" data-initial="' + provider.initial + '" data-control="' + provider.id + '" ' + checked + '><label for="show-' + provider.name + '">' + provider.name + ' <span class="glyphicon glyphicon-ok"></span></label>'
|
||||||
|
$("#metadata_provider").append($provider_button);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
$(document).on("change", ".pill", function () {
|
||||||
|
var element = $(this);
|
||||||
|
var id = element.data("control");
|
||||||
|
var initial = element.data("initial");
|
||||||
|
var val = element.prop('checked');
|
||||||
|
var params = {id : id, value: val};
|
||||||
|
if (!initial) {
|
||||||
|
params['initial'] = initial;
|
||||||
|
params['query'] = keyword;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
method:"post",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
dataType: "json",
|
||||||
|
url: getPath() + "/metadata/provider/" + id,
|
||||||
|
data: JSON.stringify(params),
|
||||||
|
success: function success(data) {
|
||||||
|
element.data("initial", "true");
|
||||||
|
data.forEach(function(book) {
|
||||||
|
var $book = $(templates.bookResult(book));
|
||||||
|
$book.find("img").on("click", function () {
|
||||||
|
populateForm(book);
|
||||||
|
});
|
||||||
|
$("#book-list").append($book);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#meta-search").on("submit", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
keyword = $("#keyword").val();
|
||||||
|
$('.pill').each(function(){
|
||||||
|
// console.log($(this).data('control'));
|
||||||
|
$(this).data("initial", $(this).prop('checked'));
|
||||||
|
// console.log($(this).data('initial'));
|
||||||
|
});
|
||||||
|
doSearch(keyword);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#get_meta").click(function () {
|
||||||
|
populate_provider();
|
||||||
|
var bookTitle = $("#book_title").val();
|
||||||
|
$("#keyword").val(bookTitle);
|
||||||
|
keyword = bookTitle;
|
||||||
|
doSearch(bookTitle);
|
||||||
|
});
|
||||||
|
$("#metaModal").on("show.bs.modal", function(e) {
|
||||||
|
$(e.relatedTarget).one('focus', function (e) {
|
||||||
|
$(this).blur();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -181,7 +181,7 @@ $("#delete_confirm").click(function() {
|
||||||
if (item.format != "") {
|
if (item.format != "") {
|
||||||
$("button[data-delete-format='"+item.format+"']").addClass('hidden');
|
$("button[data-delete-format='"+item.format+"']").addClass('hidden');
|
||||||
}
|
}
|
||||||
$( ".navbar" ).after( '<div class="row-fluid text-center" style="margin-top: -20px;">' +
|
$( ".navbar" ).after( '<div class="row-fluid text-center" >' +
|
||||||
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
|
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
|
|
||||||
|
@ -248,14 +248,13 @@ $(function() {
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../get_updater_status",
|
url: window.location.pathname + "/../../get_updater_status",
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
// console.log(data.status);
|
|
||||||
$("#DialogContent").html(updateText[data.status]);
|
$("#DialogContent").html(updateText[data.status]);
|
||||||
if (data.status > 6) {
|
if (data.status > 6) {
|
||||||
cleanUp();
|
cleanUp();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function error() {
|
error: function error() {
|
||||||
$("#DialogContent").html(updateText[7]);
|
$("#DialogContent").html(updateText[11]);
|
||||||
cleanUp();
|
cleanUp();
|
||||||
},
|
},
|
||||||
timeout: 2000
|
timeout: 2000
|
||||||
|
@ -451,7 +450,6 @@ $(function() {
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
updateText = data.text;
|
updateText = data.text;
|
||||||
$("#DialogContent").html(updateText[data.status]);
|
$("#DialogContent").html(updateText[data.status]);
|
||||||
// console.log(data.status);
|
|
||||||
updateTimerID = setInterval(updateTimer, 2000);
|
updateTimerID = setInterval(updateTimer, 2000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -554,7 +552,7 @@ $(function() {
|
||||||
function handle_response(data) {
|
function handle_response(data) {
|
||||||
if (!jQuery.isEmptyObject(data)) {
|
if (!jQuery.isEmptyObject(data)) {
|
||||||
data.forEach(function (item) {
|
data.forEach(function (item) {
|
||||||
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' +
|
$(".navbar").after('<div class="row-fluid text-center">' +
|
||||||
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,7 +85,7 @@ $(function() {
|
||||||
method:"post",
|
method:"post",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/mergebooks",
|
url: window.location.pathname + "/../ajax/mergebooks",
|
||||||
data: JSON.stringify({"Merge_books":selections}),
|
data: JSON.stringify({"Merge_books":selections}),
|
||||||
success: function success() {
|
success: function success() {
|
||||||
$("#books-table").bootstrapTable("refresh");
|
$("#books-table").bootstrapTable("refresh");
|
||||||
|
@ -104,7 +104,7 @@ $(function() {
|
||||||
method:"post",
|
method:"post",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/simulatemerge",
|
url: window.location.pathname + "/../ajax/simulatemerge",
|
||||||
data: JSON.stringify({"Merge_books":selections}),
|
data: JSON.stringify({"Merge_books":selections}),
|
||||||
success: function success(booTitles) {
|
success: function success(booTitles) {
|
||||||
$.each(booTitles.from, function(i, item) {
|
$.each(booTitles.from, function(i, item) {
|
||||||
|
@ -121,7 +121,7 @@ $(function() {
|
||||||
method:"post",
|
method:"post",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/xchange",
|
url: window.location.pathname + "/../ajax/xchange",
|
||||||
data: JSON.stringify({"xchange":selections}),
|
data: JSON.stringify({"xchange":selections}),
|
||||||
success: function success() {
|
success: function success() {
|
||||||
$("#books-table").bootstrapTable("refresh");
|
$("#books-table").bootstrapTable("refresh");
|
||||||
|
@ -161,10 +161,11 @@ $(function() {
|
||||||
|
|
||||||
$("#books-table").bootstrapTable({
|
$("#books-table").bootstrapTable({
|
||||||
sidePagination: "server",
|
sidePagination: "server",
|
||||||
|
pageList: "[10, 25, 50, 100]",
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
pagination: true,
|
pagination: true,
|
||||||
paginationLoop: false,
|
paginationLoop: false,
|
||||||
paginationDetailHAlign: " hidden",
|
paginationDetailHAlign: "right",
|
||||||
paginationHAlign: "left",
|
paginationHAlign: "left",
|
||||||
idField: "id",
|
idField: "id",
|
||||||
uniqueId: "id",
|
uniqueId: "id",
|
||||||
|
@ -187,7 +188,7 @@ $(function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method:"get",
|
method:"get",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/sort_value/" + field + "/" + row.id,
|
url: window.location.pathname + "/../ajax/sort_value/" + field + "/" + row.id,
|
||||||
success: function success(data) {
|
success: function success(data) {
|
||||||
var key = Object.keys(data)[0];
|
var key = Object.keys(data)[0];
|
||||||
$("#books-table").bootstrapTable("updateCellByUniqueId", {
|
$("#books-table").bootstrapTable("updateCellByUniqueId", {
|
||||||
|
@ -215,7 +216,7 @@ $(function() {
|
||||||
method:"post",
|
method:"post",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname + "/../../ajax/table_settings",
|
url: window.location.pathname + "/../ajax/table_settings",
|
||||||
data: "{" + st + "}",
|
data: "{" + st + "}",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -769,7 +770,7 @@ function handleListServerResponse (data) {
|
||||||
$("#flash_danger").remove();
|
$("#flash_danger").remove();
|
||||||
if (!jQuery.isEmptyObject(data)) {
|
if (!jQuery.isEmptyObject(data)) {
|
||||||
data.forEach(function(item) {
|
data.forEach(function(item) {
|
||||||
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' +
|
$(".navbar").after('<div class="row-fluid text-center">' +
|
||||||
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
|
||||||
'</div>');
|
'</div>');
|
||||||
});
|
});
|
||||||
|
|
|
@ -111,8 +111,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<label for="pubdate">{{_('Published Date')}}</label>
|
<label for="pubdate">{{_('Published Date')}}</label>
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" style="position: static;" class="datepicker form-control" name="pubdate" id="pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdateinput}}{% endif %}">
|
<input type="text" class="datepicker form-control" name="pubdate" id="pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdateinput}}{% endif %}">
|
||||||
<input type="text" style="position: absolute;" class="form-control fake-input hidden" id="fake_pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdate}}{% endif %}">
|
<input type="text" class="form-control fake-input hidden" id="fake_pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdate}}{% endif %}">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="pubdate_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
<button type="button" id="pubdate_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -156,11 +156,11 @@
|
||||||
|
|
||||||
{% if c.datatype == 'datetime' %}
|
{% if c.datatype == 'datetime' %}
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" style="position: static;" class="datepicker form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"
|
<input type="text" class="datepicker form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}"
|
||||||
{% if book['custom_column_' ~ c.id]|length > 0 %}
|
{% if book['custom_column_' ~ c.id]|length > 0 %}
|
||||||
value="{% if book['custom_column_' ~ c.id][0].value %}{{ book['custom_column_' ~ c.id][0].value|formatdateinput}}{% endif %}"
|
value="{% if book['custom_column_' ~ c.id][0].value %}{{ book['custom_column_' ~ c.id][0].value|formatdateinput}}{% endif %}"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
<input type="text" style="position: absolute;" class="fake_custom_column_{{ c.id }} form-control fake-input hidden" id="fake_pubdate"
|
<input type="text" class="fake_custom_column_{{ c.id }} form-control fake-input hidden" id="fake_pubdate_{{ c.id }}"
|
||||||
{% if book['custom_column_' ~ c.id]|length > 0 %}
|
{% if book['custom_column_' ~ c.id]|length > 0 %}
|
||||||
value="{% if book['custom_column_' ~ c.id][0].value %}{{book['custom_column_' ~ c.id][0].value|formatdate}}{% endif %}"
|
value="{% if book['custom_column_' ~ c.id][0].value %}{{book['custom_column_' ~ c.id][0].value|formatdate}}{% endif %}"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
|
@ -232,7 +232,7 @@
|
||||||
<form class="padded-bottom" id="meta-search">
|
<form class="padded-bottom" id="meta-search">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label class="sr-only" for="keyword">{{_('Keyword')}}</label>
|
<label class="sr-only" for="keyword">{{_('Keyword')}}</label>
|
||||||
<input type="text" class="form-control" id="keyword" name="keyword" placeholder="{{_(" Search keyword ")}}">
|
<input type="text" class="form-control" id="keyword" name="keyword" placeholder="{{_("Search keyword")}}">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="submit" class="btn btn-primary" id="do-search">{{_("Search")}}</button>
|
<button type="submit" class="btn btn-primary" id="do-search">{{_("Search")}}</button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -241,25 +241,12 @@
|
||||||
<div class="text-center"><strong>{{_('Click the cover to load metadata to the form')}}</strong></div>
|
<div class="text-center"><strong>{{_('Click the cover to load metadata to the form')}}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="text-center padded-bottom">
|
<div class="text-center padded-bottom" id="metadata_provider">
|
||||||
<input type="checkbox" id="show-douban" class="pill" data-control="douban" checked>
|
|
||||||
<label for="show-douban">Douban <span class="glyphicon glyphicon-ok"></span></label>
|
|
||||||
|
|
||||||
<input type="checkbox" id="show-google" class="pill" data-control="google" checked>
|
|
||||||
<label for="show-google">Google <span class="glyphicon glyphicon-ok"></span></label>
|
|
||||||
|
|
||||||
<input type="checkbox" id="show-comics" class="pill" data-control="comicvine" checked>
|
|
||||||
<label for="show-comics">ComicVine <span class="glyphicon glyphicon-ok"></span></label>
|
|
||||||
|
|
||||||
<input type="checkbox" id="show-googlescholar" class="pill" data-control="googlescholar" checked>
|
|
||||||
<label for="show-googlescholar">Google Scholar <span class="glyphicon glyphicon-ok"></span></label>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="meta-info">
|
<div id="meta-info">
|
||||||
{{_("Loading...")}}
|
{{_("Loading...")}}
|
||||||
</div>
|
</div>
|
||||||
<ul id="book-list" class="media-list"></ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||||
|
@ -275,7 +262,7 @@
|
||||||
<img class="pull-left img-responsive"
|
<img class="pull-left img-responsive"
|
||||||
data-toggle="modal"
|
data-toggle="modal"
|
||||||
data-target="#metaModal"
|
data-target="#metaModal"
|
||||||
src="<%= cover || "{{ url_for('static', filename='img/academicpaper.svg') }}" %>"
|
src="<%= cover || "{{ url_for('static', filename='img/academicpaper.svg') }}" %>"
|
||||||
alt="Cover"
|
alt="Cover"
|
||||||
>
|
>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
|
|
|
@ -78,11 +78,11 @@
|
||||||
<p></p>
|
<p></p>
|
||||||
<div class="text-left">{{_('Books with Title will be merged from:')}}</div>
|
<div class="text-left">{{_('Books with Title will be merged from:')}}</div>
|
||||||
<p></p>
|
<p></p>
|
||||||
<div class=text-left" id="merge_from"></div>
|
<div class="text-left" id="merge_from"></div>
|
||||||
<p></p>
|
<p></p>
|
||||||
<div class="text-left">{{_('Into Book with Title:')}}</div>
|
<div class="text-left">{{_('Into Book with Title:')}}</div>
|
||||||
<p></p>
|
<p></p>
|
||||||
<div class=text-left" id="merge_to"></div>
|
<div class="text-left" id="merge_to"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
|
<input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
|
||||||
|
@ -98,5 +98,9 @@
|
||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
|
||||||
|
{% if not g.user.locale == 'en' %}
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/locale/bootstrap-table-' + g.user.locale + '.min.js') }}" charset="UTF-8"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block flash %}
|
{% block flash %}
|
||||||
<div id="spinning_success" class="row-fluid text-center" style="margin-top: -20px; display:none;">
|
<div id="spinning_success" class="row-fluid text-center" style="display:none;">
|
||||||
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
|
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block flash %}
|
{% block flash %}
|
||||||
<div id="spinning_success" class="row-fluid text-center" style="margin-top: -20px; display:none;">
|
<div id="spinning_success" class="row-fluid text-center" style="display:none;">
|
||||||
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
|
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
<div class="discover">
|
<div class="discover">
|
||||||
<h2>{{title}}</h2>
|
<h2>{{title}}</h2>
|
||||||
<form role="form" method="POST" autocomplete="off">
|
<form role="form" method="POST" autocomplete="off">
|
||||||
<div class="panel-group col-md-10 col-lg-6">
|
<div class="panel-group col-md-10 col-lg-8">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<button type="button" data-toggle="modal" data-link="config_certfile" data-target="#fileModal" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
<button type="button" data-toggle="modal" data-link="config_certfile" data-target="#fileModal" id="certfile_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<label for="config_calibre_dir" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
|
<label for="config_keyfile" >{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" id="config_keyfile" name="config_keyfile" value="{% if config.config_keyfile != None %}{{ config.config_keyfile }}{% endif %}" autocomplete="off">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
|
@ -94,6 +94,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="collapsefour" class="panel-collapse collapse">
|
<div id="collapsefour" class="panel-collapse collapse">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" id="config_unicode_filename" name="config_unicode_filename" {% if config.config_unicode_filename %}checked{% endif %}>
|
||||||
|
<label for="config_unicode_filename">{{_('Convert non-English characters in title and author while saving to disk')}}</label>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}>
|
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}>
|
||||||
<label for="config_uploading">{{_('Enable Uploads')}}</label>
|
<label for="config_uploading">{{_('Enable Uploads')}}</label>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<form role="form" class="col-md-10 col-lg-6" method="POST">
|
<form role="form" class="col-md-10 col-lg-6" method="POST">
|
||||||
{% if feature_support['gmail'] %}
|
{% if feature_support['gmail'] %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="mail_server_type">{{_('Choose Server Type')}}</label>
|
<label for="config_email_type">{{_('Choose Server Type')}}</label>
|
||||||
<select name="mail_server_type" id="config_email_type" class="form-control" data-control="email-settings">
|
<select name="mail_server_type" id="config_email_type" class="form-control" data-control="email-settings">
|
||||||
<option value="0" {% if content.mail_server_type == 0 %}selected{% endif %}>{{_('Use Standard E-Mail Account')}}</option>
|
<option value="0" {% if content.mail_server_type == 0 %}selected{% endif %}>{{_('Use Standard E-Mail Account')}}</option>
|
||||||
<option value="1" {% if content.mail_server_type == 1 %}selected{% endif %}>{{_('Gmail Account with OAuth2 Verification')}}</option>
|
<option value="1" {% if content.mail_server_type == 1 %}selected{% endif %}>{{_('Gmail Account with OAuth2 Verification')}}</option>
|
||||||
|
|
|
@ -91,22 +91,22 @@
|
||||||
</div>
|
</div>
|
||||||
{% for message in get_flashed_messages(with_categories=True) %}
|
{% for message in get_flashed_messages(with_categories=True) %}
|
||||||
{%if message[0] == "error" %}
|
{%if message[0] == "error" %}
|
||||||
<div class="row-fluid text-center" style="margin-top: -20px;">
|
<div class="row-fluid text-center" >
|
||||||
<div id="flash_danger" class="alert alert-danger">{{ message[1] }}</div>
|
<div id="flash_danger" class="alert alert-danger">{{ message[1] }}</div>
|
||||||
</div>
|
</div>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
{%if message[0] == "info" %}
|
{%if message[0] == "info" %}
|
||||||
<div class="row-fluid text-center" style="margin-top: -20px;">
|
<div class="row-fluid text-center">
|
||||||
<div id="flash_info" class="alert alert-info">{{ message[1] }}</div>
|
<div id="flash_info" class="alert alert-info">{{ message[1] }}</div>
|
||||||
</div>
|
</div>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
{%if message[0] == "warning" %}
|
{%if message[0] == "warning" %}
|
||||||
<div class="row-fluid text-center" style="margin-top: -20px;">
|
<div class="row-fluid text-center">
|
||||||
<div id="flash_warning" class="alert alert-warning">{{ message[1] }}</div>
|
<div id="flash_warning" class="alert alert-warning">{{ message[1] }}</div>
|
||||||
</div>
|
</div>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
{%if message[0] == "success" %}
|
{%if message[0] == "success" %}
|
||||||
<div class="row-fluid text-center" style="margin-top: -20px;">
|
<div class="row-fluid text-center">
|
||||||
<div id="flash_success" class="alert alert-success">{{ message[1] }}</div>
|
<div id="flash_success" class="alert alert-success">{{ message[1] }}</div>
|
||||||
</div>
|
</div>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
|
|
|
@ -19,8 +19,8 @@
|
||||||
<div class="form-group col-sm-6">
|
<div class="form-group col-sm-6">
|
||||||
<label for="publishstart">{{_('Published Date From')}}</label>
|
<label for="publishstart">{{_('Published Date From')}}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" style="position: static;" class="datepicker form-control" name="publish_start" id="publishstart" value="">
|
<input type="text" class="datepicker form-control" name="publish_start" id="publishstart" value="">
|
||||||
<input type="text" style="position: absolute;" class="form-control fake-input hidden" id="fake_publishstart" value="">
|
<input type="text" class="form-control fake-input hidden" id="fake_publishstart" value="">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="publishstart_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
<button type="button" id="publishstart_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
<div class="form-group col-sm-6">
|
<div class="form-group col-sm-6">
|
||||||
<label for="publishend">{{_('Published Date To')}}</label>
|
<label for="publishend">{{_('Published Date To')}}</label>
|
||||||
<div class="input-group ">
|
<div class="input-group ">
|
||||||
<input type="text" style="position: static;" class="datepicker form-control" name="publishend" id="publishend" value="">
|
<input type="text" class="datepicker form-control" name="publishend" id="publishend" value="">
|
||||||
<input type="text" style="position: absolute;" class="form-control fake-input hidden" id="fake_publishend" value="">
|
<input type="text" class="form-control fake-input hidden" id="fake_publishend" value="">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="publishend_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
<button type="button" id="publishend_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -178,8 +178,8 @@
|
||||||
<div class="form-group col-sm-6">
|
<div class="form-group col-sm-6">
|
||||||
<label for="{{ 'custom_column_' ~ c.id }}">{{_('From:')}}</label>
|
<label for="{{ 'custom_column_' ~ c.id }}">{{_('From:')}}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" style="position: static;" class="datepicker form-control" name="{{ 'custom_column_' ~ c.id }}_start" id="{{ 'custom_column_' ~ c.id }}_start" value="">
|
<input type="text" class="datepicker form-control" name="{{ 'custom_column_' ~ c.id }}_start" id="{{ 'custom_column_' ~ c.id }}_start" value="">
|
||||||
<input type="text" style="position: absolute;" class="form-control fake-input hidden" id="fake_{{ 'custom_column_' ~ c.id }}_start" value="">
|
<input type="text" class="form-control fake-input hidden" id="fake_{{ 'custom_column_' ~ c.id }}_start" value="">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="{{ 'custom_column_' ~ c.id }}_start_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
<button type="button" id="{{ 'custom_column_' ~ c.id }}_start_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -188,8 +188,8 @@
|
||||||
<div class="form-group col-sm-6">
|
<div class="form-group col-sm-6">
|
||||||
<label for="{{ 'custom_column_' ~ c.id }}">{{_('To:')}}</label>
|
<label for="{{ 'custom_column_' ~ c.id }}">{{_('To:')}}</label>
|
||||||
<div class="input-group ">
|
<div class="input-group ">
|
||||||
<input type="text" style="position: static;" class="datepicker form-control" name="{{ 'custom_column_' ~ c.id }}_end" id="{{ 'custom_column_' ~ c.id }}_end" value="">
|
<input type="text" class="datepicker form-control" name="{{ 'custom_column_' ~ c.id }}_end" id="{{ 'custom_column_' ~ c.id }}_end" value="">
|
||||||
<input type="text" style="position: absolute;" class="form-control fake-input hidden" id="fake_{{ 'custom_column_' ~ c.id }}_end" value="">
|
<input type="text" class="form-control fake-input hidden" id="fake_{{ 'custom_column_' ~ c.id }}_end" value="">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button type="button" id="{{ 'custom_column_' ~ c.id }}_end_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
<button type="button" id="{{ 'custom_column_' ~ c.id }}_end_delete" class="datepicker_delete btn btn-default"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -21,8 +21,6 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
<!--div class="btn btn-default" id="tasks_delete">{{_('Delete finished tasks')}}</div>
|
|
||||||
<div class="btn btn-default" id="tasks_hide">{{_('Hide all tasks')}}</div-->
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|
18
cps/ub.py
18
cps/ub.py
|
@ -62,6 +62,24 @@ app_DB_path = None
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
searched_ids = {}
|
searched_ids = {}
|
||||||
|
|
||||||
|
logged_in = dict()
|
||||||
|
|
||||||
|
def store_user_session():
|
||||||
|
if flask_session.get('_user_id', ""):
|
||||||
|
if logged_in.get(flask_session.get('_user_id', "")):
|
||||||
|
logged_in[flask_session.get('_user_id', "")].append(flask_session.get('_id', ""))
|
||||||
|
else:
|
||||||
|
logged_in[flask_session.get('_user_id', "")] = [flask_session.get('_id', "")]
|
||||||
|
log.info(flask_session.get('_id', ""))
|
||||||
|
|
||||||
|
def delete_user_session(user_id, session_key):
|
||||||
|
try:
|
||||||
|
logged_in.get(str(user_id), []).remove(session_key)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_user_session(user_id, session_key):
|
||||||
|
return session_key in logged_in.get(str(user_id), [])
|
||||||
|
|
||||||
def signal_store_user_session(object, user):
|
def signal_store_user_session(object, user):
|
||||||
store_user_session()
|
store_user_session()
|
||||||
|
|
|
@ -94,14 +94,17 @@ class Updater(threading.Thread):
|
||||||
return False
|
return False
|
||||||
self.status = 4
|
self.status = 4
|
||||||
log.debug(u'Replacing files')
|
log.debug(u'Replacing files')
|
||||||
self.update_source(foldername, constants.BASE_DIR)
|
if self.update_source(foldername, constants.BASE_DIR):
|
||||||
self.status = 6
|
self.status = 6
|
||||||
log.debug(u'Preparing restart of server')
|
log.debug(u'Preparing restart of server')
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
web_server.stop(True)
|
web_server.stop(True)
|
||||||
self.status = 7
|
self.status = 7
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
self.status = 13
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as ex:
|
except requests.exceptions.HTTPError as ex:
|
||||||
log.error(u'HTTP Error %s', ex)
|
log.error(u'HTTP Error %s', ex)
|
||||||
self.status = 8
|
self.status = 8
|
||||||
|
@ -181,6 +184,28 @@ class Updater(threading.Thread):
|
||||||
rf.append(item)
|
rf.append(item)
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_permissions(cls, root_src_dir, root_dst_dir):
|
||||||
|
access = True
|
||||||
|
remove_path = len(root_src_dir) + 1
|
||||||
|
for src_dir, __, files in os.walk(root_src_dir):
|
||||||
|
root_dir = os.path.join(root_dst_dir, src_dir[remove_path:])
|
||||||
|
# Skip non existing folders on check
|
||||||
|
if not os.path.isdir(root_dir): # root_dir.lstrip(os.sep).startswith('.') or
|
||||||
|
continue
|
||||||
|
if not os.access(root_dir, os.R_OK|os.W_OK):
|
||||||
|
log.debug("Missing permissions for {}".format(root_dir))
|
||||||
|
access = False
|
||||||
|
for file_ in files:
|
||||||
|
curr_file = os.path.join(root_dir, file_)
|
||||||
|
# Skip non existing files on check
|
||||||
|
if not os.path.isfile(curr_file): # or curr_file.startswith('.'):
|
||||||
|
continue
|
||||||
|
if not os.access(curr_file, os.R_OK|os.W_OK):
|
||||||
|
log.debug("Missing permissions for {}".format(curr_file))
|
||||||
|
access = False
|
||||||
|
return access
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def moveallfiles(cls, root_src_dir, root_dst_dir):
|
def moveallfiles(cls, root_src_dir, root_dst_dir):
|
||||||
new_permissions = os.stat(root_dst_dir)
|
new_permissions = os.stat(root_dst_dir)
|
||||||
|
@ -269,20 +294,25 @@ class Updater(threading.Thread):
|
||||||
|
|
||||||
remove_items = self.reduce_dirs(rf, new_list)
|
remove_items = self.reduce_dirs(rf, new_list)
|
||||||
|
|
||||||
self.moveallfiles(source, destination)
|
if self.check_permissions(source, destination):
|
||||||
|
self.moveallfiles(source, destination)
|
||||||
|
|
||||||
for item in remove_items:
|
for item in remove_items:
|
||||||
item_path = os.path.join(destination, item[1:])
|
item_path = os.path.join(destination, item[1:])
|
||||||
if os.path.isdir(item_path):
|
if os.path.isdir(item_path):
|
||||||
log.debug("Delete dir %s", item_path)
|
log.debug("Delete dir %s", item_path)
|
||||||
shutil.rmtree(item_path, ignore_errors=True)
|
shutil.rmtree(item_path, ignore_errors=True)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
os.remove(item_path)
|
os.remove(item_path)
|
||||||
log.debug("Delete file %s", item_path)
|
log.debug("Delete file %s", item_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
log.debug("Could not remove: %s", item_path)
|
log.debug("Could not remove: %s", item_path)
|
||||||
shutil.rmtree(source, ignore_errors=True)
|
shutil.rmtree(source, ignore_errors=True)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.debug("Permissions missing for update")
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_venv():
|
def is_venv():
|
||||||
|
@ -572,5 +602,5 @@ class Updater(threading.Thread):
|
||||||
status['message'] = _(u'Timeout while establishing connection')
|
status['message'] = _(u'Timeout while establishing connection')
|
||||||
except (requests.exceptions.RequestException, ValueError):
|
except (requests.exceptions.RequestException, ValueError):
|
||||||
status['message'] = _(u'General error')
|
status['message'] = _(u'General error')
|
||||||
log.debug('Updater status: %s', status['message'])
|
log.debug('Updater status: {}'.format(status['message'] or "OK"))
|
||||||
return status, commit
|
return status, commit
|
||||||
|
|
|
@ -84,7 +84,7 @@ except ImportError:
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_security_headers(resp):
|
def add_security_headers(resp):
|
||||||
# resp.headers['Content-Security-Policy']= "script-src 'self'" https://www.googleapis.com https://api.douban.com https://comicvine.gamespot.com;"
|
resp.headers['Content-Security-Policy']= "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:"
|
||||||
resp.headers['X-Content-Type-Options'] = 'nosniff'
|
resp.headers['X-Content-Type-Options'] = 'nosniff'
|
||||||
resp.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
resp.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||||
resp.headers['X-XSS-Protection'] = '1; mode=block'
|
resp.headers['X-XSS-Protection'] = '1; mode=block'
|
||||||
|
@ -1533,6 +1533,7 @@ def login():
|
||||||
login_result, error = services.ldap.bind_user(form['username'], form['password'])
|
login_result, error = services.ldap.bind_user(form['username'], form['password'])
|
||||||
if login_result:
|
if login_result:
|
||||||
login_user(user, remember=bool(form.get('remember_me')))
|
login_user(user, remember=bool(form.get('remember_me')))
|
||||||
|
ub.store_user_session()
|
||||||
log.debug(u"You are now logged in as: '%s'", user.name)
|
log.debug(u"You are now logged in as: '%s'", user.name)
|
||||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name),
|
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name),
|
||||||
category="success")
|
category="success")
|
||||||
|
@ -1540,6 +1541,7 @@ def login():
|
||||||
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
|
elif login_result is None and user and check_password_hash(str(user.password), form['password']) \
|
||||||
and user.name != "Guest":
|
and user.name != "Guest":
|
||||||
login_user(user, remember=bool(form.get('remember_me')))
|
login_user(user, remember=bool(form.get('remember_me')))
|
||||||
|
ub.store_user_session()
|
||||||
log.info("Local Fallback Login as: '%s'", user.name)
|
log.info("Local Fallback Login as: '%s'", user.name)
|
||||||
flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known",
|
flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known",
|
||||||
nickname=user.name),
|
nickname=user.name),
|
||||||
|
@ -1569,6 +1571,7 @@ def login():
|
||||||
else:
|
else:
|
||||||
if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
|
if user and check_password_hash(str(user.password), form['password']) and user.name != "Guest":
|
||||||
login_user(user, remember=bool(form.get('remember_me')))
|
login_user(user, remember=bool(form.get('remember_me')))
|
||||||
|
ub.store_user_session()
|
||||||
log.debug(u"You are now logged in as: '%s'", user.name)
|
log.debug(u"You are now logged in as: '%s'", user.name)
|
||||||
flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
|
flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
|
||||||
config.config_is_initial = False
|
config.config_is_initial = False
|
||||||
|
|
Loading…
Reference in New Issue
Block a user