Merge branch 'master' into Develop

# Conflicts:
#	cps/templates/detail.html
#	test/Calibre-Web TestSummary_Linux.html
This commit is contained in:
Ozzie Isaacs 2022-01-23 17:51:54 +01:00
commit 127bf98aac
89 changed files with 8586 additions and 7547 deletions

View File

@ -19,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup - full graphical setup
- User management with fine-grained per-user permissions - User management with fine-grained per-user permissions
- Admin interface - Admin interface
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian - User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, korean, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
- OPDS feed for eBook reader apps - OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language - Filter and search by titles, authors, tags, series and language
- Create a custom book collection (shelves) - Create a custom book collection (shelves)

View File

@ -10,21 +10,25 @@ To receive fixes for security vulnerabilities it is required to always upgrade t
## History ## History
| Fixed in | Description |CVE number | | Fixed in | Description |CVE number |
| ---------- |---------|---------| |---------------|--------------------------------------------------------------------------------------------------------------------|---------|
| 3rd July 2018 | Guest access acts as a backdoor|| | 3rd July 2018 | Guest access acts as a backdoor ||
| V 0.6.7 |Hardcoded secret key for sessions |CVE-2020-12627 | | V 0.6.7 | Hardcoded secret key for sessions |CVE-2020-12627 |
| V 0.6.13|Calibre-Web Metadata cross site scripting |CVE-2021-25964| | V 0.6.13 | Calibre-Web Metadata cross site scripting |CVE-2021-25964|
| V 0.6.13|Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo|| | V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo ||
| V 0.6.13|JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource)|| | V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) ||
| V 0.6.13|JavaScript could get executed in a custom column of type "comment" field || | V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field ||
| V 0.6.13|JavaScript could get executed after converting a book to another format with a title containing javascript code|| | V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code ||
| V 0.6.13|JavaScript could get executed after converting a book to another format with a username containing javascript code|| | V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code ||
| V 0.6.13|JavaScript could get executed in the description series, categories or publishers title|| | V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title ||
| V 0.6.13|JavaScript could get executed in the shelf title|| | V 0.6.13 | JavaScript could get executed in the shelf title ||
| V 0.6.13|Login with the old session cookie after logout. Thanks to @ibarrionuevo|| | V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo ||
| V 0.6.14|CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965| | V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
| V 0.6.14|Cross-Site Scripting vulnerability on typeahead inputs. Thanks to @notdodo|| | V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 ||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 ||
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo ||
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo ||
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo ||
## Staement regarding Log4j (CVE-2021-44228 and related) ## Staement regarding Log4j (CVE-2021-44228 and related)

View File

@ -74,13 +74,22 @@ opt = dep_check.load_dependencys(True)
for i in (req + opt): for i in (req + opt):
ret[i[1]] = i[0] ret[i[1]] = i[0]
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
calibre_web_version = constants.STABLE_VERSION['version']
else:
calibre_web_version = (constants.STABLE_VERSION['version'] + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%','%%'))
if getattr(sys, 'frozen', False):
calibre_web_version += " - Exe-Version"
elif constants.HOME_CONFIG:
calibre_web_version += " - pyPi"
if not ret: if not ret:
_VERSIONS = OrderedDict( _VERSIONS = OrderedDict(
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()), Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
Python=sys.version, Python=sys.version,
Calibre_Web=constants.STABLE_VERSION['version'] + ' - ' Calibre_Web=calibre_web_version,
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
WebServer=server.VERSION, WebServer=server.VERSION,
Flask=flask.__version__, Flask=flask.__version__,
Flask_Login=flask_loginVersion, Flask_Login=flask_loginVersion,
@ -110,9 +119,7 @@ else:
_VERSIONS = OrderedDict( _VERSIONS = OrderedDict(
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()), Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
Python = sys.version, Python = sys.version,
Calibre_Web = constants.STABLE_VERSION['version'] + ' - ' Calibre_Web=calibre_web_version,
+ constants.NIGHTLY_VERSION[0].replace('%', '%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%', '%%'),
Werkzeug = werkzeug.__version__, Werkzeug = werkzeug.__version__,
Jinja2=jinja2.__version__, Jinja2=jinja2.__version__,
pySqlite = sqlite3.version, pySqlite = sqlite3.version,

View File

@ -129,11 +129,11 @@ def admin_forbidden():
abort(403) abort(403)
@admi.route("/shutdown") @admi.route("/shutdown", methods=["POST"])
@login_required @login_required
@admin_required @admin_required
def shutdown(): def shutdown():
task = int(request.args.get("parameter").strip()) task = request.get_json().get('parameter', -1)
showtext = {} showtext = {}
if task in (0, 1): # valid commandos received if task in (0, 1): # valid commandos received
# close all database connections # close all database connections
@ -756,7 +756,12 @@ def prepare_tags(user, action, tags_name, id_list):
return ",".join(saved_tags_list) return ",".join(saved_tags_list)
@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST']) @admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
@login_required
@admin_required
def add_user_0_restriction(res_type):
return add_restriction(res_type, 0)
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST']) @admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required @login_required
@admin_required @admin_required
@ -803,7 +808,13 @@ def add_restriction(res_type, user_id):
return "" return ""
@admi.route("/ajax/deleterestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST']) @admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
@login_required
@admin_required
def delete_user_0_restriction(res_type):
return delete_restriction(res_type, 0)
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST']) @admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required @login_required
@admin_required @admin_required
@ -895,7 +906,7 @@ def list_restriction(res_type, user_id):
response.headers["Content-Type"] = "application/json; charset=utf-8" response.headers["Content-Type"] = "application/json; charset=utf-8"
return response return response
@admi.route("/ajax/fullsync") @admi.route("/ajax/fullsync", methods=["POST"])
@login_required @login_required
def ajax_fullsync(): def ajax_fullsync():
count = ub.session.query(ub.KoboSyncedBooks).filter(current_user.id == ub.KoboSyncedBooks.user_id).delete() count = ub.session.query(ub.KoboSyncedBooks).filter(current_user.id == ub.KoboSyncedBooks.user_id).delete()
@ -1404,16 +1415,25 @@ def _delete_user(content):
for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id): for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id):
ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete() ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete()
ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id).delete() ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id).delete()
ub.session.query(ub.Bookmark).filter(content.id == ub.Bookmark.user_id).delete()
ub.session.query(ub.User).filter(ub.User.id == content.id).delete() ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session.query(ub.ArchivedBook).filter(ub.ArchivedBook.user_id == content.id).delete()
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == content.id).delete()
ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.user_id == content.id).delete()
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == content.id).delete()
# delete KoboReadingState and all it's children
kobo_entries = ub.session.query(ub.KoboReadingState).filter(ub.KoboReadingState.user_id == content.id).all()
for kobo_entry in kobo_entries:
ub.session.delete(kobo_entry)
ub.session_commit() ub.session_commit()
log.info(u"User {} deleted".format(content.name)) log.info("User {} deleted".format(content.name))
return(_(u"User '%(nick)s' deleted", nick=content.name)) return(_("User '%(nick)s' deleted", nick=content.name))
else: else:
log.warning(_(u"Can't delete Guest User")) log.warning(_("Can't delete Guest User"))
raise Exception(_(u"Can't delete Guest User")) raise Exception(_("Can't delete Guest User"))
else: else:
log.warning(u"No admin user remaining, can't delete user") log.warning("No admin user remaining, can't delete user")
raise Exception(_(u"No admin user remaining, can't delete user")) raise Exception(_("No admin user remaining, can't delete user"))
def _handle_edit_user(to_save, content, languages, translations, kobo_support): def _handle_edit_user(to_save, content, languages, translations, kobo_support):
@ -1615,7 +1635,7 @@ def edit_user(user_id):
page="edituser") page="edituser")
@admi.route("/admin/resetpassword/<int:user_id>") @admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
@login_required @login_required
@admin_required @admin_required
def reset_user_password(user_id): def reset_user_password(user_id):
@ -1791,7 +1811,7 @@ def ldap_import_create_user(user, user_data):
return 0, message return 0, message
@admi.route('/import_ldap_users') @admi.route('/import_ldap_users', methods=["POST"])
@login_required @login_required
@admin_required @admin_required
def import_ldap_users(): def import_ldap_users():

View File

@ -45,6 +45,7 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen') parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password') parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version') parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
parser.add_argument('-l', action='store_true', help='Allow loading covers from localhost')
args = parser.parse_args() args = parser.parse_args()
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db") settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
@ -77,6 +78,8 @@ if (args.k and not args.c) or (not args.k and args.c):
if args.k == "": if args.k == "":
keyfilepath = "" keyfilepath = ""
# load covers from localhost
allow_localhost = args.l or None
# handle and check ip address argument # handle and check ip address argument
ip_address = args.i or None ip_address = args.i or None
if ip_address: if ip_address:

View File

@ -56,25 +56,25 @@ COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
def _cover_processing(tmp_file_name, img, extension): def _cover_processing(tmp_file_name, img, extension):
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg') tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
if use_IM: if extension in NO_JPEG_EXTENSIONS:
# convert to jpg because calibre only supports jpg if use_IM:
if extension in NO_JPEG_EXTENSIONS: with Image(blob=img) as imgc:
with Image(filename=tmp_file_name) as imgc:
imgc.format = 'jpeg' imgc.format = 'jpeg'
imgc.transform_colorspace('rgb') imgc.transform_colorspace('rgb')
imgc.save(tmp_cover_name) imgc.save(filename=tmp_cover_name)
return tmp_cover_name return tmp_cover_name
else:
if not img: return None
if img:
with open(tmp_cover_name, 'wb') as f:
f.write(img)
return tmp_cover_name
else:
return None return None
with open(tmp_cover_name, 'wb') as f:
f.write(img)
return tmp_cover_name
def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable): def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable):
cover_data = None cover_data = extension = None
if original_file_extension.upper() == '.CBZ': if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name) cf = zipfile.ZipFile(tmp_file_name)
for name in cf.namelist(): for name in cf.namelist():
@ -106,7 +106,7 @@ def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecu
break break
except Exception as ex: except Exception as ex:
log.debug('Rarfile failed with error: %s', ex) log.debug('Rarfile failed with error: %s', ex)
return cover_data return cover_data, extension
def _extractCover(tmp_file_name, original_file_extension, rarExecutable): def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
@ -121,7 +121,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
cover_data = archive.getPage(index) cover_data = archive.getPage(index)
break break
else: else:
cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable) cover_data, extension = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
return _cover_processing(tmp_file_name, cover_data, extension) return _cover_processing(tmp_file_name, cover_data, extension)

View File

@ -151,7 +151,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages, publisher') 'series_id, languages, publisher')
STABLE_VERSION = {'version': '0.6.15 Beta'} STABLE_VERSION = {'version': '0.6.16 Beta'}
NIGHTLY_VERSION = {} NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$' NIGHTLY_VERSION[0] = '$Format:%H$'

View File

@ -23,6 +23,7 @@ import re
import ast import ast
import json import json
from datetime import datetime from datetime import datetime
from urllib.parse import quote
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
@ -164,6 +165,8 @@ class Identifiers(Base):
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val) return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
elif format_type == "isfdb": elif format_type == "isfdb":
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val) return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
elif self.val.lower().startswith("javascript:"):
return quote(self.val)
else: else:
return u"{0}".format(self.val) return u"{0}".format(self.val)
@ -172,8 +175,8 @@ class Comments(Base):
__tablename__ = 'comments' __tablename__ = 'comments'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
text = Column(String(collation='NOCASE'), nullable=False) text = Column(String(collation='NOCASE'), nullable=False)
book = Column(Integer, ForeignKey('books.id'), nullable=False)
def __init__(self, text, book): def __init__(self, text, book):
self.text = text self.text = text
@ -872,23 +875,24 @@ class CalibreDB():
def speaking_language(self, languages=None, return_all_languages=False, with_count=False, reverse_order=False): def speaking_language(self, languages=None, return_all_languages=False, with_count=False, reverse_order=False):
from . import get_locale from . import get_locale
if not languages: if with_count:
if with_count: if not languages:
languages = self.session.query(Languages, func.count('books_languages_link.book'))\ languages = self.session.query(Languages, func.count('books_languages_link.book'))\
.join(books_languages_link).join(Books)\ .join(books_languages_link).join(Books)\
.filter(self.common_filters(return_all_languages=return_all_languages)) \ .filter(self.common_filters(return_all_languages=return_all_languages)) \
.group_by(text('books_languages_link.lang_code')).all() .group_by(text('books_languages_link.lang_code')).all()
for lang in languages: for lang in languages:
lang[0].name = isoLanguages.get_language_name(get_locale(), lang[0].lang_code) lang[0].name = isoLanguages.get_language_name(get_locale(), lang[0].lang_code)
return sorted(languages, key=lambda x: x[0].name, reverse=reverse_order) return sorted(languages, key=lambda x: x[0].name, reverse=reverse_order)
else: else:
if not languages:
languages = self.session.query(Languages) \ languages = self.session.query(Languages) \
.join(books_languages_link) \ .join(books_languages_link) \
.join(Books) \ .join(Books) \
.filter(self.common_filters(return_all_languages=return_all_languages)) \ .filter(self.common_filters(return_all_languages=return_all_languages)) \
.group_by(text('books_languages_link.lang_code')).all() .group_by(text('books_languages_link.lang_code')).all()
for lang in languages: for lang in languages:
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code) lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
return sorted(languages, key=lambda x: x.name, reverse=reverse_order) return sorted(languages, key=lambda x: x.name, reverse=reverse_order)

32
cps/editbooks.py Normal file → Executable file
View File

