Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
157 changes: 93 additions & 64 deletions cps/editbooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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 = '<a href="{}">{}</a>'.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/<int:book_id>", methods=['POST'])
@login_required_if_no_ano
@edit_required
Expand Down Expand Up @@ -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 = '<a href="{}">{}</a>'.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
Expand Down
8 changes: 8 additions & 0 deletions cps/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
35 changes: 26 additions & 9 deletions cps/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,32 @@
</li>
{% endif %}
{% if current_user.role_upload() and g.allow_upload %}
<li>
<form id="form-upload" class="navbar-form" action="{{ url_for('edit-book.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"
type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple></span>
<input class="hide" id="btn-upload2" name="btn-upload2" type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}">
</div>
</form>
<li class="dropdown" id="upload-dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-upload"></span> {{_('Upload')}} <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li>
<form id="form-upload" class="navbar-form" action="{{ url_for('edit-book.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<span class="btn btn-default btn-file" style="min-width: 140px; display: inline-block; text-align: center;"> {{_('Select File')}}
<input id="btn-upload" name="btn-upload" type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}" multiple>
</span>
<input class="hide" id="btn-upload2" name="btn-upload2" type="file" accept="{% for format in accept %}.{% if format != ''%}{{format}}{% else %}*{% endif %}{{ ',' if not loop.last }}{% endfor %}">
</div>
</form>
</li>
<li>
<form id="form-upload-url" class="navbar-form" action="{{ url_for('edit-book.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="form-group" style="display: flex; gap: 8px; align-items: center;">
<button type="submit" class="btn btn-default" style="min-width: 140px;">{{_('Fetch from URL')}}</button>
<input type="url" class="form-control" name="upload_url" placeholder="{{_('Paste eBook URL')}}" required>
</div>
</form>
</li>
</ul>
</li>
{% endif %}
{% if not current_user.is_anonymous and not simple%}
Expand Down