mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-31 07:13:02 +00:00 
			
		
		
		
	Version Bump
Merge branch 'Develop'
This commit is contained in:
		| @@ -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: | ||||||
|  |         if not config.config_unicode_filename: | ||||||
|             value = (unidecode.unidecode(value)) |             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,37 +14,11 @@ | |||||||
|  *  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 ggSearch = "/books/v1/volumes"; |  | ||||||
|   var ggDone = 0; |  | ||||||
|  |  | ||||||
|   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 = { |     var templates = { | ||||||
|         bookResult: _.template( |         bookResult: _.template( | ||||||
| @@ -58,7 +32,6 @@ $(function () { | |||||||
|         $.each(book.tags, function(i, el) { |         $.each(book.tags, function(i, el) { | ||||||
|             if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el); |             if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         var ampSeparatedAuthors = (book.authors || []).join(" & "); |         var ampSeparatedAuthors = (book.authors || []).join(" & "); | ||||||
|         $("#bookAuthor").val(ampSeparatedAuthors); |         $("#bookAuthor").val(ampSeparatedAuthors); | ||||||
|         $("#book_title").val(book.title); |         $("#book_title").val(book.title); | ||||||
| @@ -75,319 +48,100 @@ $(function () { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   function showResult () { |     function doSearch (keyword) { | ||||||
|     showFlag++; |         if (keyword) { | ||||||
|     if (showFlag === 1) { |             $("#meta-info").text(msg.loading); | ||||||
|       $("#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; |  | ||||||
|       } 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({ |             $.ajax({ | ||||||
|       url: google + ggSearch + "?q=" + title.replace(/\s+/gm, "+"), |                 url: getPath() + "/metadata/search", | ||||||
|       type: "GET", |                 type: "POST", | ||||||
|       dataType: "jsonp", |                 data: {"query": keyword}, | ||||||
|       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", |                 dataType: "json", | ||||||
|                 success: function success(data) { |                 success: function success(data) { | ||||||
|         gsResults = 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); | ||||||
|  |                     }); | ||||||
|                 }, |                 }, | ||||||
|       complete: function complete() { |                 error: function error() { | ||||||
|         gsDone = 1; |                     $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>" + $("#meta-info")[0].innerHTML); | ||||||
|         showResult(); |                 }, | ||||||
|         $("#show-googlescholar").trigger("change"); |             }); | ||||||
|         } |         } | ||||||
|     }) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   function doSearch (keyword) { |     function populate_provider() { | ||||||
|     showFlag = 0; |         $("#metadata_provider").empty(); | ||||||
|     dbDone = ggDone = cvDone = 0; |         $.ajax({ | ||||||
|     dbResults = []; |             url: getPath() + "/metadata/provider", | ||||||
|     ggResults = []; |             type: "get", | ||||||
|     cvResults = []; |             dataType: "json", | ||||||
|     gsResults = []; |             success: function success(data) { | ||||||
|     $("#meta-info").text(msg.loading); |                 data.forEach(function(provider) { | ||||||
|     if (keyword) { |                     var checked = ""; | ||||||
|       dbSearchBook(keyword); |                     if (provider.active) { | ||||||
|       ggSearchBook(keyword); |                         checked = "checked"; | ||||||
|       cvSearchBook(keyword); |  | ||||||
|       gsSearchBook(keyword); |  | ||||||
|                     } |                     } | ||||||
|  |                     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) { |     $("#meta-search").on("submit", function (e) { | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|     var keyword = $("#keyword").val(); |         keyword = $("#keyword").val(); | ||||||
|     if (keyword) { |         $('.pill').each(function(){ | ||||||
|  |             // console.log($(this).data('control')); | ||||||
|  |             $(this).data("initial", $(this).prop('checked')); | ||||||
|  |             // console.log($(this).data('initial')); | ||||||
|  |         }); | ||||||
|         doSearch(keyword); |         doSearch(keyword); | ||||||
|     } |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     $("#get_meta").click(function () { |     $("#get_meta").click(function () { | ||||||
|  |         populate_provider(); | ||||||
|         var bookTitle = $("#book_title").val(); |         var bookTitle = $("#book_title").val(); | ||||||
|     if (bookTitle) { |  | ||||||
|         $("#keyword").val(bookTitle); |         $("#keyword").val(bookTitle); | ||||||
|  |         keyword = bookTitle; | ||||||
|         doSearch(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> | ||||||
|   | |||||||
| @@ -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,7 +94,7 @@ 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) | ||||||
| @@ -102,6 +102,9 @@ class Updater(threading.Thread): | |||||||
|                 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,6 +294,7 @@ class Updater(threading.Thread): | |||||||
|  |  | ||||||
|         remove_items = self.reduce_dirs(rf, new_list) |         remove_items = self.reduce_dirs(rf, new_list) | ||||||
|  |  | ||||||
|  |         if self.check_permissions(source, destination): | ||||||
|             self.moveallfiles(source, destination) |             self.moveallfiles(source, destination) | ||||||
|  |  | ||||||
|             for item in remove_items: |             for item in remove_items: | ||||||
| @@ -283,6 +309,10 @@ class Updater(threading.Thread): | |||||||
|                     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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Ozzie Isaacs
					Ozzie Isaacs