Skip to content
Merged
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
202 changes: 198 additions & 4 deletions cps/editbooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import os
from datetime import datetime
import json
from shutil import copyfile
import sqlite3
import subprocess
from shutil import copyfile, move
from uuid import uuid4
from markupsafe import escape, Markup # dependency of flask
from functools import wraps
Expand All @@ -33,16 +35,16 @@
except ImportError:
clean_html = None

from flask import Blueprint, request, flash, redirect, url_for, abort, Response
from flask import Blueprint, request, flash, redirect, url_for, abort, Response, jsonify
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from flask_babel import get_locale
from flask_login import current_user, login_required
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError
from sqlalchemy.exc import OperationalError, IntegrityError, InterfaceError, InvalidRequestError
from sqlalchemy.orm.exc import StaleDataError
from sqlalchemy.sql.expression import func

from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status
from . import constants, logger, isoLanguages, gdriveutils, uploader, helper, kobo_sync_status, shelf
from . import config, ub, db, calibre_db
from .services.worker import WorkerThread
from .tasks.upload import TaskUpload
Expand Down Expand Up @@ -308,6 +310,198 @@ def upload():
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')


@editbook.route("/media", methods=["POST"])
@login_required_if_no_ano
@upload_required
def media():
if not config.config_uploading:
abort(404)

def get_yb_executable():
yb_executable = os.getenv("YB_EXECUTABLE", "yb")
return yb_executable

def run_subprocess(command_args):
try:
completed_process = subprocess.run(
command_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
text=True
)
return (True, completed_process.stdout, completed_process.stderr)

except subprocess.CalledProcessError as e:
error_message = f"Subprocess error (return code {e.returncode}): {e.stderr}, {e.stdout}"
log.error(error_message)
return False, error_message, e.stdout, e.stderr
except Exception as e:
error_message = f"An error occurred while running the subprocess: {e}"
log.error(error_message)
return False, error_message

def process_media_download(media_url):
yb_executable = get_yb_executable()

if media_url:
download_args = [
yb_executable,
media_url,
]
subprocess_result = run_subprocess(download_args)

log.info("Subprocess result: {}".format(subprocess_result))

if subprocess_result[0]:
requested_files = []
download_db_path = "/var/tmp/download.db"
conn = sqlite3.connect(download_db_path)
c = conn.cursor()
c.execute("SELECT path FROM media")
for row in c.fetchall():
requested_files.append(row[0])
# check also if there is a "playlists" table.
# if the "playlists" table exists, create the shelf_title variable to store the playlist title
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='playlists'")
if c.fetchone():
c.execute("SELECT title FROM playlists")
shelf_title = c.fetchone()[0]
else:
shelf_title = None
conn.close()

if shelf_title:
shelf_object = ub.Shelf()
is_public = 1
if shelf.check_shelf_is_unique(shelf_title, is_public, shelf_id=None):
shelf_object.name = shelf_title
shelf_object.is_public = is_public
shelf_object.user_id = int(current_user.id)
ub.session.add(shelf_object)
shelf_action = "created"
flash_text = _("Shelf %(title)s created", title=shelf_title)
try:
ub.session.commit()
shelf_id = shelf_object.id
log.info("Shelf {} {}".format(shelf_title, shelf_action))
flash(flash_text, category="success")
except (OperationalError, InvalidRequestError) as ex:
ub.session.rollback()
log.error_or_exception(ex)
log.error_or_exception("Settings Database error: {}".format(ex))
flash(_("Oops! Database Error: %(error)s.", error=ex.orig), category="error")
except Exception as ex:
ub.session.rollback()
log.error_or_exception(ex)
flash(_("There was an error"), category="error")

log.info("Requested files: {}".format(requested_files))
for requested_file in requested_files:
requested_file = open(requested_file, "rb")
requested_file.filename = os.path.basename(requested_file.name)
requested_file.save = lambda path: copyfile(requested_file.name, path)

log.info("Processing file: {}".format(requested_file))
try:
modify_date = False
calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function(
"uuid4", 0, lambda: str(uuid4())
)

meta, error = file_handling_on_upload(requested_file)
if error:
return error

(
db_book,
input_authors,
title_dir,
renamed_authors,
) = 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

error = helper.update_dir_structure(
book_id,
config.config_calibre_dir,
input_authors[0],
meta.file_path,
title_dir + meta.extension.lower(),
renamed_author=renamed_authors,
)

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 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 shelf_title:
shelf.add_to_shelf.__closure__[0].cell_contents(
shelf_id, book_id
)

if len(requested_files) < 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 Response(json.dumps(resp), mimetype="application/json")
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",
)
else:
flash("Error: 'lb' executable not found in PATH", category="error")
return False