@ -26,6 +26,8 @@ import json
from shutil import copyfile from shutil import copyfile
from uuid import uuid4 from uuid import uuid4
from markupsafe import escape from markupsafe import escape
from functools import wraps
try: try:
from lxml.html.clean import clean_html from lxml.html.clean import clean_html
except ImportError: except ImportError:
@ -52,13 +54,6 @@ from .render_template import render_title_template
from .usermanagement import login_required_if_no_ano from .usermanagement import login_required_if_no_ano
from .kobo_sync_status import change_archived_books from .kobo_sync_status import change_archived_books
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
editbook = Blueprint('editbook', __name__) editbook = Blueprint('editbook', __name__)
log = logger.create() log = logger.create()
@ -238,14 +233,14 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
changed = True changed = True
return changed, error return changed, error
@editbook.route("/ajax/delete/<int:book_id>") @editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@login_required @login_required
def delete_book_from_details(book_id): def delete_book_from_details(book_id):
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json') return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}) @editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
@editbook.route("/delete/<int:book_id>/<string:book_format>") @editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
@login_required @login_required
def delete_book_ajax(book_id, book_format): def delete_book_ajax(book_id, book_format):
return delete_book_from_table(book_id, book_format, False) return delete_book_from_table(book_id, book_format, False)
@ -347,6 +342,8 @@ def delete_book_from_table(book_id, book_format, jsonResponse):
else: else:
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete() filter(db.Data.format == book_format).delete()
if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']:
kobo_sync_status.remove_synced_book(book.id, True)
calibre_db.session.commit() calibre_db.session.commit()
except Exception as ex: except Exception as ex:
log.debug_or_exception(ex) log.debug_or_exception(ex)
@ -363,7 +360,16 @@ def delete_book_from_table(book_id, book_format, jsonResponse):
else: else:
# book not found # book not found
log.error('Book with id "%s" could not be deleted: not found', book_id) log.error('Book with id "%s" could not be deleted: not found', book_id)
return render_delete_book_result(book_format, jsonResponse, warning, book_id) return render_delete_book_result(book_format, jsonResponse, warning, book_id)
message = _("You are missing permissions to delete books")
if jsonResponse:
return json.dumps({"location": url_for("editbook.edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": message})
else:
flash(message, category="error")
return redirect(url_for('editbook.edit_book', book_id=book_id))
def render_edit_book(book_id): def render_edit_book(book_id):
@ -847,7 +853,7 @@ def edit_book(book_id):
if modif_date: if modif_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.utcnow()
kobo_sync_status.remove_synced_book(edited_books_id) kobo_sync_status.remove_synced_book(edited_books_id, all=True)
calibre_db.session.merge(book) calibre_db.session.merge(book)
calibre_db.session.commit() calibre_db.session.commit()
@ -1041,7 +1047,7 @@ def move_coverfile(meta, db_book):
category="error") category="error")
@editbook.route("/upload", methods=["GET", "POST"]) @editbook.route("/upload", methods=["POST"])
@login_required_if_no_ano @login_required_if_no_ano
@upload_required @upload_required
def upload(): def upload():

View File

@ -109,7 +109,7 @@ def revoke_watch_gdrive():
try: try:
gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'], gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'],
last_watch_response['resourceId']) last_watch_response['resourceId'])
except HttpError: except (HttpError, AttributeError):
pass pass
config.config_google_drive_watch_changes_response = {} config.config_google_drive_watch_changes_response = {}
config.save() config.save()

View File

@ -56,11 +56,13 @@ try:
from pydrive2.auth import GoogleAuth from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive from pydrive2.drive import GoogleDrive
from pydrive2.auth import RefreshError from pydrive2.auth import RefreshError
from pydrive2.files import ApiRequestError
except ImportError as err: except ImportError as err:
try: try:
from pydrive.auth import GoogleAuth from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError from pydrive.auth import RefreshError
from pydrive.files import ApiRequestError
except ImportError as err: except ImportError as err:
importError = err importError = err
gdrive_support = False gdrive_support = False
@ -322,6 +324,11 @@ def getFolderId(path, drive):
log.error("gdrive.db DB is not Writeable") log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex) log.debug('Database error: %s', ex)
session.rollback() session.rollback()
except ApiRequestError as ex:
log.error('{} {}'.format(ex.error['message'], path))
session.rollback()
except RefreshError as ex:
log.error(ex)
return currentFolderId return currentFolderId

View File

@ -17,18 +17,18 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os import os
import io import io
import mimetypes import mimetypes
import re import re
import shutil import shutil
import time import socket
import unicodedata import unicodedata
from datetime import datetime, timedelta from datetime import datetime, timedelta
from tempfile import gettempdir from tempfile import gettempdir
from urllib.parse import urlparse
import requests import requests
from babel.dates import format_datetime from babel.dates import format_datetime
from babel.units import format_unit from babel.units import format_unit
from flask import send_from_directory, make_response, redirect, abort, url_for from flask import send_from_directory, make_response, redirect, abort, url_for
@ -38,11 +38,7 @@ from sqlalchemy.sql.expression import true, false, and_, text, func
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from markupsafe import escape from markupsafe import escape
from urllib.parse import quote
try:
from urllib.parse import quote
except ImportError:
from urllib import quote
try: try:
import unidecode import unidecode
@ -50,7 +46,7 @@ try:
except ImportError: except ImportError:
use_unidecode = False use_unidecode = False
from . import calibre_db from . import calibre_db, cli
from .tasks.convert import TaskConvert from .tasks.convert import TaskConvert
from . import logger, config, get_locale, db, ub, kobo_sync_status from . import logger, config, get_locale, db, ub, kobo_sync_status
from . import gdriveutils as gd from . import gdriveutils as gd
@ -673,10 +669,17 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
# saves book cover from url # saves book cover from url
def save_cover_from_url(url, book_path): def save_cover_from_url(url, book_path):
try: try:
if not cli.allow_localhost:
# 127.0.x.x, localhost, [::1], [::ffff:7f00:1]
ip = socket.getaddrinfo(urlparse(url).hostname, 0)[0][4][0]
if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1":
log.error("Localhost was accessed for cover upload")
return False, _("You are not allowed to access localhost for cover uploads")
img = requests.get(url, timeout=(10, 200)) # ToDo: Error Handling img = requests.get(url, timeout=(10, 200)) # ToDo: Error Handling
img.raise_for_status() img.raise_for_status()
return save_cover(img, book_path) return save_cover(img, book_path)
except (requests.exceptions.HTTPError, except (socket.gaierror,
requests.exceptions.HTTPError,
requests.exceptions.ConnectionError, requests.exceptions.ConnectionError,
requests.exceptions.Timeout) as ex: requests.exceptions.Timeout) as ex:
log.info(u'Cover Download Error %s', ex) log.info(u'Cover Download Error %s', ex)
@ -758,9 +761,9 @@ def save_cover(img, book_path):
def do_download_file(book, book_format, client, data, headers): def do_download_file(book, book_format, client, data, headers):
if config.config_use_google_drive: if config.config_use_google_drive:
startTime = time.time() #startTime = time.time()
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format) df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
log.debug('%s', time.time() - startTime) #log.debug('%s', time.time() - startTime)
if df: if df:
return gd.do_gdrive_download(df, headers) return gd.do_gdrive_download(df, headers)
else: else:

View File

