diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5f538dfa86..552e2dfd24 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,7 +16,9 @@ We can offer only very limited support regarding configuration of **Reverse-Prox
### **Translation**
-Some of the user languages in Calibre-Web having missing translations. We are happy to add the missing texts if you translate them. Create a Pull Request, create an issue with the .po file attached, or write an email to "ozzie.fernandez.isaacs@googlemail.com" with attached translation file. To display all book languages in your native language an additional file is used (iso_language_names.py). The content of this file is auto-generated with the corresponding translations of Calibre, please do not edit this file on your own.
+Some of the user languages in Calibre-Web have missing translations. We are happy to add the missing texts if you translate them. Create a Pull Request, create an issue with the .po file attached, or write an email to "ozzie.fernandez.isaacs@googlemail.com" with attached translation file. To display all book languages in your native language an additional file is used (iso_language_names.py). The content of this file is auto-generated with the corresponding translations of Calibre, please do not edit this file on your own.
+
+If your contribution has changed any line numbers of messages that require translation (very likely), you can update the main messages.pot file by running ```pybabel extract --no-wrap -F babel.cfg -o messages.pot cps```. Afterwards you can update the translation files via ```pybabel update -i messages.pot -d cps/translations``` and fill in the gaps where you can.
### **Documentation**
diff --git a/cps/editbooks.py b/cps/editbooks.py
index a2c39b9d95..38e5126f8b 100644
--- a/cps/editbooks.py
+++ b/cps/editbooks.py
@@ -49,6 +49,10 @@
from .usermanagement import user_login_required, login_required_if_no_ano
from .string_helper import strip_whitespaces
+import io
+import requests
+from werkzeug.datastructures import FileStorage
+
editbook = Blueprint('edit-book', __name__)
log = logger.create()
@@ -100,78 +104,19 @@ def show_edit_book(book_id):
def edit_book(book_id):
return do_edit_book(book_id)
-
@editbook.route("/upload", methods=["POST"])
@login_required_if_no_ano
@upload_required
def upload():
- if len(request.files.getlist("btn-upload-format")):
+ if request.form.get("upload_url"):
+ return do_upload_url(request.form.get("upload_url"))
+ elif len(request.files.getlist("btn-upload")):
+ return do_upload_file_list(request.files.getlist("btn-upload"))
+ elif len(request.files.getlist("btn-upload-format")):
book_id = request.form.get('book_id', -1)
return do_edit_book(book_id, request.files.getlist("btn-upload-format"))
- elif len(request.files.getlist("btn-upload")):
- for requested_file in request.files.getlist("btn-upload"):
- try:
- modify_date = False
- # create the function for sorting...
- calibre_db.create_functions(config)
- meta, error = file_handling_on_upload(requested_file)
- if error:
- return error
-
- db_book, input_authors, title_dir = create_book_on_upload(modify_date, meta)
-
- # Comments need book id therefore only possible after flush
- modify_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
-
- book_id = db_book.id
- title = db_book.title
- if config.config_use_google_drive:
- helper.upload_new_file_gdrive(book_id,
- input_authors[0],
- title,
- title_dir,
- meta.file_path,
- meta.extension.lower())
- for file_format in db_book.data:
- file_format.name = (helper.get_valid_filename(title, chars=42) + ' - '
- + helper.get_valid_filename(input_authors[0], chars=42))
- else:
- error = helper.update_dir_structure(book_id,
- config.get_book_path(),
- input_authors[0],
- meta.file_path,
- title_dir + meta.extension.lower())
- move_coverfile(meta, db_book)
- if modify_date:
- calibre_db.set_metadata_dirty(book_id)
- # save data to database, reread data
- calibre_db.session.commit()
-
- if config.config_use_google_drive:
- gdriveutils.updateGdriveCalibreFromLocal()
- if error:
- flash(error, category="error")
- link = '{}'.format(url_for('web.show_book', book_id=book_id), escape(title))
- upload_text = N_("File %(file)s uploaded", file=link)
- WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
- helper.add_book_to_thumbnail_cache(book_id)
-
- if len(request.files.getlist("btn-upload")) < 2:
- if current_user.role_edit() or current_user.role_admin():
- resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
- return make_response(jsonify(resp))
- else:
- resp = {"location": url_for('web.show_book', book_id=book_id)}
- return Response(json.dumps(resp), mimetype='application/json')
- except (OperationalError, IntegrityError, StaleDataError) as e:
- calibre_db.session.rollback()
- log.error_or_exception("Database error: {}".format(e))
- flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
- category="error")
- return make_response(jsonify(location=url_for("web.index")))
abort(404)
-
@editbook.route("/admin/book/convert/", methods=['POST'])
@login_required_if_no_ano
@edit_required
@@ -613,6 +558,90 @@ def table_xchange_author_title():
return make_response(jsonify(success=True))
return ""
+def do_upload_url(url: str) -> FileStorage:
+ try:
+ r = requests.get(url, stream=True, timeout=30)
+ r.raise_for_status()
+ url_end = r.url.split('/')[-1]
+ filename_sanitized = "".join(c for c in url_end if c.isalnum() or c in "._-") # sanitize filename
+ filename = filename_sanitized if len(filename_sanitized) > 0 else "uploaded_file"
+
+ file_buffer = io.BytesIO()
+ for chunk in r.iter_content():
+ file_buffer.write(chunk)
+ file_buffer.seek(0)
+
+ return do_upload_file_list([FileStorage(file_buffer, filename=filename)])
+
+
+ except Exception as e:
+ log.error_or_exception("File download error: {}".format(e))
+ flash(_("Oops! Download failed: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
+ category="error")
+ return make_response(jsonify(location=url_for("web.index")))
+
+
+def do_upload_file_list(file_list: list[FileStorage]):
+ for requested_file in file_list:
+ try:
+ modify_date = False
+ # create the function for sorting...
+ calibre_db.create_functions(config)
+ meta, error = file_handling_on_upload(requested_file)
+ if error:
+ return error
+
+ db_book, input_authors, title_dir = create_book_on_upload(modify_date, meta)
+
+ # Comments need book id therefore only possible after flush
+ modify_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
+
+ book_id = db_book.id
+ title = db_book.title
+ if config.config_use_google_drive:
+ helper.upload_new_file_gdrive(book_id,
+ input_authors[0],
+ title,
+ title_dir,
+ meta.file_path,
+ meta.extension.lower())
+ for file_format in db_book.data:
+ file_format.name = (helper.get_valid_filename(title, chars=42) + ' - '
+ + helper.get_valid_filename(input_authors[0], chars=42))
+ else:
+ error = helper.update_dir_structure(book_id,
+ config.get_book_path(),
+ input_authors[0],
+ meta.file_path,
+ title_dir + meta.extension.lower())
+ move_coverfile(meta, db_book)
+ if modify_date:
+ calibre_db.set_metadata_dirty(book_id)
+ # save data to database, reread data
+ calibre_db.session.commit()
+
+ if config.config_use_google_drive:
+ gdriveutils.updateGdriveCalibreFromLocal()
+ if error:
+ flash(error, category="error")
+ link = '{}'.format(url_for('web.show_book', book_id=book_id), escape(title))
+ upload_text = N_("File %(file)s uploaded", file=link)
+ WorkerThread.add(current_user.name, TaskUpload(upload_text, escape(title)))
+ helper.add_book_to_thumbnail_cache(book_id)
+
+ if len(file_list) < 2:
+ if current_user.role_edit() or current_user.role_admin():
+ resp = {"location": url_for('edit-book.show_edit_book', book_id=book_id)}
+ return make_response(jsonify(resp))
+ else:
+ resp = {"location": url_for('web.show_book', book_id=book_id)}
+ return Response(json.dumps(resp), mimetype='application/json')
+ except (OperationalError, IntegrityError, StaleDataError) as e:
+ calibre_db.session.rollback()
+ log.error_or_exception("Database error: {}".format(e))
+ flash(_("Oops! Database Error: %(error)s.", error=e.orig if hasattr(e, "orig") else e),
+ category="error")
+ return make_response(jsonify(location=url_for("web.index")))
def do_edit_book(book_id, upload_formats=None):
modify_date = False
diff --git a/cps/static/js/main.js b/cps/static/js/main.js
index 9138d80e69..fe8d656cf1 100644
--- a/cps/static/js/main.js
+++ b/cps/static/js/main.js
@@ -166,6 +166,14 @@ $("#form-upload-format").uploadprogress({
modalTitleFailed: $("#form-upload-format").data("failed")
});
+$("#form-upload-url").uploadprogress({
+ redirect_url: getPath() + "/",
+ uploadedMsg: $("#form-upload-url").data("message"),
+ modalTitle: $("#form-upload-url").data("title"),
+ modalFooter: $("#form-upload-url").data("footer"),
+ modalTitleFailed: $("#form-upload-url").data("failed")
+});
+
$(document).ready(function() {
var inp = $('#query').first()
if (inp.length) {
diff --git a/cps/templates/layout.html b/cps/templates/layout.html
index dfb347ae3b..1061c1e8de 100644
--- a/cps/templates/layout.html
+++ b/cps/templates/layout.html
@@ -75,15 +75,32 @@
{% endif %}
{% if current_user.role_upload() and g.allow_upload %}
-
-
+
+
+ {{_('Upload')}}
+
+
{% endif %}
{% if not current_user.is_anonymous and not simple%}