if request.method == "POST" and "mediaURL" in request.form:
media_url = request.form["mediaURL"]

if process_media_download(media_url):
response = {
"success": "Downloaded media successfully",
}
return jsonify(response)
else:
response = {
"error": "Failed to download media",
}
return jsonify(response), 500


@editbook.route("/admin/book/convert/<int:book_id>", methods=['POST'])
@login_required_if_no_ano
@edit_required
Expand Down
132 changes: 132 additions & 0 deletions cps/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,137 @@ $(document).ready(function() {
inp.val('').blur().focus().val(val)
}
}

// Function to toggle advanced options visibility
function toggleAdvancedOptions() {
var advancedOptions = $("#advancedOptions");
if (advancedOptions.is(":visible")) {
advancedOptions.hide();
$("#advancedOptionsToggle").text("Show advanced options")
} else {
advancedOptions.show();
$("#advancedOptionsToggle").text("Hide advanced options")
}
}

// Handle click event for the advanced options toggle
$("#advancedOptionsToggle").click(function(event) {
event.preventDefault();
toggleAdvancedOptions();
});

// Function to initiate the media download AJAX request
function initiateMediaDownload() {
var url = $("#mediaURL").val();
var videoQuality = $("input[name='videoQuality']:checked").val();
var maxVideos = $("#maxVideos").val();
var maxVideosSize = $("#maxVideosSize").val();
var addToBookshelf = $("#addToBookshelf").is(":checked");

// Set empty number values to zero
maxVideos = maxVideos === "" ? 0 : parseInt(maxVideos);
maxVideosSize = maxVideosSize === "" ? 0 : parseInt(maxVideosSize);

// Check if the input URL is a valid URL
// First check if URL starts with https:// if not, prepend it
url = url.startsWith("https://") ? url : "https://" + url;
if (!isValidURL(url)) {
alert("Invalid URL");
return;
}

$.ajax({
url: "/live/media",
method: "POST",
data: {
csrf_token: $("#mediaDownloadForm input[name=csrf_token]").val(),
mediaURL: url,
videoQuality: videoQuality,
maxVideos: maxVideos,
maxVideosSize: maxVideosSize,
addToBookshelf: addToBookshelf
},
success: function(response) {
// Handle success response here
if (response && response.location) {
// Redirect to the specified location
window.location.href = response.location;
} else {
// Handle any specific success behavior
console.log("Media download request successful.");
}
},
error: function(xhr, status, error) {
// Handle error here
console.log("Media download request failed:", error);
$("#mediaDownloadForm .error-message").text("Media download request failed.");
}
});
}

// Handle Enter key press event in the input field
$(document).on('keydown', function(event) {
// Check if the pressed key is Enter (key code 13)
if (event.which === 13 && $("#mediaDownloadModal").is(":visible")) {
initiateMediaDownload();
}
});

// Handle the "Start" button click event
$("#btn-download-media-submit").click(function() {
initiateMediaDownload();
});

// Handle change event for the video quality radio buttons
$("input[name='videoQuality']").change(function() {
// Handle change event
});

// Handle input event for the max videos input
$("#maxVideos").on('input', function() {
var inputValue = $(this).val();
if (!/^\d*$/.test(inputValue)) {
alert("Please enter a valid number.");
$(this).val("");
}

// If maxVideos is changed, disable and clear maxVideosSize
if (inputValue) {
$("#maxVideosSize").prop("disabled", true).val("");
} else {
$("#maxVideosSize").prop("disabled", false);
}
});

// Handle input event for the max size input
$("#maxVideosSize").on('input', function() {
var inputValue = $(this).val();
if (!/^\d*$/.test(inputValue)) {
alert("Please enter a valid number.");
$(this).val("");
}

// If maxVideosSize is changed, disable and clear maxVideos
if (inputValue) {
$("#maxVideos").prop("disabled", true).val("");
} else {
$("#maxVideos").prop("disabled", false);
}
});

// Handle change event for the add to bookshelf checkbox
$("#addToBookshelf").change(function() {
// Handle change event
});

// Function to validate URL
function isValidURL(url) {
// Regex to validate URL (should be any url starting with https://)
var urlPattern = /^https?:\/\//i;
// Check if the URL matches the pattern
return urlPattern.test(url);
}

});

$(".session").click(function() {
Expand Down Expand Up @@ -333,6 +464,7 @@ $(function() {
} else {
$("#parent").addClass('hidden')
}
// console.log(data);
data.files.forEach(function(entry) {
if(entry.type === "dir") {
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";
Expand Down
Loading