@ -102,6 +102,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolanguage)", "doi": "Dogri (macrolanguage)",
"dsb": "Sorbian; Lower", "dsb": "Sorbian; Lower",
"dse": "holandský znakový jazyk",
"dua": "dualština", "dua": "dualština",
"dum": "Dutch; Middle (ca. 1050-1350)", "dum": "Dutch; Middle (ca. 1050-1350)",
"dyu": "djula", "dyu": "djula",
@ -526,6 +527,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (Makrosprache)", "doi": "Dogri (Makrosprache)",
"dsb": "Sorbisch; Nieder", "dsb": "Sorbisch; Nieder",
"dse": "Niederländische Zeichensprache",
"dua": "Duala", "dua": "Duala",
"dum": "Niederländisch; Mittel (ca. 1050-1350)", "dum": "Niederländisch; Mittel (ca. 1050-1350)",
"dyu": "Dyula", "dyu": "Dyula",
@ -945,6 +947,7 @@ LANGUAGE_NAMES = {
"dgr": "Dogrib", "dgr": "Dogrib",
"dua": "Duala", "dua": "Duala",
"nld": "Ολλανδικά", "nld": "Ολλανδικά",
"dse": "Ολλανδική νοηματική γλώσσα",
"dyu": "Dyula", "dyu": "Dyula",
"dzo": "Dzongkha", "dzo": "Dzongkha",
"efi": "Efik", "efi": "Efik",
@ -1329,6 +1332,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolengua)", "doi": "Dogri (macrolengua)",
"dsb": "Bajo sorabo", "dsb": "Bajo sorabo",
"dse": "Lengua de signos neerlandesa",
"dua": "Duala", "dua": "Duala",
"dum": "Neerlandés medio (ca. 1050-1350)", "dum": "Neerlandés medio (ca. 1050-1350)",
"dyu": "Diula", "dyu": "Diula",
@ -1753,6 +1757,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolanguage)", "doi": "Dogri (macrolanguage)",
"dsb": "alasorbi", "dsb": "alasorbi",
"dse": "Dutch Sign Language",
"dua": "duala", "dua": "duala",
"dum": "Dutch; Middle (ca. 1050-1350)", "dum": "Dutch; Middle (ca. 1050-1350)",
"dyu": "dyula", "dyu": "dyula",
@ -2177,6 +2182,7 @@ LANGUAGE_NAMES = {
"div": "dhivehi", "div": "dhivehi",
"doi": "dogri (macrolangue)", "doi": "dogri (macrolangue)",
"dsb": "bas-sorbien", "dsb": "bas-sorbien",
"dse": "langue des signes néerlandaise",
"dua": "duala", "dua": "duala",
"dum": "néerlandais moyen (environ 1050-1350)", "dum": "néerlandais moyen (environ 1050-1350)",
"dyu": "dioula", "dyu": "dioula",
@ -2601,6 +2607,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolanguage)", "doi": "Dogri (macrolanguage)",
"dsb": "Sorbian; Lower", "dsb": "Sorbian; Lower",
"dse": "Dutch Sign Language",
"dua": "duala", "dua": "duala",
"dum": "Dutch; Middle (ca. 1050-1350)", "dum": "Dutch; Middle (ca. 1050-1350)",
"dyu": "djula", "dyu": "djula",
@ -3025,6 +3032,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolingua)", "doi": "Dogri (macrolingua)",
"dsb": "Lusaziano inferiore", "dsb": "Lusaziano inferiore",
"dse": "Olandense (linguaggio dei segni)",
"dua": "Duala", "dua": "Duala",
"dum": "Olandese medio (ca. 1050-1350)", "dum": "Olandese medio (ca. 1050-1350)",
"dyu": "Diula", "dyu": "Diula",
@ -3449,6 +3457,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolanguage)", "doi": "Dogri (macrolanguage)",
"dsb": "Sorbian; Lower", "dsb": "Sorbian; Lower",
"dse": "Dutch Sign Language",
"dua": "ドゥアラ語", "dua": "ドゥアラ語",
"dum": "Dutch; Middle (ca. 1050-1350)", "dum": "Dutch; Middle (ca. 1050-1350)",
"dyu": "デュラ語", "dyu": "デュラ語",
@ -3873,6 +3882,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolanguage)", "doi": "Dogri (macrolanguage)",
"dsb": "Sorbian; Lower", "dsb": "Sorbian; Lower",
"dse": "Dutch Sign Language",
"dua": "Duala", "dua": "Duala",
"dum": "Dutch; Middle (ca. 1050-1350)", "dum": "Dutch; Middle (ca. 1050-1350)",
"dyu": "Dyula", "dyu": "Dyula",
@ -4207,6 +4217,384 @@ LANGUAGE_NAMES = {
"zxx": "No linguistic content", "zxx": "No linguistic content",
"zza": "Zaza" "zza": "Zaza"
}, },
"ko": {
"abk": "압하스어",
"ace": "아체어",
"ach": "아촐리어",
"ada": "Adangme",
"ady": "Adyghe",
"aar": "아파르어",
"afh": "Afrihili",
"afr": "아프리칸스어",
"ain": "Ainu (Japan)",
"aka": "Akan",
"akk": "Akkadian",
"sqi": "Albanian",
"ale": "Aleut",
"amh": "Amharic",
"anp": "Angika",
"ara": "아라비아어",
"arg": "Aragonese",
"arp": "Arapaho",
"arw": "Arawak",
"hye": "아르메니아어",
"asm": "Assamese",
"ast": "Asturian",
"ava": "Avaric",
"ave": "아베스타어",
"awa": "Awadhi",
"aym": "Aymara",
"aze": "Azerbaijani",
"ban": "발리 문자",
"bal": "Baluchi",
"bam": "Bambara",
"bas": "Basa (Cameroon)",
"bak": "Bashkir",
"eus": "바스크어",
"bej": "Beja",
"bel": "벨로루시어",
"bem": "Bemba (Zambia)",
"ben": "벵골 문자",
"bit": "Berinomo",
"bho": "Bhojpuri",
"bik": "Bikol",
"byn": "Bilin",
"bin": "Bini",
"bis": "Bislama",
"zbl": "Blissymbols",
"bos": "Bosnian",
"bra": "Braj",
"bre": "Breton",
"bug": "부기 문자",
"bul": "불가리아어",
"bua": "Buriat",
"mya": "Burmese",
"cad": "Caddo",
"cat": "카탈로니아어",
"ceb": "Cebuano",
"chg": "Chagatai",
"cha": "Chamorro",
"che": "Chechen",
"chr": "체로키 문자",
"chy": "Cheyenne",
"chb": "Chibcha",
"zho": "중국어",
"chn": "Chinook jargon",
"chp": "Chipewyan",
"cho": "Choctaw",
"cht": "Cholón",
"chk": "Chuukese",
"chv": "Chuvash",
"cop": "콥트어",
"cor": "Cornish",
"cos": "Corsican",
"cre": "Cree",
"mus": "Creek",
"hrv": "크로아티아어",
"ces": "체크어",
"dak": "Dakota",
"dan": "덴마크어",
"dar": "Dargwa",
"del": "Delaware",
"div": "Dhivehi",
"din": "Dinka",
"doi": "Dogri (macrolanguage)",
"dgr": "Dogrib",
"dua": "Duala",
"nld": "네덜란드어",
"dse": "Dutch Sign Language",
"dyu": "Dyula",
"dzo": "Dzongkha",
"efi": "Efik",
"egy": "Egyptian (Ancient)",
"eka": "Ekajuk",
"elx": "Elamite",
"eng": "영어",
"enu": "Enu",
"myv": "Erzya",
"epo": "에스페란토어",
"est": "에스토니아어",
"ewe": "Ewe",
"ewo": "Ewondo",
"fan": "Fang (Equatorial Guinea)",
"fat": "Fanti",
"fao": "페로스어",
"fij": "Fijian",
"fil": "Filipino",
"fin": "핀란드어",
"fon": "Fon",
"fra": "프랑스어",
"fur": "Friulian",
"ful": "Fulah",
"gaa": "Ga",
"glg": "Galician",
"lug": "Ganda",
"gay": "Gayo",
"gba": "Gbaya (Central African Republic)",
"hmj": "Ge",
"gez": "Geez",
"kat": "그루지야어",
"deu": "독일어",
"gil": "Gilbertese",
"gon": "Gondi",
"gor": "Gorontalo",
"got": "고트어",
"grb": "Grebo",
"grn": "Guarani",
"guj": "구자라트 문자",
"gwi": "Gwichʼin",
"hai": "Haida",
"hau": "Hausa",
"haw": "Hawaiian",
"heb": "헤브루어",
"her": "Herero",
"hil": "Hiligaynon",
"hin": "Hindi",
"hmo": "Hiri Motu",
"hit": "Hittite",
"hmn": "Hmong",
"hun": "헝가리어",
"hup": "Hupa",
"iba": "Iban",
"isl": "아이슬란드어",
"ido": "Ido",
"ibo": "Igbo",
"ilo": "Iloko",
"ind": "인도네시아어",
"inh": "Ingush",
"ina": "Interlingua (International Auxiliary Language Association)",
"ile": "Interlingue",
"iku": "Inuktitut",
"ipk": "Inupiaq",
"gle": "아일랜드어",
"ita": "이탈리아어",
"jpn": "일본어",
"jav": "Javanese",
"jrb": "Judeo-Arabic",
"jpr": "Judeo-Persian",
"kbd": "Kabardian",
"kab": "Kabyle",
"kac": "Kachin",
"kal": "Kalaallisut",
"xal": "Kalmyk",
"kam": "Kamba (Kenya)",
"kan": " 칸나다 문자",
"kau": "Kanuri",
"kaa": "Kara-Kalpak",
"krc": "Karachay-Balkar",
"krl": "Karelian",
"kas": "Kashmiri",
"csb": "Kashubian",
"kaw": "Kawi",
"kaz": "Kazakh",
"kha": "Khasi",
"kho": "Khotanese",
"kik": "Kikuyu",
"kmb": "Kimbundu",
"kin": "Kinyarwanda",
"kir": "Kirghiz",
"tlh": "Klingon",
"kom": "Komi",
"kon": "Kongo",
"kok": "Konkani (macrolanguage)",
"kor": "한국어",
"kos": "Kosraean",
"kpe": "Kpelle",
"kua": "Kuanyama",
"kum": "Kumyk",
"kur": "Kurdish",
"kru": "Kurukh",
"kut": "Kutenai",
"lad": "Ladino",
"lah": "Lahnda",
"lam": "Lamba",
"lao": "라오 문자",
"lat": "Latin",
"lav": "라트비아어",
"lez": "Lezghian",
"lim": "Limburgan",
"lin": "Lingala",
"lit": "리투아니아어",
"jbo": "Lojban",
"loz": "Lozi",
"lub": "Luba-Katanga",
"lua": "Luba-Lulua",
"lui": "Luiseno",
"smj": "Lule Sami",
"lun": "Lunda",
"luo": "Luo (Kenya and Tanzania)",
"lus": "Lushai",
"ltz": "Luxembourgish",
"mkd": "마케도니아어",
"mad": "Madurese",
"mag": "Magahi",
"mai": "Maithili",
"mak": "Makasar",
"mlg": "Malagasy",
"msa": "Malay (macrolanguage)",
"mal": "말라얄람 문자",
"mlt": "Maltese",
"mnc": "Manchu",
"mdr": "Mandar",
"man": "Mandingo",
"mni": "Manipuri",
"glv": "Manx",
"mri": "Maori",
"arn": "Mapudungun",
"mar": "Marathi",
"chm": "Mari (Russia)",
"mah": "Marshallese",
"mwr": "Marwari",
"mas": "Masai",
"men": "Mende (Sierra Leone)",
"mic": "Mi'kmaq",
"min": "Minangkabau",
"mwl": "Mirandese",
"moh": "Mohawk",
"mdf": "Moksha",
"lol": "Mongo",
"mon": "몽골 문자",
"mos": "Mossi",
"mul": "Multiple languages",
"nqo": "응코 문자",
"nau": "나우루어",
"nav": "나바호어",
"ndo": "Ndonga",
"nap": "Neapolitan",
"nia": "Nias",
"niu": "Niuean",
"zxx": "No linguistic content",
"nog": "Nogai",
"nor": "노르웨이어",
"nob": "Norwegian Bokmål",
"nno": "Norwegian Nynorsk",
"nym": "Nyamwezi",
"nya": "Nyanja",
"nyn": "Nyankole",
"nyo": "Nyoro",
"nzi": "Nzima",
"oci": "Occitan (post 1500)",
"oji": "Ojibwa",
"orm": "Oromo",
"osa": "Osage",
"oss": "Ossetian",
"pal": "Pahlavi",
"pau": "Palauan",
"pli": "Pali",
"pam": "Pampanga",
"pag": "Pangasinan",
"pan": "Panjabi",
"pap": "Papiamento",
"fas": "Persian",
"phn": " 페니키아 문자",
"pon": "Pohnpeian",
"pol": "폴란드어",
"por": "포르투갈어",
"pus": "Pashto",
"que": "Quechua",
"raj": "Rajasthani",
"rap": "Rapanui",
"ron": "루마니아어",
"roh": "Romansh",
"rom": "Romany",
"run": "Rundi",
"rus": "러시아어",
"smo": "Samoan",
"sad": "Sandawe",
"sag": "Sango",
"san": "Sanskrit",
"sat": "Santali",
"srd": "Sardinian",
"sas": "Sasak",
"sco": "Scots",
"sel": "Selkup",
"srp": "세르비아어",
"srr": "Serer",
"shn": "Shan",
"sna": "Shona",
"scn": "Sicilian",
"sid": "Sidamo",
"bla": "Siksika",
"snd": "Sindhi",
"sin": "싱할라 문자",
"den": "Slave (Athapascan)",
"slk": "슬로바키아어",
"slv": "슬로베니아어",
"sog": "Sogdian",
"som": "Somali",
"snk": "Soninke",
"spa": "스페인어",
"srn": "Sranan Tongo",
"suk": "Sukuma",
"sux": "Sumerian",
"sun": "Sundanese",
"sus": "Susu",
"swa": "Swahili (macrolanguage)",
"ssw": "Swati",
"swe": "스웨덴어",
"syr": "시리아 문자",
"tgl": "타갈로그 문자",
"tah": "Tahitian",
"tgk": "Tajik",
"tmh": "Tamashek",
"tam": "타밀 문자",
"tat": "Tatar",
"tel": "텔루구 문자",
"ter": "Tereno",
"tet": "Tetum",
"tha": "태국어",
"bod": "티베트 문자",
"tig": "Tigre",
"tir": "Tigrinya",
"tem": "Timne",
"tiv": "Tiv",
"tli": "Tlingit",
"tpi": "Tok Pisin",
"tkl": "Tokelau",
"tog": "Tonga (Nyasa)",
"ton": "Tonga (Tonga Islands)",
"tsi": "Tsimshian",
"tso": "Tsonga",
"tsn": "Tswana",
"tum": "Tumbuka",
"tur": "터키어",
"tuk": "Turkmen",
"tvl": "Tuvalu",
"tyv": "Tuvinian",
"twi": "Twi",
"udm": "Udmurt",
"uga": "우가리트 문자",
"uig": "Uighur",
"ukr": "Ukrainian",
"umb": "Umbundu",
"mis": "Uncoded languages",
"und": "Undetermined",
"urd": "Urdu",
"uzb": "Uzbek",
"vai": "Vai",
"ven": "Venda",
"vie": "베트남어",
"vol": "Volapük",
"vot": "Votic",
"wln": "Walloon",
"war": "Waray (Philippines)",
"was": "Washo",
"cym": "Welsh",
"wal": "Wolaytta",
"wol": "Wolof",
"xho": "Xhosa",
"sah": "Yakut",
"yao": "Yao",
"yap": "Yapese",
"yid": "Yiddish",
"yor": "Yoruba",
"zap": "Zapotec",
"zza": "Zaza",
"zen": "Zenaga",
"zha": "Zhuang",
"zul": "Zulu",
"zun": "Zuni"
},
"nl": { "nl": {
"aar": "Afar; Hamitisch", "aar": "Afar; Hamitisch",
"abk": "Abchazisch", "abk": "Abchazisch",
@ -4297,6 +4685,7 @@ LANGUAGE_NAMES = {
"div": "Divehi", "div": "Divehi",
"doi": "Dogri", "doi": "Dogri",
"dsb": "Sorbisch; lager", "dsb": "Sorbisch; lager",
"dse": "Nederlandse gebarentaal",
"dua": "Duala", "dua": "Duala",
"dum": "Nederlands; middel (ca. 1050-1350)", "dum": "Nederlands; middel (ca. 1050-1350)",
"dyu": "Dyula", "dyu": "Dyula",
@ -4721,6 +5110,7 @@ LANGUAGE_NAMES = {
"div": "malediwski; divehi", "div": "malediwski; divehi",
"doi": "dogri (makrojęzyk)", "doi": "dogri (makrojęzyk)",
"dsb": "dolnołużycki", "dsb": "dolnołużycki",
"dse": "holenderski język migowy",
"dua": "duala", "dua": "duala",
"dum": "holenderski średniowieczny (ok. 1050-1350)", "dum": "holenderski średniowieczny (ok. 1050-1350)",
"dyu": "diula", "dyu": "diula",
@ -5140,6 +5530,7 @@ LANGUAGE_NAMES = {
"dgr": "Dogrib", "dgr": "Dogrib",
"dua": "Duala", "dua": "Duala",
"nld": "Holandês", "nld": "Holandês",
"dse": "Língua gestual holandesa",
"dyu": "Dyula", "dyu": "Dyula",
"dzo": "Dzongkha", "dzo": "Dzongkha",
"efi": "Efik", "efi": "Efik",
@ -5522,6 +5913,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolanguage)", "doi": "Dogri (macrolanguage)",
"dsb": "Sorbian; Lower", "dsb": "Sorbian; Lower",
"dse": "Dutch Sign Language",
"dua": "Дуала", "dua": "Дуала",
"dum": "Dutch; Middle (ca. 1050-1350)", "dum": "Dutch; Middle (ca. 1050-1350)",
"dyu": "Диула (Дьюла)", "dyu": "Диула (Дьюла)",
@ -5946,6 +6338,7 @@ LANGUAGE_NAMES = {
"div": "Divehi", "div": "Divehi",
"doi": "Dogri (macrolanguage)", "doi": "Dogri (macrolanguage)",
"dsb": "Sorbian; nedre", "dsb": "Sorbian; nedre",
"dse": "Nederländskt teckenspråk",
"dua": "Duala", "dua": "Duala",
"dum": "Hollänska; medeltida (ca. 1050-1350)", "dum": "Hollänska; medeltida (ca. 1050-1350)",
"dyu": "Dyula", "dyu": "Dyula",
@ -6365,6 +6758,7 @@ LANGUAGE_NAMES = {
"dgr": "Dogrib (Kanada)", "dgr": "Dogrib (Kanada)",
"dua": "Duala (Afrika)", "dua": "Duala (Afrika)",
"nld": "Flâmanca (Hollanda dili)", "nld": "Flâmanca (Hollanda dili)",
"dse": "Hollandalı İşaret Dili",
"dyu": "Dyula (Burkina Faso; Mali)", "dyu": "Dyula (Burkina Faso; Mali)",
"dzo": "Dzongkha (Butan)", "dzo": "Dzongkha (Butan)",
"efi": "Efik (Afrika)", "efi": "Efik (Afrika)",
@ -6747,6 +7141,7 @@ LANGUAGE_NAMES = {
"div": "мальдивська", "div": "мальдивська",
"doi": "догрі (макромова)", "doi": "догрі (макромова)",
"dsb": "нижньолужицька", "dsb": "нижньолужицька",
"dse": "голландська мова жестів",
"dua": "дуала", "dua": "дуала",
"dum": "середньовічна голландська (бл. 1050-1350)", "dum": "середньовічна голландська (бл. 1050-1350)",
"dyu": "діула", "dyu": "діула",
@ -7171,6 +7566,7 @@ LANGUAGE_NAMES = {
"div": "迪维希语", "div": "迪维希语",
"doi": "多格拉语", "doi": "多格拉语",
"dsb": "索布语(下)", "dsb": "索布语(下)",
"dse": "荷兰手语",
"dua": "杜亚拉语", "dua": "杜亚拉语",
"dum": "荷兰语(中古,约 1050-1350", "dum": "荷兰语(中古,约 1050-1350",
"dyu": "迪尤拉语", "dyu": "迪尤拉语",
@ -7590,6 +7986,7 @@ LANGUAGE_NAMES = {
"dgr": "Dogrib", "dgr": "Dogrib",
"dua": "Duala", "dua": "Duala",
"nld": "荷蘭文", "nld": "荷蘭文",
"dse": "Dutch Sign Language",
"dyu": "Dyula", "dyu": "Dyula",
"dzo": "Dzongkha", "dzo": "Dzongkha",
"efi": "Efik", "efi": "Efik",
@ -7973,6 +8370,7 @@ LANGUAGE_NAMES = {
"div": "Dhivehi", "div": "Dhivehi",
"doi": "Dogri (macrolanguage)", "doi": "Dogri (macrolanguage)",
"dsb": "Sorbian; Lower", "dsb": "Sorbian; Lower",
"dse": "Dutch Sign Language",
"dua": "Duala", "dua": "Duala",
"dum": "Dutch; Middle (ca. 1050-1350)", "dum": "Dutch; Middle (ca. 1050-1350)",
"dyu": "Dyula", "dyu": "Dyula",

View File

@ -23,11 +23,7 @@ import os
import uuid import uuid
from time import gmtime, strftime from time import gmtime, strftime
import json import json
from urllib.parse import unquote
try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from flask import ( from flask import (
Blueprint, Blueprint,
@ -177,10 +173,10 @@ def HandleSyncRequest():
ub.BookShelf.date_added, ub.BookShelf.date_added,
ub.ArchivedBook.is_archived) ub.ArchivedBook.is_archived)
changed_entries = (changed_entries changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) .join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True) ub.ArchivedBook.user_id == current_user.id))
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id, .filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
ub.KoboSyncedBooks.book_id == None)) .filter(ub.KoboSyncedBooks.user_id == current_user.id)))
.filter(ub.BookShelf.date_added > sync_token.books_last_modified) .filter(ub.BookShelf.date_added > sync_token.books_last_modified)
.filter(db.Data.format.in_(KOBO_FORMATS)) .filter(db.Data.format.in_(KOBO_FORMATS))
.filter(calibre_db.common_filters(allow_show_archived=True)) .filter(calibre_db.common_filters(allow_show_archived=True))
@ -200,11 +196,11 @@ def HandleSyncRequest():
ub.ArchivedBook.last_modified, ub.ArchivedBook.last_modified,
ub.ArchivedBook.is_archived) ub.ArchivedBook.is_archived)
changed_entries = (changed_entries changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) .join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True) ub.ArchivedBook.user_id == current_user.id))
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id, .filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
ub.KoboSyncedBooks.book_id == None)) .filter(ub.KoboSyncedBooks.user_id == current_user.id)))
.filter(calibre_db.common_filters()) .filter(calibre_db.common_filters(allow_show_archived=True))
.filter(db.Data.format.in_(KOBO_FORMATS)) .filter(db.Data.format.in_(KOBO_FORMATS))
.order_by(db.Books.last_modified) .order_by(db.Books.last_modified)
.order_by(db.Books.id) .order_by(db.Books.id)
@ -261,10 +257,12 @@ def HandleSyncRequest():
if sqlalchemy_version2: if sqlalchemy_version2:
max_change = calibre_db.session.execute(changed_entries max_change = calibre_db.session.execute(changed_entries
.filter(ub.ArchivedBook.is_archived) .filter(ub.ArchivedBook.is_archived)
.filter(ub.ArchivedBook.user_id == current_user.id)
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\ .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
.columns(db.Books).first() .columns(db.Books).first()
else: else:
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived) \ max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
.filter(ub.ArchivedBook.user_id==current_user.id) \
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first() .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
max_change = max_change.last_modified if max_change else new_archived_last_modified max_change = max_change.last_modified if max_change else new_archived_last_modified
@ -299,7 +297,8 @@ def HandleSyncRequest():
changed_reading_states = changed_reading_states.filter( changed_reading_states = changed_reading_states.filter(
and_(ub.KoboReadingState.user_id == current_user.id, and_(ub.KoboReadingState.user_id == current_user.id,
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))) ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))\
.order_by(ub.KoboReadingState.last_modified)
cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT) cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT)
for kobo_reading_state in changed_reading_states.limit(SYNC_ITEM_LIMIT).all(): for kobo_reading_state in changed_reading_states.limit(SYNC_ITEM_LIMIT).all():
book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none() book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
@ -325,7 +324,7 @@ def HandleSyncRequest():
def generate_sync_response(sync_token, sync_results, set_cont=False): def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers = {} extra_headers = {}
if config.config_kobo_proxy: if config.config_kobo_proxy and not set_cont:
# Merge in sync results from the official Kobo store. # Merge in sync results from the official Kobo store.
try: try:
store_response = make_request_to_kobo_store(sync_token) store_response = make_request_to_kobo_store(sync_token)
@ -343,7 +342,7 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers["x-kobo-sync"] = "continue" extra_headers["x-kobo-sync"] = "continue"
sync_token.to_headers(extra_headers) sync_token.to_headers(extra_headers)
log.debug("Kobo Sync Content: {}".format(sync_results)) # log.debug("Kobo Sync Content: {}".format(sync_results))
# jsonify decodes the unicode string different to what kobo expects # jsonify decodes the unicode string different to what kobo expects
response = make_response(json.dumps(sync_results), extra_headers) response = make_response(json.dumps(sync_results), extra_headers)
response.headers["Content-Type"] = "application/json; charset=utf-8" response.headers["Content-Type"] = "application/json; charset=utf-8"
@ -675,11 +674,8 @@ def HandleTagRemoveItem(tag_id):
# Note: Public shelves that aren't owned by the user aren't supported. # Note: Public shelves that aren't owned by the user aren't supported.
def sync_shelves(sync_token, sync_results, only_kobo_shelves=False): def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
new_tags_last_modified = sync_token.tags_last_modified new_tags_last_modified = sync_token.tags_last_modified
# transmit all archived shelfs independent of last sync (why should this matter?)
for shelf in ub.session.query(ub.ShelfArchive).filter( for shelf in ub.session.query(ub.ShelfArchive).filter(ub.ShelfArchive.user_id == current_user.id):
func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified,
ub.ShelfArchive.user_id == current_user.id
):
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified) new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
sync_results.append({ sync_results.append({
"DeletedTag": { "DeletedTag": {
@ -692,7 +688,6 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
ub.session.delete(shelf) ub.session.delete(shelf)
ub.session_commit() ub.session_commit()
extra_filters = [] extra_filters = []
if only_kobo_shelves: if only_kobo_shelves:
for shelf in ub.session.query(ub.Shelf).filter( for shelf in ub.session.query(ub.Shelf).filter(
@ -855,7 +850,7 @@ def get_ub_read_status(kobo_read_status):
def get_or_create_reading_state(book_id): def get_or_create_reading_state(book_id):
book_read = ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id, book_read = ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id,
ub.ReadBook.user_id == current_user.id).one_or_none() ub.ReadBook.user_id == int(current_user.id)).one_or_none()
if not book_read: if not book_read:
book_read = ub.ReadBook(user_id=current_user.id, book_id=book_id) book_read = ub.ReadBook(user_id=current_user.id, book_id=book_id)
if not book_read.kobo_reading_state: if not book_read.kobo_reading_state:

View File

@ -62,6 +62,7 @@ particular calls to non-Kobo specific endpoints such as the CalibreWeb book down
from binascii import hexlify from binascii import hexlify
from datetime import datetime from datetime import datetime
from os import urandom from os import urandom
from functools import wraps
from flask import g, Blueprint, url_for, abort, request from flask import g, Blueprint, url_for, abort, request
from flask_login import login_user, current_user, login_required from flask_login import login_user, current_user, login_required
@ -70,11 +71,6 @@ from flask_babel import gettext as _
from . import logger, config, calibre_db, db, helper, ub, lm from . import logger, config, calibre_db, db, helper, ub, lm
from .render_template import render_title_template from .render_template import render_title_template
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
log = logger.create() log = logger.create()
@ -122,55 +118,49 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
@kobo_auth.route("/generate_auth_token/<int:user_id>") @kobo_auth.route("/generate_auth_token/<int:user_id>")
@login_required @login_required
def generate_auth_token(user_id): def generate_auth_token(user_id):
warning = False
host_list = request.host.rsplit(':') host_list = request.host.rsplit(':')
if len(host_list) == 1: if len(host_list) == 1:
host = ':'.join(host_list) host = ':'.join(host_list)
else: else:
host = ':'.join(host_list[0:-1]) host = ':'.join(host_list[0:-1])
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'): if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f') or host == "[::1]":
warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device') warning = _('Please access Calibre-Web from non localhost to get valid api_endpoint for kobo device')
return render_title_template(
"generate_kobo_auth_url.html",
title=_(u"Kobo Setup"),
warning = warning
)
else:
# Invalidate any prevously generated Kobo Auth token for this user.
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
ub.RemoteAuthToken.user_id == user_id
).filter(ub.RemoteAuthToken.token_type==1).first()
if not auth_token: # Generate auth token if none is existing for this user
auth_token = ub.RemoteAuthToken() auth_token = ub.session.query(ub.RemoteAuthToken).filter(
auth_token.user_id = user_id ub.RemoteAuthToken.user_id == user_id
auth_token.expiration = datetime.max ).filter(ub.RemoteAuthToken.token_type==1).first()
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
auth_token.token_type = 1
ub.session.add(auth_token) if not auth_token:
ub.session_commit() auth_token = ub.RemoteAuthToken()
auth_token.user_id = user_id
auth_token.expiration = datetime.max
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
auth_token.token_type = 1
books = calibre_db.session.query(db.Books).join(db.Data).all() ub.session.add(auth_token)
ub.session_commit()
for book in books: books = calibre_db.session.query(db.Books).join(db.Data).all()
formats = [data.format for data in book.data]
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
return render_title_template( for book in books:
"generate_kobo_auth_url.html", formats = [data.format for data in book.data]
title=_(u"Kobo Setup"), if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
kobo_auth_url=url_for( helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
), return render_title_template(
warning = False "generate_kobo_auth_url.html",
) title=_(u"Kobo Setup"),
auth_token=auth_token.auth_token,
warning = warning
)
@kobo_auth.route("/deleteauthtoken/<int:user_id>") @kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
@login_required @login_required
def delete_auth_token(user_id): def delete_auth_token(user_id):
# Invalidate any prevously generated Kobo Auth token for this user. # Invalidate any previously generated Kobo Auth token for this user
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\ ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
.filter(ub.RemoteAuthToken.token_type==1).delete() .filter(ub.RemoteAuthToken.token_type==1).delete()

View File

@ -20,7 +20,8 @@
from flask_login import current_user from flask_login import current_user
from . import ub from . import ub
import datetime import datetime
from sqlalchemy.sql.expression import or_, and_ from sqlalchemy.sql.expression import or_, and_, true
from sqlalchemy import exc
# Add the current book id to kobo_synced_books table for current user, if entry is already present, # Add the current book id to kobo_synced_books table for current user, if entry is already present,
# do nothing (safety precaution) # do nothing (safety precaution)
@ -36,10 +37,18 @@ def add_synced_books(book_id):
# Select all entries of current book in kobo_synced_books table, which are from current user and delete them # Select all entries of current book in kobo_synced_books table, which are from current user and delete them
def remove_synced_book(book_id): def remove_synced_book(book_id, all=False, session=None):
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id) \ if not all:
.filter(ub.KoboSyncedBooks.user_id == current_user.id).delete() user = ub.KoboSyncedBooks.user_id == current_user.id
ub.session_commit() else:
user = true()
if not session:
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete()
ub.session_commit()
else:
session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete()
ub.session_commit(sess=session)
def change_archived_books(book_id, state=None, message=None): def change_archived_books(book_id, state=None, message=None):
@ -56,7 +65,7 @@ def change_archived_books(book_id, state=None, message=None):
return archived_book.is_archived return archived_book.is_archived
# select all books which are synced by the current user and do not belong to a synced shelf and them to archive # select all books which are synced by the current user and do not belong to a synced shelf and set them to archive
# select all shelves from current user which are synced and do not belong to the "only sync" shelves # select all shelves from current user which are synced and do not belong to the "only sync" shelves
def update_on_sync_shelfs(user_id): def update_on_sync_shelfs(user_id):
books_to_archive = (ub.session.query(ub.KoboSyncedBooks) books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
@ -71,6 +80,7 @@ def update_on_sync_shelfs(user_id):
.filter(ub.KoboSyncedBooks.user_id == user_id).delete() .filter(ub.KoboSyncedBooks.user_id == user_id).delete()
ub.session_commit() ub.session_commit()
# Search all shelf which are currently not synced
shelves_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == user_id).filter( shelves_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == user_id).filter(
ub.Shelf.kobo_sync == 0).all() ub.Shelf.kobo_sync == 0).all()
for a in shelves_to_archive: for a in shelves_to_archive:

View File

@ -42,20 +42,15 @@ logging.addLevelName(logging.CRITICAL, "CRIT")
class _Logger(logging.Logger): class _Logger(logging.Logger):
def debug_or_exception(self, message, *args, **kwargs): def debug_or_exception(self, message, stacklevel=2, *args, **kwargs):
if sys.version_info > (3, 7): if sys.version_info > (3, 7):
if is_debug_enabled(): if is_debug_enabled():
self.exception(message, stacklevel=2, *args, **kwargs) self.exception(message, stacklevel=stacklevel, *args, **kwargs)
else: else:
self.error(message, stacklevel=2, *args, **kwargs) self.error(message, stacklevel=stacklevel, *args, **kwargs)
elif sys.version_info > (3, 0):
if is_debug_enabled():
self.exception(message, stack_info=True, *args, **kwargs)
else:
self.error(message, *args, **kwargs)
else: else:
if is_debug_enabled(): if is_debug_enabled():
self.exception(message, *args, **kwargs) self.exception(message, stack_info=True, *args, **kwargs)
else: else:
self.error(message, *args, **kwargs) self.error(message, *args, **kwargs)

View File

@ -26,7 +26,7 @@ class ComicVine(Metadata):
__name__ = "ComicVine" __name__ = "ComicVine"
__id__ = "comicvine" __id__ = "comicvine"
def search(self, query, __): def search(self, query, generic_cover=""):
val = list() val = list()
apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6" apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6"
if self.active: if self.active:
@ -36,7 +36,7 @@ class ComicVine(Metadata):
result = requests.get("https://comicvine.gamespot.com/api/search?api_key=" result = requests.get("https://comicvine.gamespot.com/api/search?api_key="
+ apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers) + apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers)
for r in result.json()['results']: for r in result.json().get('results'):
seriesTitle = r['volume'].get('name', "") seriesTitle = r['volume'].get('name', "")
if r.get('store_date'): if r.get('store_date'):
dateFomers = r.get('store_date') dateFomers = r.get('store_date')

View File

@ -26,14 +26,14 @@ class Google(Metadata):
__name__ = "Google" __name__ = "Google"
__id__ = "google" __id__ = "google"
def search(self, query, __): def search(self, query, generic_cover=""):
if self.active: if self.active:
val = list() val = list()
result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+")) result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+"))
for r in result.json()['items']: for r in result.json().get('items'):
v = dict() v = dict()
v['id'] = r['id'] v['id'] = r['id']
v['title'] = r['volumeInfo']['title'] v['title'] = r['volumeInfo'].get('title',"")
v['authors'] = r['volumeInfo'].get('authors', []) v['authors'] = r['volumeInfo'].get('authors', [])
v['description'] = r['volumeInfo'].get('description', "") v['description'] = r['volumeInfo'].get('description', "")
v['publisher'] = r['volumeInfo'].get('publisher', "") v['publisher'] = r['volumeInfo'].get('publisher', "")

