|
|
|
@ -139,11 +139,13 @@ class Gauth:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.auth = GoogleAuth(settings_file='settings.yaml')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Singleton
|
|
|
|
|
class Gdrive:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.drive = gdriveutils.getDrive(Gauth.Instance().auth)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReverseProxied(object):
|
|
|
|
|
"""Wrap the application in this middleware and configure the
|
|
|
|
|
front-end server to add these headers, to let you quietly bind
|
|
|
|
@ -235,6 +237,7 @@ if config.config_log_level == logging.DEBUG :
|
|
|
|
|
def is_gdrive_ready():
|
|
|
|
|
return os.path.exists('settings.yaml') and os.path.exists('gdrive_credentials')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@babel.localeselector
|
|
|
|
|
def get_locale():
|
|
|
|
|
# if a user is logged in, use the locale from the user settings
|
|
|
|
@ -274,16 +277,19 @@ def load_user_from_header(header_val):
|
|
|
|
|
return user
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_auth(username, password):
|
|
|
|
|
user = ub.session.query(ub.User).filter(ub.User.nickname == username).first()
|
|
|
|
|
return bool(user and check_password_hash(user.password, password))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def authenticate():
|
|
|
|
|
return Response(
|
|
|
|
|
'Could not verify your access level for that URL.\n'
|
|
|
|
|
'You have to login with proper credentials', 401,
|
|
|
|
|
{'WWW-Authenticate': 'Basic realm="Login Required"'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def updateGdriveCalibreFromLocal():
|
|
|
|
|
gdriveutils.backupCalibreDbAndOptionalDownload(Gdrive.Instance().drive)
|
|
|
|
|
gdriveutils.copyToDrive(Gdrive.Instance().drive, config.config_calibre_dir, False, True)
|
|
|
|
@ -291,6 +297,7 @@ def updateGdriveCalibreFromLocal():
|
|
|
|
|
if os.path.isdir(os.path.join(config.config_calibre_dir, x)):
|
|
|
|
|
shutil.rmtree(os.path.join(config.config_calibre_dir, x))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def requires_basic_auth_if_no_ano(f):
|
|
|
|
|
@wraps(f)
|
|
|
|
|
def decorated(*args, **kwargs):
|
|
|
|
@ -383,12 +390,14 @@ def mimetype_filter(val):
|
|
|
|
|
s = 'application/octet-stream'
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.template_filter('formatdate')
|
|
|
|
|
def formatdate(val):
|
|
|
|
|
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val)
|
|
|
|
|
formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S")
|
|
|
|
|
return format_date(formatdate, format='medium', locale=get_locale())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.template_filter('strftime')
|
|
|
|
|
def timestamptodate(date, fmt=None):
|
|
|
|
|
date = datetime.datetime.fromtimestamp(
|
|
|
|
@ -401,6 +410,7 @@ def timestamptodate(date, fmt=None):
|
|
|
|
|
time_format = '%d %m %Y - %H:%S'
|
|
|
|
|
return native.strftime(time_format)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def admin_required(f):
|
|
|
|
|
"""
|
|
|
|
|
Checks if current_user.role == 1
|
|
|
|
@ -484,8 +494,12 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|
|
|
|
del_elements = []
|
|
|
|
|
for c_elements in db_book_object:
|
|
|
|
|
found = False
|
|
|
|
|
if db_type == 'custom':
|
|
|
|
|
type_elements=c_elements.value
|
|
|
|
|
else:
|
|
|
|
|
type_elements=c_elements.name
|
|
|
|
|
for inp_element in input_elements:
|
|
|
|
|
if inp_element == c_elements.name:
|
|
|
|
|
if inp_element == type_elements:
|
|
|
|
|
found = True
|
|
|
|
|
break
|
|
|
|
|
# if the element was not found in the new list, add it to remove list
|
|
|
|
@ -496,7 +510,11 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|
|
|
|
for inp_element in input_elements:
|
|
|
|
|
found = False
|
|
|
|
|
for c_elements in db_book_object:
|
|
|
|
|
if inp_element == c_elements.name:
|
|
|
|
|
if db_type == 'custom':
|
|
|
|
|
type_elements = c_elements.value
|
|
|
|
|
else:
|
|
|
|
|
type_elements = c_elements.name
|
|
|
|
|
if inp_element == type_elements:
|
|
|
|
|
found = True
|
|
|
|
|
break
|
|
|
|
|
if not found:
|
|
|
|
@ -511,6 +529,8 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|
|
|
|
if len(add_elements) > 0:
|
|
|
|
|
if db_type == 'languages':
|
|
|
|
|
db_filter = db_object.lang_code
|
|
|
|
|
elif db_type == 'custom':
|
|
|
|
|
db_filter = db_object.value
|
|
|
|
|
else:
|
|
|
|
|
db_filter = db_object.name
|
|
|
|
|
for add_element in add_elements:
|
|
|
|
@ -520,9 +540,10 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|
|
|
|
if new_element is None:
|
|
|
|
|
if db_type == 'author':
|
|
|
|
|
new_element = db_object(add_element, add_element, "")
|
|
|
|
|
else:
|
|
|
|
|
if db_type == 'series':
|
|
|
|
|
elif db_type == 'series':
|
|
|
|
|
new_element = db_object(add_element, add_element)
|
|
|
|
|
elif db_type == 'custom':
|
|
|
|
|
new_element = db_object(value=add_element)
|
|
|
|
|
else: # db_type should be tag, or languages
|
|
|
|
|
new_element = db_object(add_element)
|
|
|
|
|
db_session.add(new_element)
|
|
|
|
@ -530,7 +551,6 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|
|
|
|
# add element to book
|
|
|
|
|
db_book_object.append(new_element)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_title_template(*args, **kwargs):
|
|
|
|
|
return render_template(instance=config.config_calibre_web_title, *args, **kwargs)
|
|
|
|
|
|
|
|
|
@ -642,6 +662,7 @@ def feed_best_rated():
|
|
|
|
|
response.headers["Content-Type"] = "application/xml"
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/opds/hot")
|
|
|
|
|
@requires_basic_auth_if_no_ano
|
|
|
|
|
def feed_hot():
|
|
|
|
@ -781,6 +802,7 @@ def partial(total_byte_len, part_size_limit):
|
|
|
|
|
s.append([p, last])
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def do_gdrive_download(df, headers):
|
|
|
|
|
total_size = int(df.metadata.get('fileSize'))
|
|
|
|
|
download_url = df.metadata.get('downloadUrl')
|
|
|
|
@ -796,6 +818,7 @@ def do_gdrive_download(df, headers):
|
|
|
|
|
return
|
|
|
|
|
return Response(stream_with_context(stream()), headers=headers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/opds/download/<book_id>/<book_format>/")
|
|
|
|
|
@requires_basic_auth_if_no_ano
|
|
|
|
|
@download_required
|
|
|
|
@ -861,6 +884,7 @@ def get_tags_json():
|
|
|
|
|
json_dumps = json.dumps([dict(name=r.name) for r in entries])
|
|
|
|
|
return json_dumps
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/get_update_status", methods=['GET'])
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def get_update_status():
|
|
|
|
@ -880,6 +904,7 @@ def get_update_status():
|
|
|
|
|
status['status'] = False
|
|
|
|
|
return json.dumps(status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/get_updater_status", methods=['GET', 'POST'])
|
|
|
|
|
@login_required
|
|
|
|
|
@admin_required
|
|
|
|
@ -1165,6 +1190,7 @@ def toggle_read(book_id):
|
|
|
|
|
ub.session.commit()
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/book/<int:book_id>")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def show_book(book_id):
|
|
|
|
@ -1196,8 +1222,6 @@ def show_book(book_id):
|
|
|
|
|
for entry in shelfs:
|
|
|
|
|
book_in_shelfs.append(entry.shelf)
|
|
|
|
|
|
|
|
|
|
#return render_title_template('detail.html', entry=entries, cc=cc,
|
|
|
|
|
# title=entries.title, books_shelfs=book_in_shelfs)
|
|
|
|
|
if not current_user.is_anonymous():
|
|
|
|
|
matching_have_read_book = ub.session.query(ub.ReadBook).filter(ub.and_(ub.ReadBook.user_id == int(current_user.id),
|
|
|
|
|
ub.ReadBook.book_id == book_id)).all()
|
|
|
|
@ -1255,6 +1279,29 @@ def stats():
|
|
|
|
|
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions,
|
|
|
|
|
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/delete/<int:book_id>/")
|
|
|
|
|
@login_required
|
|
|
|
|
def delete_book(book_id):
|
|
|
|
|
if current_user.role_delete_books():
|
|
|
|
|
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
|
|
|
|
if book:
|
|
|
|
|
# check if only this book links to:
|
|
|
|
|
# author, language, series, tags,
|
|
|
|
|
modify_database_object([u''], book.authors, db.Authors, db.session, 'author')
|
|
|
|
|
modify_database_object([u''], book.tags, db.Tags, db.session, 'tags')
|
|
|
|
|
modify_database_object([u''], book.series, db.Series, db.session, 'series')
|
|
|
|
|
modify_database_object([u''], book.languages, db.Languages, db.session, 'languages')
|
|
|
|
|
modify_database_object([u''], book.publishers, db.Publishers, db.session, 'series')
|
|
|
|
|
|
|
|
|
|
# custom colums open
|
|
|
|
|
ub.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
|
|
|
|
#return redirect(url_for('index'))
|
|
|
|
|
else:
|
|
|
|
|
app.logger.info('Book with id "'+book_id+'" could not be deleted')
|
|
|
|
|
# book not found
|
|
|
|
|
return redirect(url_for('index'))
|
|
|
|
|
|
|
|
|
|
@app.route("/gdrive/authenticate")
|
|
|
|
|
@login_required
|
|
|
|
|
@admin_required
|
|
|
|
@ -1262,6 +1309,7 @@ def authenticate_google_drive():
|
|
|
|
|
authUrl = Gauth.Instance().auth.GetAuthUrl()
|
|
|
|
|
return redirect(authUrl)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/gdrive/callback")
|
|
|
|
|
def google_drive_callback():
|
|
|
|
|
auth_code = request.args.get('code')
|
|
|
|
@ -1270,6 +1318,7 @@ def google_drive_callback():
|
|
|
|
|
f.write(credentials.to_json())
|
|
|
|
|
return redirect(url_for('configuration'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/gdrive/watch/subscribe")
|
|
|
|
|
@login_required
|
|
|
|
|
@admin_required
|
|
|
|
@ -1291,6 +1340,7 @@ def watch_gdrive():
|
|
|
|
|
|
|
|
|
|
return redirect(url_for('configuration'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/gdrive/watch/revoke")
|
|
|
|
|
@login_required
|
|
|
|
|
@admin_required
|
|
|
|
@ -1308,6 +1358,7 @@ def revoke_watch_gdrive():
|
|
|
|
|
config.loadSettings()
|
|
|
|
|
return redirect(url_for('configuration'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/gdrive/watch/callback", methods=['GET', 'POST'])
|
|
|
|
|
def on_received_watch_confirmation():
|
|
|
|
|
app.logger.info(request.headers)
|
|
|
|
@ -1373,6 +1424,7 @@ def shutdown():
|
|
|
|
|
return json.dumps({})
|
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/update")
|
|
|
|
|
@login_required
|
|
|
|
|
@admin_required
|
|
|
|
@ -1487,6 +1539,7 @@ def get_cover_via_gdrive(cover_path):
|
|
|
|
|
gdriveutils.session.commit()
|
|
|
|
|
return df.metadata.get('webContentLink')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/cover/<path:cover_path>")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def get_cover(cover_path):
|
|
|
|
@ -1508,6 +1561,7 @@ def feed_get_cover(book_id):
|
|
|
|
|
else:
|
|
|
|
|
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), "cover.jpg")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_read_books(page, are_read, as_xml=False):
|
|
|
|
|
readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id)).filter(ub.ReadBook.is_read == True).all()
|
|
|
|
|
readBookIds = [x.book_id for x in readBooks]
|
|
|
|
@ -1528,6 +1582,7 @@ def render_read_books(page, are_read, as_xml=False):
|
|
|
|
|
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
|
|
|
|
|
title=_(name, name=name))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/opds/readbooks/")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def feed_read_books():
|
|
|
|
@ -1536,12 +1591,14 @@ def feed_read_books():
|
|
|
|
|
off = 0
|
|
|
|
|
return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/readbooks/", defaults={'page': 1})
|
|
|
|
|
@app.route("/readbooks/<int:page>'")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def read_books(page):
|
|
|
|
|
return render_read_books(page, True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/opds/unreadbooks/")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def feed_unread_books():
|
|
|
|
@ -1550,12 +1607,14 @@ def feed_unread_books():
|
|
|
|
|
off = 0
|
|
|
|
|
return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/unreadbooks/", defaults={'page': 1})
|
|
|
|
|
@app.route("/unreadbooks/<int:page>'")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def unread_books(page):
|
|
|
|
|
return render_read_books(page, False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/read/<int:book_id>/<book_format>")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
def read_book(book_id, book_format):
|
|
|
|
@ -1613,6 +1672,7 @@ def read_book(book_id, book_format):
|
|
|
|
|
flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
|
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/download/<int:book_id>/<book_format>")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
@download_required
|
|
|
|
@ -1644,12 +1704,14 @@ def get_download_link(book_id, book_format):
|
|
|
|
|
else:
|
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/download/<int:book_id>/<book_format>/<anyname>")
|
|
|
|
|
@login_required_if_no_ano
|
|
|
|
|
@download_required
|
|
|
|
|
def get_download_link_ext(book_id, book_format, anyname):
|
|
|
|
|
return get_download_link(book_id, book_format)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/register', methods=['GET', 'POST'])
|
|
|
|
|
def register():
|
|
|
|
|
if not config.config_public_reg:
|
|
|
|
@ -2016,7 +2078,7 @@ def configuration_helper(origin):
|
|
|
|
|
if content.config_calibre_dir != to_save["config_calibre_dir"]:
|
|
|
|
|
content.config_calibre_dir = to_save["config_calibre_dir"]
|
|
|
|
|
db_change = True
|
|
|
|
|
##Google drive setup
|
|
|
|
|
# Google drive setup
|
|
|
|
|
create_new_yaml = False
|
|
|
|
|
if "config_google_drive_client_id" in to_save:
|
|
|
|
|
if content.config_google_drive_client_id != to_save["config_google_drive_client_id"]:
|
|
|
|
@ -2082,6 +2144,8 @@ def configuration_helper(origin):
|
|
|
|
|
content.config_default_role = content.config_default_role + ub.ROLE_UPLOAD
|
|
|
|
|
if "edit_role" in to_save:
|
|
|
|
|
content.config_default_role = content.config_default_role + ub.ROLE_EDIT
|
|
|
|
|
if "delete_role" in to_save:
|
|
|
|
|
content.config_default_role = content.config_default_role + ub.ROLE_DELETE_BOOKS
|
|
|
|
|
if "passwd_role" in to_save:
|
|
|
|
|
content.config_default_role = content.config_default_role + ub.ROLE_PASSWD
|
|
|
|
|
if "passwd_role" in to_save:
|
|
|
|
@ -2176,6 +2240,8 @@ def new_user():
|
|
|
|
|
if "upload_role" in to_save:
|
|
|
|
|
content.role = content.role + ub.ROLE_UPLOAD
|
|
|
|
|
if "edit_role" in to_save:
|
|
|
|
|
content.role = content.role + ub.ROLE_DELETE_BOOKS
|
|
|
|
|
if "delete_role" in to_save:
|
|
|
|
|
content.role = content.role + ub.ROLE_EDIT
|
|
|
|
|
if "passwd_role" in to_save:
|
|
|
|
|
content.role = content.role + ub.ROLE_PASSWD
|
|
|
|
@ -2279,6 +2345,11 @@ def edit_user(user_id):
|
|
|
|
|
elif "edit_role" not in to_save and content.role_edit():
|
|
|
|
|
content.role = content.role - ub.ROLE_EDIT
|
|
|
|
|
|
|
|
|
|
if "delete_role" in to_save and not content.role_delete_books():
|
|
|
|
|
content.role = content.role + ub.ROLE_DELETE_BOOKS
|
|
|
|
|
elif "delete_role" not in to_save and content.role_delete_books():
|
|
|
|
|
content.role = content.role - ub.ROLE_DELETE_BOOKS
|
|
|
|
|
|
|
|
|
|
if "passwd_role" in to_save and not content.role_passwd():
|
|
|
|
|
content.role = content.role + ub.ROLE_PASSWD
|
|
|
|
|
elif "passwd_role" not in to_save and content.role_passwd():
|
|
|
|
@ -2384,8 +2455,14 @@ def edit_book(book_id):
|
|
|
|
|
input_authors = to_save["author_name"].split('&')
|
|
|
|
|
input_authors = map(lambda it: it.strip(), input_authors)
|
|
|
|
|
# we have all author names now
|
|
|
|
|
if input_authors == ['']:
|
|
|
|
|
input_authors = [_(u'unknown')] # prevent empty Author
|
|
|
|
|
if book.authors:
|
|
|
|
|
author0_before_edit = book.authors[0].name
|
|
|
|
|
else:
|
|
|
|
|
author0_before_edit = db.Authors(_(u'unknown'),'',0)
|
|
|
|
|
modify_database_object(input_authors, book.authors, db.Authors, db.session, 'author')
|
|
|
|
|
if book.authors:
|
|
|
|
|
if author0_before_edit != book.authors[0].name:
|
|
|
|
|
edited_books_id.add(book.id)
|
|
|
|
|
book.author_sort = helper.get_sorted_author(input_authors[0])
|
|
|
|
@ -2510,7 +2587,8 @@ def edit_book(book_id):
|
|
|
|
|
else:
|
|
|
|
|
input_tags = to_save[cc_string].split(',')
|
|
|
|
|
input_tags = map(lambda it: it.strip(), input_tags)
|
|
|
|
|
input_tags = [x for x in input_tags if x != '']
|
|
|
|
|
modify_database_object(input_tags, getattr(book, cc_string),db.cc_classes[c.id], db.session, 'custom')
|
|
|
|
|
'''input_tags = [x for x in input_tags if x != '']
|
|
|
|
|
# we have all author names now
|
|
|
|
|
# 1. search for tags to remove
|
|
|
|
|
del_tags = []
|
|
|
|
@ -2552,7 +2630,7 @@ def edit_book(book_id):
|
|
|
|
|
new_tag = db.session.query(db.cc_classes[c.id]).filter(
|
|
|
|
|
db.cc_classes[c.id].value == add_tag).first()
|
|
|
|
|
# add tag to book
|
|
|
|
|
getattr(book, cc_string).append(new_tag)
|
|
|
|
|
getattr(book, cc_string).append(new_tag)'''
|
|
|
|
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
author_names = []
|
|
|
|
@ -2669,10 +2747,8 @@ def upload():
|
|
|
|
|
db.session.flush() # flush content get db_book.id avalible
|
|
|
|
|
# add comment
|
|
|
|
|
upload_comment = Markup(meta.description).unescape()
|
|
|
|
|
db_comment = None
|
|
|
|
|
if upload_comment != "":
|
|
|
|
|
db_comment = db.Comments(upload_comment, db_book.id)
|
|
|
|
|
db.session.add(db_comment)
|
|
|
|
|
db.session.add(db.Comments(upload_comment, db_book.id))
|
|
|
|
|
db.session.commit()
|
|
|
|
|
if db_language is not None: # display Full name instead of iso639.part3
|
|
|
|
|
db_book.languages[0].language_name = _(meta.languages)
|
|
|
|
@ -2691,6 +2767,7 @@ def upload():
|
|
|
|
|
else:
|
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start_gevent():
|
|
|
|
|
from gevent.wsgi import WSGIServer
|
|
|
|
|
global gevent_server
|
|
|
|
|