View File

@ -20,7 +20,6 @@ from scholarly import scholarly
from cps.services.Metadata import Metadata from cps.services.Metadata import Metadata
class scholar(Metadata): class scholar(Metadata):
__name__ = "Google Scholar" __name__ = "Google Scholar"
__id__ = "googlescholar" __id__ = "googlescholar"
@ -32,7 +31,7 @@ class scholar(Metadata):
i = 0 i = 0
for publication in scholar_gen: for publication in scholar_gen:
v = dict() v = dict()
v['id'] = "1234" # publication['bib'].get('title') v['id'] = publication['url_scholarbib'].split(':')[1]
v['title'] = publication['bib'].get('title') v['title'] = publication['bib'].get('title')
v['authors'] = publication['bib'].get('author', []) v['authors'] = publication['bib'].get('author', [])
v['description'] = publication['bib'].get('abstract', "") v['description'] = publication['bib'].get('abstract', "")
@ -41,10 +40,10 @@ class scholar(Metadata):
v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01" v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01"
else: else:
v['publishedDate'] = "" v['publishedDate'] = ""
v['tags'] = "" v['tags'] = []
v['ratings'] = 0 v['rating'] = 0
v['series'] = "" v['series'] = ""
v['cover'] = generic_cover v['cover'] = ""
v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "", v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "",
v['source'] = { v['source'] = {
"id": self.__id__, "id": self.__id__,

View File

@ -27,10 +27,8 @@
# http://flask.pocoo.org/snippets/62/ # http://flask.pocoo.org/snippets/62/
try: from urllib.parse import urlparse, urljoin
from urllib.parse import urlparse, urljoin
except ImportError:
from urlparse import urlparse, urljoin
from flask import request, url_for, redirect from flask import request, url_for, redirect

View File

@ -103,9 +103,9 @@ def metadata_search():
data = list() data = list()
active = current_user.view_settings.get('metadata', {}) active = current_user.view_settings.get('metadata', {})
if query: if query:
static_cover = url_for('static', filename='generic_cover.jpg') generic_cover = ""
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: 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)} meta = {executor.submit(c.search, query, generic_cover): c for c in cl if active.get(c.__id__, True)}
for future in concurrent.futures.as_completed(meta): for future in concurrent.futures.as_completed(meta):
data.extend(future.result()) data.extend(future.result())
return Response(json.dumps(data), mimetype='application/json') return Response(json.dumps(data), mimetype='application/json')

View File

@ -21,11 +21,8 @@ import sys
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__ from jsonschema import validate, exceptions, __version__
from datetime import datetime from datetime import datetime
try:
# pylint: disable=unused-import from urllib.parse import unquote
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from flask import json from flask import json
from .. import logger from .. import logger

View File

@ -56,7 +56,7 @@ def check_shelf_view_permissions(cur_shelf):
return True return True
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>") @shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
@login_required @login_required
def add_to_shelf(shelf_id, book_id): def add_to_shelf(shelf_id, book_id):
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest' xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
@ -112,7 +112,7 @@ def add_to_shelf(shelf_id, book_id):
return "", 204 return "", 204
@shelf.route("/shelf/massadd/<int:shelf_id>") @shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
@login_required @login_required
def search_to_shelf(shelf_id): def search_to_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
@ -164,7 +164,7 @@ def search_to_shelf(shelf_id):
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>") @shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
@login_required @login_required
def remove_from_shelf(shelf_id, book_id): def remove_from_shelf(shelf_id, book_id):
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest' xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
@ -248,12 +248,17 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on": if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
flash(_(u"Sorry you are not allowed to create a public shelf"), category="error") flash(_(u"Sorry you are not allowed to create a public shelf"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
shelf.is_public = 1 if to_save.get("is_public") else 0 is_public = 1 if to_save.get("is_public") == "on" else 0
if config.config_kobo_sync: if config.config_kobo_sync:
shelf.kobo_sync = True if to_save.get("kobo_sync") else False shelf.kobo_sync = True if to_save.get("kobo_sync") else False
if shelf.kobo_sync:
ub.session.query(ub.ShelfArchive).filter(ub.ShelfArchive.user_id == current_user.id).filter(
ub.ShelfArchive.uuid == shelf.uuid).delete()
ub.session_commit()
shelf_title = to_save.get("title", "") shelf_title = to_save.get("title", "")
if check_shelf_is_unique(shelf, shelf_title, shelf_id): if check_shelf_is_unique(shelf, shelf_title, is_public, shelf_id):
shelf.name = shelf_title shelf.name = shelf_title
shelf.is_public = is_public
if not shelf_id: if not shelf_id:
shelf.user_id = int(current_user.id) shelf.user_id = int(current_user.id)
ub.session.add(shelf) ub.session.add(shelf)
@ -284,12 +289,12 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
sync_only_selected_shelves=sync_only_selected_shelves) sync_only_selected_shelves=sync_only_selected_shelves)
def check_shelf_is_unique(shelf, title, shelf_id=False): def check_shelf_is_unique(shelf, title, is_public, shelf_id=False):
if shelf_id: if shelf_id:
ident = ub.Shelf.id != shelf_id ident = ub.Shelf.id != shelf_id
else: else:
ident = true() ident = true()
if shelf.is_public == 1: if is_public == 1:
is_shelf_name_unique = ub.session.query(ub.Shelf) \ is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \ .filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \
.filter(ident) \ .filter(ident) \
@ -323,12 +328,13 @@ def delete_shelf_helper(cur_shelf):
ub.session_commit("successfully deleted Shelf {}".format(cur_shelf.name)) ub.session_commit("successfully deleted Shelf {}".format(cur_shelf.name))
@shelf.route("/shelf/delete/<int:shelf_id>") @shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
@login_required @login_required
def delete_shelf(shelf_id): def delete_shelf(shelf_id):
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
try: try:
delete_shelf_helper(cur_shelf) delete_shelf_helper(cur_shelf)
flash(_("Shelf successfully deleted"), category="success")
except InvalidRequestError: except InvalidRequestError:
ub.session.rollback() ub.session.rollback()
log.error("Settings DB is not Writeable") log.error("Settings DB is not Writeable")

View File

@ -61,8 +61,11 @@ $("#archived_cb").on("change", function() {
$("#shelf-actions").on("click", "[data-shelf-action]", function (e) { $("#shelf-actions").on("click", "[data-shelf-action]", function (e) {
e.preventDefault(); e.preventDefault();
$.ajax({
$.get(this.href) url: this.href,
method:"post",
data: {csrf_token:$("input[name='csrf_token']").val()},
})
.done(function() { .done(function() {
var $this = $(this); var $this = $(this);
switch ($this.data("shelf-action")) { switch ($this.data("shelf-action")) {

View File

@ -40,7 +40,7 @@ $(function () {
$("#book_title").val(book.title); $("#book_title").val(book.title);
$("#tags").val(uniqueTags.join(", ")); $("#tags").val(uniqueTags.join(", "));
$("#rating").data("rating").setValue(Math.round(book.rating)); $("#rating").data("rating").setValue(Math.round(book.rating));
if(book.cover !== null){ if(book.cover && $("#cover_url").length){
$(".cover img").attr("src", book.cover); $(".cover img").attr("src", book.cover);
$("#cover_url").val(book.cover); $("#cover_url").val(book.cover);
} }
@ -128,9 +128,7 @@ $(function () {
e.preventDefault(); e.preventDefault();
keyword = $("#keyword").val(); keyword = $("#keyword").val();
$('.pill').each(function(){ $('.pill').each(function(){
// console.log($(this).data('control'));
$(this).data("initial", $(this).prop('checked')); $(this).data("initial", $(this).prop('checked'));
// console.log($(this).data('initial'));
}); });
doSearch(keyword); doSearch(keyword);
}); });

View File

@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.ko={days:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],daysShort:["일","월","화","수","목","금","토"],daysMin:["일","월","화","수","목","금","토"],months:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthsShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],today:"오늘",clear:"삭제",format:"yyyy-mm-dd",titleFormat:"yyyy년mm월",weekStart:0}}(jQuery);

View File

@ -0,0 +1,261 @@
tinymce.addI18n('ko_KR',{
"Redo": "\ub2e4\uc2dc\uc2e4\ud589",
"Undo": "\uc2e4\ud589\ucde8\uc18c",
"Cut": "\uc798\ub77c\ub0b4\uae30",
"Copy": "\ubcf5\uc0ac\ud558\uae30",
"Paste": "\ubd99\uc5ec\ub123\uae30",
"Select all": "\uc804\uccb4\uc120\ud0dd",
"New document": "\uc0c8 \ubb38\uc11c",
"Ok": "\ud655\uc778",
"Cancel": "\ucde8\uc18c",
"Visual aids": "\uc2dc\uac01\uad50\uc7ac",
"Bold": "\uad75\uac8c",
"Italic": "\uae30\uc6b8\uc784\uaf34",
"Underline": "\ubc11\uc904",
"Strikethrough": "\ucde8\uc18c\uc120",
"Superscript": "\uc717\ucca8\uc790",
"Subscript": "\uc544\ub798\ucca8\uc790",
"Clear formatting": "\ud3ec\ub9f7\ucd08\uae30\ud654",
"Align left": "\uc67c\ucabd\uc815\ub82c",
"Align center": "\uac00\uc6b4\ub370\uc815\ub82c",
"Align right": "\uc624\ub978\ucabd\uc815\ub82c",
"Justify": "\uc591\ucabd\uc815\ub82c",
"Bullet list": "\uc810\ub9ac\uc2a4\ud2b8",
"Numbered list": "\uc22b\uc790\ub9ac\uc2a4\ud2b8",
"Decrease indent": "\ub0b4\uc5b4\uc4f0\uae30",
"Increase indent": "\ub4e4\uc5ec\uc4f0\uae30",
"Close": "\ub2eb\uae30",
"Formats": "\ud3ec\ub9f7",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\ube0c\ub77c\uc6b0\uc838\uac00 \ud074\ub9bd\ubcf4\ub4dc \uc811\uadfc\uc744 \ud5c8\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. Ctrl+X\/C\/V \ud0a4\ub97c \uc774\uc6a9\ud574 \uc8fc\uc138\uc694.",
"Headers": "\uc2a4\ud0c0\uc77c",
"Header 1": "\uc81c\ubaa9 1",
"Header 2": "\uc81c\ubaa9 2",
"Header 3": "\uc81c\ubaa9 3",
"Header 4": "\uc81c\ubaa9 4",
"Header 5": "\uc81c\ubaa9 5",
"Header 6": "\uc81c\ubaa9 6",
"Headings": "\uc81c\ubaa9",
"Heading 1": "\uc81c\ubaa9 1",
"Heading 2": "\uc81c\ubaa9 2",
"Heading 3": "\uc81c\ubaa9 3",
"Heading 4": "\uc81c\ubaa9 4",
"Heading 5": "\uc81c\ubaa9 5",
"Heading 6": "\uc81c\ubaa9 6",
"Preformatted": "Preformatted",
"Div": "\uad6c\ubd84",
"Pre": "Pre",
"Code": "\ucf54\ub4dc",
"Paragraph": "\ub2e8\ub77d",
"Blockquote": "\uad6c\ud68d",
"Inline": "\ub77c\uc778 \uc124\uc815",
"Blocks": "\ube14\ub85d \uc124\uc815",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\uc2a4\ud0c0\uc77c\ubcf5\uc0ac \ub044\uae30. \uc774 \uc635\uc158\uc744 \ub044\uae30 \uc804\uc5d0\ub294 \ubcf5\uc0ac \uc2dc, \uc2a4\ud0c0\uc77c\uc774 \ubcf5\uc0ac\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
"Font Family": "\uae00\uaf34",
"Font Sizes": "\ud3f0\ud2b8 \uc0ac\uc774\uc988",
"Class": "\ud074\ub798\uc2a4",
"Browse for an image": "\uc774\ubbf8\uc9c0 \ucc3e\uae30",
"OR": "\ud639\uc740",
"Drop an image here": "\uc774\ubbf8\uc9c0 \ub4dc\ub86d",
"Upload": "\uc5c5\ub85c\ub4dc",
"Block": "\ube14\ub85d",
"Align": "\uc815\ub82c",
"Default": "\uae30\ubcf8",
"Circle": "\uc6d0",
"Disc": "\uc6d0\ubc18",
"Square": "\uc0ac\uac01",
"Lower Alpha": "\uc54c\ud30c\ubcb3 \uc18c\ubb38\uc790",
"Lower Greek": "\uadf8\ub9ac\uc2a4\uc5b4 \uc18c\ubb38\uc790",
"Lower Roman": "\ub85c\ub9c8\uc790 \uc18c\ubb38\uc790",
"Upper Alpha": "\uc54c\ud30c\ubcb3 \uc18c\ubb38\uc790",
"Upper Roman": "\ub85c\ub9c8\uc790 \ub300\ubb38\uc790",
"Anchor": "\uc575\ucee4",
"Name": "\uc774\ub984",
"Id": "\uc544\uc774\ub514",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\uc544\uc774\ub514\ub294 \ubb38\uc790, \uc22b\uc790, \ub300\uc2dc, \uc810, \ucf5c\ub860 \ub610\ub294 \ubc11\uc904\ub85c \uc2dc\uc791\ud574\uc57c\ud569\ub2c8\ub2e4.",
"You have unsaved changes are you sure you want to navigate away?": "\uc800\uc7a5\ud558\uc9c0 \uc54a\uc740 \uc815\ubcf4\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uc774 \ud398\uc774\uc9c0\ub97c \ubc97\uc5b4\ub098\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"Restore last draft": "\ub9c8\uc9c0\ub9c9 \ucd08\uc548 \ubcf5\uc6d0",
"Special character": "\ud2b9\uc218\ubb38\uc790",
"Source code": "\uc18c\uc2a4\ucf54\ub4dc",
"Insert\/Edit code sample": "\ucf54\ub4dc\uc0d8\ud50c \uc0bd\uc785\/\ud3b8\uc9d1",
"Language": "\uc5b8\uc5b4",
"Code sample": "\ucf54\ub4dc\uc0d8\ud50c",
"Color": "\uc0c9\uc0c1",
"R": "R",
"G": "G",
"B": "B",
"Left to right": "\uc67c\ucabd\uc5d0\uc11c \uc624\ub978\ucabd",
"Right to left": "\uc624\ub978\ucabd\uc5d0\uc11c \uc67c\ucabd",
"Emoticons": "\uc774\ubaa8\ud2f0\ucf58",
"Document properties": "\ubb38\uc11c \uc18d\uc131",
"Title": "\uc81c\ubaa9",
"Keywords": "\ud0a4\uc6cc\ub4dc",
"Description": "\uc124\uba85",
"Robots": "\ub85c\ubd07",
"Author": "\uc800\uc790",
"Encoding": "\uc778\ucf54\ub529",
"Fullscreen": "\uc804\uccb4\ud654\uba74",
"Action": "\ub3d9\uc791",
"Shortcut": "\ub2e8\ucd95\ud0a4",
"Help": "\ub3c4\uc6c0\ub9d0",
"Address": "\uc8fc\uc18c",
"Focus to menubar": "\uba54\ub274\uc5d0 \ud3ec\ucee4\uc2a4",
"Focus to toolbar": "\ud234\ubc14\uc5d0 \ud3ec\ucee4\uc2a4",
"Focus to element path": "element path\uc5d0 \ud3ec\ucee4\uc2a4",
"Focus to contextual toolbar": "\ucf04\ud14d\uc2a4\ud2b8 \ud234\ubc14\uc5d0 \ud3ec\ucee4\uc2a4",
"Insert link (if link plugin activated)": "\ub9c1\ud06c \uc0bd\uc785 (link \ud50c\ub7ec\uadf8\uc778\uc774 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c)",
"Save (if save plugin activated)": "\uc800\uc7a5 (save \ud50c\ub7ec\uadf8\uc778\uc774 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c)",
"Find (if searchreplace plugin activated)": "\ucc3e\uae30(searchreplace \ud50c\ub7ec\uadf8\uc778\uc774 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c)",
"Plugins installed ({0}):": "\uc124\uce58\ub41c \ud50c\ub7ec\uadf8\uc778 ({0}):",
"Premium plugins:": "\uace0\uae09 \ud50c\ub7ec\uadf8\uc778",
"Learn more...": "\uc880 \ub354 \uc0b4\ud3b4\ubcf4\uae30",
"You are using {0}": "{0}\ub97c \uc0ac\uc6a9\uc911",
"Plugins": "\ud50c\ub7ec\uadf8\uc778",
"Handy Shortcuts": "\ub2e8\ucd95\ud0a4",
"Horizontal line": "\uac00\ub85c",
"Insert\/edit image": "\uc774\ubbf8\uc9c0 \uc0bd\uc785\/\uc218\uc815",
"Image description": "\uc774\ubbf8\uc9c0 \uc124\uba85",
"Source": "\uc18c\uc2a4",
"Dimensions": "\ud06c\uae30",
"Constrain proportions": "\uc791\uc5c5 \uc81c\ud55c",
"General": "\uc77c\ubc18",
"Advanced": "\uace0\uae09",
"Style": "\uc2a4\ud0c0\uc77c",
"Vertical space": "\uc218\uc9c1 \uacf5\ubc31",
"Horizontal space": "\uc218\ud3c9 \uacf5\ubc31",
"Border": "\ud14c\ub450\ub9ac",
"Insert image": "\uc774\ubbf8\uc9c0 \uc0bd\uc785",
"Image": "\uc774\ubbf8\uc9c0",
"Image list": "\uc774\ubbf8\uc9c0 \ubaa9\ub85d",
"Rotate counterclockwise": "\uc2dc\uacc4\ubc18\ub300\ubc29\ud5a5\uc73c\ub85c \ud68c\uc804",
"Rotate clockwise": "\uc2dc\uacc4\ubc29\ud5a5\uc73c\ub85c \ud68c\uc804",
"Flip vertically": "\uc218\uc9c1 \ub4a4\uc9d1\uae30",
"Flip horizontally": "\uc218\ud3c9 \ub4a4\uc9d1\uae30",
"Edit image": "\uc774\ubbf8\uc9c0 \ud3b8\uc9d1",
"Image options": "\uc774\ubbf8\uc9c0 \uc635\uc158",
"Zoom in": "\ud655\ub300",
"Zoom out": "\ucd95\uc18c",
"Crop": "\uc790\ub974\uae30",
"Resize": "\ud06c\uae30 \uc870\uc808",
"Orientation": "\ubc29\ud5a5",
"Brightness": "\ubc1d\uae30",
"Sharpen": "\uc120\uba85\ud558\uac8c",
"Contrast": "\ub300\ube44",
"Color levels": "\uc0c9\uc0c1\ub808\ubca8",
"Gamma": "\uac10\ub9c8",
"Invert": "\ubc18\uc804",
"Apply": "\uc801\uc6a9",
"Back": "\ub4a4\ub85c",
"Insert date\/time": "\ub0a0\uc9dc\/\uc2dc\uac04\uc0bd\uc785",
"Date\/time": "\ub0a0\uc9dc\/\uc2dc\uac04",
"Insert link": "\ub9c1\ud06c \uc0bd\uc785 ",
"Insert\/edit link": "\ub9c1\ud06c \uc0bd\uc785\/\uc218\uc815",
"Text to display": "\ubcf8\ubb38",
"Url": "\uc8fc\uc18c",
"Target": "\ub300\uc0c1",
"None": "\uc5c6\uc74c",
"New window": "\uc0c8\ucc3d",
"Remove link": "\ub9c1\ud06c\uc0ad\uc81c",
"Anchors": "\ucc45\uac08\ud53c",
"Link": "\ub9c1\ud06c",
"Paste or type a link": "\ub9c1\ud06c\ub97c \ubd99\uc5ec\ub123\uac70\ub098 \uc785\ub825\ud558\uc138\uc694",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\ud604\uc7ac E-mail\uc8fc\uc18c\ub97c \uc785\ub825\ud558\uc168\uc2b5\ub2c8\ub2e4. E-mail \uc8fc\uc18c\uc5d0 \ub9c1\ud06c\ub97c \uac78\uae4c\uc694?",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\ud604\uc7ac \uc6f9\uc0ac\uc774\ud2b8 \uc8fc\uc18c\ub97c \uc785\ub825\ud558\uc168\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \uc8fc\uc18c\uc5d0 \ub9c1\ud06c\ub97c \uac78\uae4c\uc694?",
"Link list": "\ub9c1\ud06c \ub9ac\uc2a4\ud2b8",
"Insert video": "\ube44\ub514\uc624 \uc0bd\uc785",
"Insert\/edit video": "\ube44\ub514\uc624 \uc0bd\uc785\/\uc218\uc815",
"Insert\/edit media": "\ubbf8\ub514\uc5b4 \uc0bd\uc785\/\uc218\uc815",
"Alternative source": "\ub300\uccb4 \uc18c\uc2a4",
"Poster": "\ud3ec\uc2a4\ud130",
"Paste your embed code below:": "\uc544\ub798\uc5d0 \ucf54\ub4dc\ub97c \ubd99\uc5ec\ub123\uc73c\uc138\uc694:",
"Embed": "\uc0bd\uc785",
"Media": "\ubbf8\ub514\uc5b4",
"Nonbreaking space": "\ub744\uc5b4\uc4f0\uae30",
"Page break": "\ud398\uc774\uc9c0 \uad6c\ubd84\uc790",
"Paste as text": "\ud14d\uc2a4\ud2b8\ub85c \ubd99\uc5ec\ub123\uae30",
"Preview": "\ubbf8\ub9ac\ubcf4\uae30",
"Print": "\ucd9c\ub825",
"Save": "\uc800\uc7a5",
"Find": "\ucc3e\uae30",
"Replace with": "\uad50\uccb4",
"Replace": "\uad50\uccb4",
"Replace all": "\uc804\uccb4 \uad50\uccb4",
"Prev": "\uc774\uc804",
"Next": "\ub2e4\uc74c",
"Find and replace": "\ucc3e\uc544\uc11c \uad50\uccb4",
"Could not find the specified string.": "\ubb38\uc790\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"Match case": "\ub300\uc18c\ubb38\uc790 \uc77c\uce58",
"Whole words": "\uc804\uccb4 \ub2e8\uc5b4",
"Spellcheck": "\ubb38\ubc95\uccb4\ud06c",
"Ignore": "\ubb34\uc2dc",
"Ignore all": "\uc804\uccb4\ubb34\uc2dc",
"Finish": "\uc644\ub8cc",
"Add to Dictionary": "\uc0ac\uc804\uc5d0 \ucd94\uac00",
"Insert table": "\ud14c\uc774\ube14 \uc0bd\uc785",
"Table properties": "\ud14c\uc774\ube14 \uc18d\uc131",
"Delete table": "\ud14c\uc774\ube14 \uc0ad\uc81c",
"Cell": "\uc140",
"Row": "\uc5f4",
"Column": "\ud589",
"Cell properties": "\uc140 \uc18d",
"Merge cells": "\uc140 \ud569\uce58\uae30",
"Split cell": "\uc140 \ub098\ub204\uae30",
"Insert row before": "\uc774\uc804\uc5d0 \ud589 \uc0bd\uc785",
"Insert row after": "\ub2e4\uc74c\uc5d0 \ud589 \uc0bd\uc785",
"Delete row": "\ud589 \uc9c0\uc6b0\uae30",
"Row properties": "\ud589 \uc18d\uc131",
"Cut row": "\ud589 \uc798\ub77c\ub0b4\uae30",
"Copy row": "\ud589 \ubcf5\uc0ac",
"Paste row before": "\uc774\uc804\uc5d0 \ud589 \ubd99\uc5ec\ub123\uae30",
"Paste row after": "\ub2e4\uc74c\uc5d0 \ud589 \ubd99\uc5ec\ub123\uae30",
"Insert column before": "\uc774\uc804\uc5d0 \ud589 \uc0bd\uc785",
"Insert column after": "\ub2e4\uc74c\uc5d0 \uc5f4 \uc0bd\uc785",
"Delete column": "\uc5f4 \uc9c0\uc6b0\uae30",
"Cols": "\uc5f4",
"Rows": "\ud589",
"Width": "\ub113\uc774",
"Height": "\ub192\uc774",
"Cell spacing": "\uc140 \uac04\uaca9",
"Cell padding": "\uc140 \uc548\ucabd \uc5ec\ubc31",
"Caption": "\ucea1\uc158",
"Left": "\uc67c\ucabd",
"Center": "\uac00\uc6b4\ub370",
"Right": "\uc624\ub978\ucabd",
"Cell type": "\uc140 \ud0c0\uc785",
"Scope": "\ubc94\uc704",
"Alignment": "\uc815\ub82c",
"H Align": "\uac00\ub85c \uc815\ub82c",
"V Align": "\uc138\ub85c \uc815\ub82c",
"Top": "\uc0c1\ub2e8",
"Middle": "\uc911\uac04",
"Bottom": "\ud558\ub2e8",
"Header cell": "\ud5e4\ub354 \uc140",
"Row group": "\ud589 \uadf8\ub8f9",
"Column group": "\uc5f4 \uadf8\ub8f9",
"Row type": "\ud589 \ud0c0\uc785",
"Header": "\ud5e4\ub354",
"Body": "\ubc14\ub514",
"Footer": "\ud478\ud130",
"Border color": "\ud14c\ub450\ub9ac \uc0c9",
"Insert template": "\ud15c\ud50c\ub9bf \uc0bd\uc785",
"Templates": "\ud15c\ud50c\ub9bf",
"Template": "\ud15c\ud50c\ub9bf",
"Text color": "\ubb38\uc790 \uc0c9\uae54",
"Background color": "\ubc30\uacbd\uc0c9",
"Custom...": "\uc9c1\uc811 \uc0c9\uae54 \uc9c0\uc815\ud558\uae30",
"Custom color": "\uc9c1\uc811 \uc9c0\uc815\ud55c \uc0c9\uae54",
"No color": "\uc0c9\uc0c1 \uc5c6\uc74c",
"Table of Contents": "\ubaa9\ucc28",
"Show blocks": "\ube14\ub7ed \ubcf4\uc5ec\uc8fc\uae30",
"Show invisible characters": "\uc548\ubcf4\uc774\ub294 \ubb38\uc790 \ubcf4\uc774\uae30",
"Words: {0}": "\ub2e8\uc5b4: {0}",
"{0} words": "{0} \ub2e8\uc5b4",
"File": "\ud30c\uc77c",
"Edit": "\uc218\uc815",
"Insert": "\uc0bd\uc785",
"View": "\ubcf4\uae30",
"Format": "\ud3ec\ub9f7",
"Table": "\ud14c\uc774\ube14",
"Tools": "\ub3c4\uad6c",
"Powered by {0}": "Powered by {0}",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\uc11c\uc2dd \uc788\ub294 \ud14d\uc2a4\ud2b8 \ud3b8\uc9d1\uae30 \uc785\ub2c8\ub2e4. ALT-F9\ub97c \ub204\ub974\uba74 \uba54\ub274, ALT-F10\ub97c \ub204\ub974\uba74 \ud234\ubc14, ALT-0\uc744 \ub204\ub974\uba74 \ub3c4\uc6c0\ub9d0\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4."
});

71
cps/static/js/main.js Normal file → Executable file
View File

@ -20,6 +20,20 @@ function getPath() {
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path
} }
function postButton(event, action){
event.preventDefault();
var newForm = jQuery('<form>', {
"action": action,
'target': "_top",
'method': "post"
}).append(jQuery('<input>', {
'name': 'csrf_token',
'value': $("input[name=\'csrf_token\']").val(),
'type': 'hidden'
})).appendTo('body');
newForm.submit();
}
function elementSorter(a, b) { function elementSorter(a, b) {
a = +a.slice(0, -2); a = +a.slice(0, -2);
b = +b.slice(0, -2); b = +b.slice(0, -2);
@ -71,6 +85,22 @@ $(document).on("change", "select[data-controlall]", function() {
} }
}); });
/*$(document).on("click", "#sendbtn", function (event) {
postButton(event, $(this).data('action'));
});
$(document).on("click", ".sendbutton", function (event) {
// $(".sendbutton").on("click", "body", function(event) {
postButton(event, $(this).data('action'));
});*/
$(document).on("click", ".postAction", function (event) {
// $(".sendbutton").on("click", "body", function(event) {
postButton(event, $(this).data('action'));
});
// Syntax has to be bind not on, otherwise problems with firefox // Syntax has to be bind not on, otherwise problems with firefox
$(".container-fluid").bind("dragenter dragover", function () { $(".container-fluid").bind("dragenter dragover", function () {
if($("#btn-upload").length && !$('body').hasClass('shelforder')) { if($("#btn-upload").length && !$('body').hasClass('shelforder')) {
@ -168,18 +198,18 @@ function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
$confirm.modal('show'); $confirm.modal('show');
} }
$("#delete_confirm").click(function() { $("#delete_confirm").click(function(event) {
//get data-id attribute of the clicked element //get data-id attribute of the clicked element
var deleteId = $(this).data("delete-id"); var deleteId = $(this).data("delete-id");
var bookFormat = $(this).data("delete-format"); var bookFormat = $(this).data("delete-format");
var ajaxResponse = $(this).data("ajax"); var ajaxResponse = $(this).data("ajax");
if (bookFormat) { if (bookFormat) {
window.location.href = getPath() + "/delete/" + deleteId + "/" + bookFormat; postButton(event, getPath() + "/delete/" + deleteId + "/" + bookFormat);
} else { } else {
if (ajaxResponse) { if (ajaxResponse) {
path = getPath() + "/ajax/delete/" + deleteId; path = getPath() + "/ajax/delete/" + deleteId;
$.ajax({ $.ajax({
method:"get", method:"post",
url: path, url: path,
timeout: 900, timeout: 900,
success:function(data) { success:function(data) {
@ -198,8 +228,7 @@ $("#delete_confirm").click(function() {
} }
}); });
} else { } else {
window.location.href = getPath() + "/delete/" + deleteId; postButton(event, getPath() + "/delete/" + deleteId);
} }
} }
@ -376,9 +405,11 @@ $(function() {
$("#restart").click(function() { $("#restart").click(function() {
$.ajax({ $.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../shutdown", url: getPath() + "/shutdown",
data: {"parameter":0}, data: JSON.stringify({"parameter":0}),
success: function success() { success: function success() {
$("#spinner").show(); $("#spinner").show();
setTimeout(restartTimer, 3000); setTimeout(restartTimer, 3000);
@ -387,9 +418,11 @@ $(function() {
}); });
$("#shutdown").click(function() { $("#shutdown").click(function() {
$.ajax({ $.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../shutdown", url: getPath() + "/shutdown",
data: {"parameter":1}, data: JSON.stringify({"parameter":1}),
success: function success(data) { success: function success(data) {
return alert(data.text); return alert(data.text);
} }
@ -447,9 +480,11 @@ $(function() {
$("#DialogContent").html(""); $("#DialogContent").html("");
$("#spinner2").show(); $("#spinner2").show();
$.ajax({ $.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: getPath() + "/shutdown", url: getPath() + "/shutdown",
data: {"parameter":2}, data: JSON.stringify({"parameter":2}),
success: function success(data) { success: function success(data) {
$("#spinner2").hide(); $("#spinner2").hide();
$("#DialogContent").html(data.text); $("#DialogContent").html(data.text);
@ -500,6 +535,7 @@ $(function() {
$("#modal_kobo_token") $("#modal_kobo_token")
.on("show.bs.modal", function(e) { .on("show.bs.modal", function(e) {
$(e.relatedTarget).one('focus', function(e){$(this).blur();});
var $modalBody = $(this).find(".modal-body"); var $modalBody = $(this).find(".modal-body");
// Prevent static assets from loading multiple times // Prevent static assets from loading multiple times
@ -527,7 +563,7 @@ $(function() {
$(this).data('value'), $(this).data('value'),
function (value) { function (value) {
$.ajax({ $.ajax({
method: "get", method: "post",
url: getPath() + "/kobo_auth/deleteauthtoken/" + value, url: getPath() + "/kobo_auth/deleteauthtoken/" + value,
}); });
$("#config_delete_kobo_token").hide(); $("#config_delete_kobo_token").hide();
@ -574,7 +610,7 @@ $(function() {
function(value){ function(value){
path = getPath() + "/ajax/fullsync" path = getPath() + "/ajax/fullsync"
$.ajax({ $.ajax({
method:"get", method:"post",
url: path, url: path,
timeout: 900, timeout: 900,
success:function(data) { success:function(data) {
@ -638,7 +674,7 @@ $(function() {
else { else {
$("#InvalidDialog").modal('show'); $("#InvalidDialog").modal('show');
} }
} else { } else {
changeDbSettings(); changeDbSettings();
} }
} }
@ -679,13 +715,14 @@ $(function() {
}); });
}); });
$("#delete_shelf").click(function() { $("#delete_shelf").click(function(event) {
confirmDialog( confirmDialog(
$(this).attr('id'), $(this).attr('id'),
"GeneralDeleteModal", "GeneralDeleteModal",
$(this).data('value'), $(this).data('value'),
function(value){ function(value){
window.location.href = window.location.pathname + "/../../shelf/delete/" + value postButton(event, $("#delete_shelf").data("action"));
// $("#delete_shelf").closest("form").submit()
} }
); );
@ -734,7 +771,8 @@ $(function() {
$("#DialogContent").html(""); $("#DialogContent").html("");
$("#spinner2").show(); $("#spinner2").show();
$.ajax({ $.ajax({
method:"get", method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: getPath() + "/import_ldap_users", url: getPath() + "/import_ldap_users",
success: function success(data) { success: function success(data) {
@ -768,4 +806,3 @@ $(function() {
}); });
}); });
}); });

View File

@ -47,15 +47,15 @@ $(function() {
var rows = rowsAfter; var rows = rowsAfter;
if (e.type === "uncheck-all") { if (e.type === "uncheck-all") {
rows = rowsBefore; selections = [];
} else {
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id;
});
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids);
} }
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id;
});
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids);
if (selections.length >= 2) { if (selections.length >= 2) {
$("#merge_books").removeClass("disabled"); $("#merge_books").removeClass("disabled");
$("#merge_books").attr("aria-disabled", false); $("#merge_books").attr("aria-disabled", false);
@ -540,14 +540,14 @@ $(function() {
var rows = rowsAfter; var rows = rowsAfter;
if (e.type === "uncheck-all") { if (e.type === "uncheck-all") {
rows = rowsBefore; selections = [];
} else {
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id;
});
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids);
} }
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id;
});
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids);
handle_header_buttons(); handle_header_buttons();
}); });
}); });

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os import os
import re import re
@ -31,7 +30,8 @@ from cps import db
from cps import logger, config from cps import logger, config
from cps.subproc_wrapper import process_open from cps.subproc_wrapper import process_open
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask import url_for from cps.kobo_sync_status import remove_synced_book
from cps.ub import ini
from cps.tasks.mail import TaskEmail from cps.tasks.mail import TaskEmail
from cps import gdriveutils from cps import gdriveutils
@ -147,6 +147,10 @@ class TaskConvert(CalibreTask):
try: try:
local_db.session.merge(new_format) local_db.session.merge(new_format)
local_db.session.commit() local_db.session.commit()
if self.settings['new_book_format'].upper() in ['KEPUB', 'EPUB', 'EPUB3']:
ub_session = ini()
remove_synced_book(book_id, True, ub_session)
ub_session.close()
except SQLAlchemyError as e: except SQLAlchemyError as e:
local_db.session.rollback() local_db.session.rollback()
log.error("Database error: %s", e) log.error("Database error: %s", e)

View File

@ -158,10 +158,10 @@ class TaskEmail(CalibreTask):
else: else:
self.send_gmail_email(msg) self.send_gmail_email(msg)
except MemoryError as e: except MemoryError as e:
log.debug_or_exception(e) log.debug_or_exception(e, stacklevel=3)
self._handleError(u'MemoryError sending e-mail: {}'.format(str(e))) self._handleError(u'MemoryError sending e-mail: {}'.format(str(e)))
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e: except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
log.debug_or_exception(e) log.debug_or_exception(e, stacklevel=3)
if hasattr(e, "smtp_error"): if hasattr(e, "smtp_error"):
text = e.smtp_error.decode('utf-8').replace("\n", '. ') text = e.smtp_error.decode('utf-8').replace("\n", '. ')
elif hasattr(e, "message"): elif hasattr(e, "message"):
@ -171,11 +171,11 @@ class TaskEmail(CalibreTask):
else: else:
text = '' text = ''
self._handleError(u'Smtplib Error sending e-mail: {}'.format(text)) self._handleError(u'Smtplib Error sending e-mail: {}'.format(text))
except socket.error as e: except (socket.error) as e:
log.debug_or_exception(e) log.debug_or_exception(e, stacklevel=3)
self._handleError(u'Socket Error sending e-mail: {}'.format(e.strerror)) self._handleError(u'Socket Error sending e-mail: {}'.format(e.strerror))
except Exception as ex: except Exception as ex:
log.debug_or_exception(ex) log.debug_or_exception(ex, stacklevel=3)
self._handleError(u'Error sending e-mail: {}'.format(ex)) self._handleError(u'Error sending e-mail: {}'.format(ex))
def send_standard_email(self, msg): def send_standard_email(self, msg):
@ -248,7 +248,7 @@ class TaskEmail(CalibreTask):
data = file_.read() data = file_.read()
file_.close() file_.close()
except IOError as e: except IOError as e:
log.debug_or_exception(e) log.debug_or_exception(e, stacklevel=3)
log.error(u'The requested file could not be read. Maybe wrong permissions?') log.error(u'The requested file could not be read. Maybe wrong permissions?')
return None return None
# Set mimetype # Set mimetype

View File

@ -188,9 +188,8 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if feature_support['updater'] %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if feature_support['updater'] %}
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div> <div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
<div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div> <div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div>
<div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div> <div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div>

View File

@ -5,11 +5,11 @@
{% if author is not none %} {% if author is not none %}
<section class="author-bio"> <section class="author-bio">
{%if author.image_url is not none %} {%if author.image_url is not none %}
<img title="{{author.name|safe}}" src="{{author.image_url}}" alt="{{author.name|safe}}" class="author-photo pull-left"> <img title="{{author.name}}" src="{{author.image_url}}" alt="{{author.name}}" class="author-photo pull-left">
{% endif %} {% endif %}
{%if author.about is not none %} {%if author.about is not none %}
<p>{{author.about|safe}}</p> <p>{{author.about}}</p>
{% endif %} {% endif %}
- {{_("via")}} <a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">Goodreads</a> - {{_("via")}} <a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">Goodreads</a>
@ -36,7 +36,7 @@
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book"> <div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<span class="img" title="{{entry.title|safe}}"> <span class="img" title="{{entry.title}}">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" /> <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
@ -98,7 +98,7 @@
{% if other_books and author is not none %} {% if other_books and author is not none %}
<div class="discover"> <div class="discover">
<h3>{{_("More by")}} {{ author.name.replace('|',',')|safe }}</h3> <h3>{{_("More by")}} {{ author.name.replace('|',',') }}</h3>
<div class="row"> <div class="row">
{% for entry in other_books %} {% for entry in other_books %}
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">

View File

@ -226,7 +226,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="metaModalLabel">{{_('Fetch Metadata')}}</h4> <h4 class="modal-title text-center" id="metaModalLabel">{{_('Fetch Metadata')}}</h4>
<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>
@ -247,7 +247,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button> <button id="meta_close" type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
</div> </div>
</div> </div>
</div> </div>
@ -265,17 +265,17 @@
> >
<div class="media-body"> <div class="media-body">
<h4 class="media-heading"> <h4 class="media-heading">
<a href="<%= url %>" target="_blank" rel="noopener"><%= title %></a> <a class="meta_title" href="<%= url %>" target="_blank" rel="noopener"><%= title %></a>
</h4> </h4>
<p>{{_('Author')}}<%= authors.join(" & ") %></p> <p class="meta_author">{{_('Author')}}<%= authors.join(" & ") %></p>
<% if (publisher) { %> <% if (publisher) { %>
<p>{{_('Publisher')}}<%= publisher %></p> <p class="meta_publisher">{{_('Publisher')}}<%= publisher %></p>
<% } %> <% } %>
<% if (description) { %> <% if (description) { %>
<p>{{_('Description')}}: <%= description %></p> <p class="meta_description">{{_('Description')}}: <%= description %></p>
<% } %> <% } %>
<p>{{_('Source')}}: <p>{{_('Source')}}:
<a href="<%= source.url %>" target="_blank" rel="noopener"><%= source.description %></a> <a class="meta_source" href="<%= source.link %>" target="_blank" rel="noopener"><%= source.description %></a>
</p> </p>
</div> </div>
</li> </li>

View File

@ -123,8 +123,8 @@
<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 id="merge_confirm" type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button> <button id="merge_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -38,7 +38,7 @@
{% endif %} {% endif %}
{% if g.user.kindle_mail and entry.kindle_list %} {% if g.user.kindle_mail and entry.kindle_list %}
{% if entry.kindle_list.__len__() == 1 %} {% if entry.kindle_list.__len__() == 1 %}
<a href="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=entry.kindle_list[0]['format'], convert=entry.kindle_list[0]['convert'])}}" id="sendbtn" data-text="{{_('Send to Kindle')}}" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-send"></span> {{entry.kindle_list[0]['text']}}</a> <div id="sendbtn" data-action="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=entry.kindle_list[0]['format'], convert=entry.kindle_list[0]['convert'])}}" data-text="{{_('Send to Kindle')}}" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-send"></span> {{entry.kindle_list[0]['text']}}</div>
{% else %} {% else %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -47,7 +47,7 @@
</button> </button>
<ul class="dropdown-menu" aria-labelledby="send-to-kindle"> <ul class="dropdown-menu" aria-labelledby="send-to-kindle">
{% for format in entry.kindle_list %} {% for format in entry.kindle_list %}
<li><a href="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li> <li><a class="postAction" data-action="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li>
{%endfor%} {%endfor%}
</ul> </ul>
</div> </div>

View File

@ -1,12 +1,15 @@
{% extends "fragment.html" %} {% extends "fragment.html" %}
{% block body %} {% block body %}
<div class="well"> <div class="well">
<p> <p>
{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a> {% if not warning %}
{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}
</p><p>
api_endpoint={{url_for("kobo.TopLevelEndpoint", auth_token=auth_token, _external=True)}}
{% else %}
{{warning}}
</p><p>{{_('Kobo Token:')}} {{ auth_token }}
{% endif %}
</p> </p>
<p>
{% if not warning %}api_endpoint={{kobo_auth_url}}{% else %}{{warning}}{% endif %}</a>
</p>
<p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -183,7 +183,7 @@
</div> </div>
<div class="modal-body">...</div> <div class="modal-body">...</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button> <button type="button" id="details_close" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -17,7 +17,7 @@
<ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf"> <ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
{% for shelf in g.shelves_access %} {% for shelf in g.shelves_access %}
{% if not shelf.is_public or g.user.role_edit_shelfs() %} {% if not shelf.is_public or g.user.role_edit_shelfs() %}
<li><a href="{{ url_for('shelf.search_to_shelf', shelf_id=shelf.id) }}"> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li> <li><a class="postAction" role="button" data-action="{{ url_for('shelf.search_to_shelf', shelf_id=shelf.id) }}"> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
{% endif %} {% endif %}
{%endfor%} {%endfor%}
</ul> </ul>

View File

@ -2,14 +2,16 @@
{% block body %} {% block body %}
<div class="discover"> <div class="discover">
<h2>{{title}}</h2> <h2>{{title}}</h2>
<!--form method="post"--->
{% if g.user.role_download() %} {% if g.user.role_download() %}
<a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a> <a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
{% endif %} {% endif %}
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %} {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="btn btn-danger" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div> <div class="btn btn-danger" data-action="{{url_for('shelf.delete_shelf', shelf_id=shelf.id)}}" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf Properties') }} </a> <a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf Properties') }} </a>
</form>
{% if entries.__len__() %} {% if entries.__len__() %}
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Arrange books manually') }} </a> <a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Arrange books manually') }} </a>
<button id="toggle_order_shelf" type="button" data-alt-text="{{ _('Disable Change order') }}" class="btn btn-primary">{{ _('Enable Change order') }}</button> <button id="toggle_order_shelf" type="button" data-alt-text="{{ _('Disable Change order') }}" class="btn btn-primary">{{ _('Enable Change order') }}</button>
@ -84,22 +86,6 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<!--div id="DeleteShelfDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you sure you want to delete this shelf?')}}</span>
</div>
<div class="modal-body text-center">
<span>{{_('Shelf will be deleted for all users')}}</span>
<p></p>
<a id="confirm" href="{{ url_for('shelf.delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('OK')}}</a>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div-->
{% endblock %} {% endblock %}
{% block modal %} {% block modal %}
{{ delete_confirm_modal() }} {{ delete_confirm_modal() }}

View File

@ -17,7 +17,7 @@
</div> </div>
{% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %} {% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %}
{% if g.user and g.user.role_admin() and not new_user and not profile and ( mail_configured and content.email if content.email != None ) %} {% if g.user and g.user.role_admin() and not new_user and not profile and ( mail_configured and content.email if content.email != None ) %}
<a class="btn btn-default" id="resend_password" href="{{url_for('admin.reset_user_password', user_id = content.id) }}">{{_('Reset user Password')}}</a> <a class="btn btn-default postAction" id="resend_password" role="button" data-action="{{url_for('admin.reset_user_password', user_id = content.id) }}">{{_('Reset user Password')}}</a>
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<label for="password">{{_('Password')}}</label> <label for="password">{{_('Password')}}</label>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -427,8 +427,8 @@ class KoboReadingState(Base):
book_id = Column(Integer) book_id = Column(Integer)
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
priority_timestamp = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) priority_timestamp = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
current_bookmark = relationship("KoboBookmark", uselist=False, backref="kobo_reading_state", cascade="all") current_bookmark = relationship("KoboBookmark", uselist=False, backref="kobo_reading_state", cascade="all, delete")
statistics = relationship("KoboStatistics", uselist=False, backref="kobo_reading_state", cascade="all") statistics = relationship("KoboStatistics", uselist=False, backref="kobo_reading_state", cascade="all, delete")
class KoboBookmark(Base): class KoboBookmark(Base):
@ -779,6 +779,14 @@ def create_admin_user(session):
except Exception: except Exception:
session.rollback() session.rollback()
def ini():
global app_DB_path
engine = create_engine(u'sqlite:///{0}'.format(app_DB_path), echo=False)
Session = scoped_session(sessionmaker())
Session.configure(bind=engine)
return Session()
def init_db(app_db_path): def init_db(app_db_path):
# Open session for database connection # Open session for database connection
@ -836,12 +844,13 @@ def dispose():
except Exception: except Exception:
pass pass
def session_commit(success=None): def session_commit(success=None, sess=None):
s = sess if sess else session
try: try:
session.commit() s.commit()
if success: if success:
log.info(success) log.info(success)
except (exc.OperationalError, exc.InvalidRequestError) as e: except (exc.OperationalError, exc.InvalidRequestError) as e:
session.rollback() s.rollback()
log.debug_or_exception(e) log.debug_or_exception(e)
return "" return ""

View File

@ -1104,7 +1104,8 @@ def get_tasks_status():
return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks") return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks")
@app.route("/reconnect") # method is available without login and not protected by CSRF to make it easy reachable
@app.route("/reconnect", methods=['GET'])
def reconnect(): def reconnect():
calibre_db.reconnect_db(config, ub.app_DB_path) calibre_db.reconnect_db(config, ub.app_DB_path)
return json.dumps({}) return json.dumps({})
@ -1117,7 +1118,7 @@ def reconnect():
def search(): def search():
term = request.args.get("query") term = request.args.get("query")
if term: if term:
return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term)) return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term.strip()))
else: else:
return render_title_template('search.html', return render_title_template('search.html',
searchterm="", searchterm="",
@ -1501,7 +1502,7 @@ def download_link(book_id, book_format, anyname):
return get_download_link(book_id, book_format, client) return get_download_link(book_id, book_format, client)
@web.route('/send/<int:book_id>/<book_format>/<int:convert>') @web.route('/send/<int:book_id>/<book_format>/<int:convert>', methods=["POST"])
@login_required @login_required
@download_required @download_required
def send_to_kindle(book_id, book_format, convert): def send_to_kindle(book_id, book_format, convert):

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
# GDrive Integration # GDrive Integration
google-api-python-client>=1.7.11,<2.36.0
gevent>20.6.0,<22.0.0 gevent>20.6.0,<22.0.0
greenlet>=0.4.17,<1.2.0 greenlet>=0.4.17,<1.2.0
httplib2>=0.9.2,<0.21.0 httplib2>=0.9.2,<0.21.0
@ -8,34 +9,32 @@ pyasn1-modules>=0.0.8,<0.3.0
pyasn1>=0.1.9,<0.5.0 pyasn1>=0.1.9,<0.5.0
PyDrive2>=1.3.1,<1.11.0 PyDrive2>=1.3.1,<1.11.0
PyYAML>=3.12 PyYAML>=3.12
rsa>=3.4.2,<4.8.0 rsa>=3.4.2,<4.9.0
six>=1.10.0,<1.17.0 six>=1.10.0,<1.17.0
# Gdrive and Gmail integration
google-api-python-client>=1.7.11,<2.32.0
# Gmail # Gmail
google-auth-oauthlib>=0.4.3,<0.5.0 google-auth-oauthlib>=0.4.3,<0.5.0
google-api-python-client>=1.7.11,<2.36.0
# goodreads # goodreads
goodreads>=0.3.2,<0.4.0 goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.13.0 python-Levenshtein>=0.12.0,<0.13.0
# ldap login # ldap login
python-ldap>=3.0.0,<3.4.0 python-ldap>=3.0.0,<3.5.0
Flask-SimpleLDAP>=1.4.0,<1.5.0 Flask-SimpleLDAP>=1.4.0,<1.5.0
#oauth # oauth
Flask-Dance>=2.0.0,<5.2.0 Flask-Dance>=2.0.0,<5.2.0
SQLAlchemy-Utils>=0.33.5,<0.38.0 SQLAlchemy-Utils>=0.33.5,<0.39.0
# extracting metadata # metadata extraction
rarfile>=2.7 rarfile>=2.7
scholarly>=1.2.0, <1.5 scholarly>=1.2.0,<1.6
# other # Comics
natsort>=2.2.0,<8.1.0 natsort>=2.2.0,<8.1.0
comicapi>=2.2.0,<2.3.0 comicapi>=2.2.0,<2.3.0
#Kobo integration # Kobo integration
jsonschema>=3.2.0,<4.3.0 jsonschema>=3.2.0,<4.5.0

View File

@ -7,10 +7,11 @@ Flask>=1.0.2,<2.1.0
iso-639>=0.4.5,<0.5.0 iso-639>=0.4.5,<0.5.0
PyPDF3>=1.0.0,<1.0.6 PyPDF3>=1.0.0,<1.0.6
pytz>=2016.10 pytz>=2016.10
requests>=2.11.1,<2.25.0 requests>=2.11.1,<2.28.0
SQLAlchemy>=1.3.0,<1.5.0 SQLAlchemy>=1.3.0,<1.5.0
tornado>=4.1,<6.2 tornado>=4.1,<6.2
Wand>=0.4.4,<0.7.0 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.3.0 unidecode>=0.04.19,<1.4.0
lxml>=3.8.0,<4.7.0 lxml>=3.8.0,<4.8.0
flask-wtf>=0.14.2,<1.1.0 flask-wtf>=0.14.2,<1.1.0
chardet>=3.0.0,<4.1.0

151
setup.cfg
View File

@ -1,93 +1,94 @@
[metadata] [metadata]
name = calibreweb name = calibreweb
url = https://github.com/janeczku/calibre-web url = https://github.com/janeczku/calibre-web
project_urls = project_urls =
Bug Tracker = https://github.com/janeczku/calibre-web/issues Bug Tracker = https://github.com/janeczku/calibre-web/issues
Release Management = https://github.com/janeczku/calibre-web/releases Release Management = https://github.com/janeczku/calibre-web/releases
Documentation = https://github.com/janeczku/calibre-web/wiki Documentation = https://github.com/janeczku/calibre-web/wiki
Source Code = https://github.com/janeczku/calibre-web Source Code = https://github.com/janeczku/calibre-web
description = Web app for browsing, reading and downloading eBooks stored in a Calibre database. description = Web app for browsing, reading and downloading eBooks stored in a Calibre database.
long_description = file: README.md long_description = file: README.md
long_description_content_type= text/markdown long_description_content_type = text/markdown
author = @OzzieIsaacs author = @OzzieIsaacs
author_email = Ozzie.Fernandez.Isaacs@googlemail.com author_email = Ozzie.Fernandez.Isaacs@googlemail.com
maintainer = @OzzieIsaacs maintainer = @OzzieIsaacs
license = GPLv3+ license = GPLv3+
license_file = LICENSE license_file = LICENSE
classifiers = classifiers =
Development Status :: 5 - Production/Stable Development Status :: 5 - Production/Stable
License :: OSI Approved :: GNU Affero General Public License v3 License :: OSI Approved :: GNU Affero General Public License v3
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.10
Operating System :: OS Independent Operating System :: OS Independent
keywords = keywords =
calibre calibre
calibre-web calibre-web
library library
python_requires = >=3.5 python_requires = >=3.5
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =
cps = calibreweb:main cps = calibreweb:main
[options] [options]
include_package_data = True include_package_data = True
install_requires = install_requires =
Babel>=1.3,<3.0 Babel>=1.3,<3.0
Flask-Babel>=0.11.1,<2.1.0 Flask-Babel>=0.11.1,<2.1.0
Flask-Login>=0.3.2,<0.5.1 Flask-Login>=0.3.2,<0.5.1
Flask-Principal>=0.3.2,<0.5.1 Flask-Principal>=0.3.2,<0.5.1
backports_abc>=0.4 backports_abc>=0.4
Flask>=1.0.2,<2.1.0 Flask>=1.0.2,<2.1.0
iso-639>=0.4.5,<0.5.0 iso-639>=0.4.5,<0.5.0
PyPDF3>=1.0.0,<1.0.6 PyPDF3>=1.0.0,<1.0.6
pytz>=2016.10 pytz>=2016.10
requests>=2.11.1,<2.25.0 requests>=2.11.1,<2.28.0
SQLAlchemy>=1.3.0,<1.5.0 SQLAlchemy>=1.3.0,<1.5.0
tornado>=4.1,<6.2 tornado>=4.1,<6.2
Wand>=0.4.4,<0.7.0 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.3.0 unidecode>=0.04.19,<1.4.0
lxml>=3.8.0,<4.7.0 lxml>=3.8.0,<4.8.0
flask-wtf>=0.14.2,<1.1.0 flask-wtf>=0.14.2,<1.1.0
chardet>=3.0.0,<4.1.0
[options.extras_require] [options.extras_require]
gdrive = gdrive =
google-api-python-client>=1.7.11,<2.32.0 google-api-python-client>=1.7.11,<2.36.0
gevent>20.6.0,<22.0.0 gevent>20.6.0,<22.0.0
greenlet>=0.4.17,<1.2.0 greenlet>=0.4.17,<1.2.0
httplib2>=0.9.2,<0.21.0 httplib2>=0.9.2,<0.21.0
oauth2client>=4.0.0,<4.1.4 oauth2client>=4.0.0,<4.1.4
uritemplate>=3.0.0,<4.2.0 uritemplate>=3.0.0,<4.2.0
pyasn1-modules>=0.0.8,<0.3.0 pyasn1-modules>=0.0.8,<0.3.0
pyasn1>=0.1.9,<0.5.0 pyasn1>=0.1.9,<0.5.0
PyDrive2>=1.3.1,<1.11.0 PyDrive2>=1.3.1,<1.11.0
PyYAML>=3.12 PyYAML>=3.12
rsa>=3.4.2,<4.8.0 rsa>=3.4.2,<4.9.0
six>=1.10.0,<1.17.0 six>=1.10.0,<1.17.0
gmail = gmail =
google-auth-oauthlib>=0.4.3,<0.5.0 google-auth-oauthlib>=0.4.3,<0.5.0
google-api-python-client>=1.7.11,<2.32.0 google-api-python-client>=1.7.11,<2.36.0
goodreads = goodreads =
goodreads>=0.3.2,<0.4.0 goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.13.0 python-Levenshtein>=0.12.0,<0.13.0
ldap = ldap =
python-ldap>=3.0.0,<3.4.0 python-ldap>=3.0.0,<3.5.0
Flask-SimpleLDAP>=1.4.0,<1.5.0 Flask-SimpleLDAP>=1.4.0,<1.5.0
oauth = oauth =
Flask-Dance>=2.0.0,<5.2.0 Flask-Dance>=2.0.0,<5.2.0
SQLAlchemy-Utils>=0.33.5,<0.38.0 SQLAlchemy-Utils>=0.33.5,<0.39.0
metadata = metadata =
rarfile>=2.7 rarfile>=2.7
scholarly>=1.2.0,<1.5 scholarly>=1.2.0,<1.6
comics = comics =
natsort>=2.2.0,<8.1.0 natsort>=2.2.0,<8.1.0
comicapi>= 2.2.0,<2.3.0 comicapi>=2.2.0,<2.3.0
kobo = kobo =
jsonschema>=3.2.0,<4.3.0 jsonschema>=3.2.0,<4.5.0

File diff suppressed because it is too large Load Diff