diff --git a/app.py b/app.py index 9f0780a..b5f072d 100644 --- a/app.py +++ b/app.py @@ -3,8 +3,12 @@ import json from mediascan import load_config, save_config, list_media_files, filter_media_files, human_readable_size, get_media_info_with_ffprobe, media_get_from_cache, media_create_cache, media_remove_cache, get_single_media_by_path from urllib.parse import unquote +from flask_socketio import SocketIO +import shutil +from transcodate import transcode_file app = Flask(__name__) +socketio = SocketIO(app) # Load configuration CONFIG_FILE = 'config.json' @@ -52,6 +56,41 @@ media_create_cache(config["cache_dir"], filtered_files) return jsonify(filtered_files) +@app.route('/process-media', methods=['POST']) +def process_media(): + data = request.json + file_path = data.get('path') + codec = data.get('codec') + resolution = data.get('resolution') + crf = data.get('crf') + preset = data.get('preset-libx') + cpu_used = data.get('preset-vp9') + if codec == "libaom-av1": + cpu_used = data.get('preset-av1') + + if not file_path or not os.path.exists(file_path): + return jsonify({"error": "File path is invalid or file does not exist"}), 400 + + # Step 1: Copy the file to the transcoded directory + dest_dir = config.get('transcoded_directory', 'transcoded_files') + os.makedirs(dest_dir, exist_ok=True) + dest_path = os.path.join(dest_dir, os.path.basename(file_path)) + shutil.copy(file_path, dest_path) + + # Step 2: Start transcoding + socketio.start_background_task( + target=transcode_file, + socketio=socketio, + file_path=file_path, + codec=codec, + resolution=resolution, + crf=crf, + preset=preset, + cpu_used=cpu_used + ) + + return jsonify({"status": "processing started", "file": file_path}), 202 + @app.route('/configure', methods=['GET', 'POST']) def configure(): if request.method == 'POST': diff --git a/config.json b/config.json index 2c2fb59..294b168 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,10 @@ { "directories": [ - "/home/gmikcon/media-storage/NetTera1" + "/home/gmikcon/media-storage/firstraid5", + "/home/gmikcon/media-storage/NetTera1", + "/home/gmikcon/media-storage/secraid5_storage" ], "allowed_formats": [".mp4", ".m4p", ".m4v", ".mkv", ".webm", ".flv", ".vob", ".ogv", ".amv", ".mng", ".avi", ".mov", ".viv"], - "cache_dir": "cache" + "cache_dir": "cache", + "transcoded_directory": "/home/gmikcon/media-storage/transcoded" } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0ea9fdd..a85de7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,16 @@ +bidict==0.23.1 blinker==1.9.0 click==8.1.8 ffmpeg-python==0.2.0 Flask==3.1.0 +Flask-SocketIO==5.5.1 future==1.0.0 +h11==0.14.0 itsdangerous==2.2.0 Jinja2==3.1.5 MarkupSafe==3.0.2 +python-engineio==4.11.2 +python-socketio==5.12.1 +simple-websocket==1.1.0 Werkzeug==3.1.3 +wsproto==1.2.0 diff --git a/static/css/main.css b/static/css/main.css index 8bf58fe..0baa756 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -50,4 +50,13 @@ white-space: nowrap; display: inline-block; width: 100px; +} + +.crf-range { + margin-left: 5px; +} + +#progress { + margin-top: 20px; + margin-bottom: 0; } \ No newline at end of file diff --git a/static/js/media.js b/static/js/media.js index 1a67548..460a935 100644 --- a/static/js/media.js +++ b/static/js/media.js @@ -97,6 +97,20 @@ } function transcodateFormHandlers() { + const codecCRFMap = { + "libx264": [0, 51], + "libx265": [0, 51], + "libvpx-vp9": [0, 63], + "libaom-av1": [0, 63] + }; + + const maxResolution = $(`[name="resolution"]`).data("max-resolution"); + $(`[name="resolution"] option`).each(function(){ + parseInt($(this).attr("value")) > maxResolution + ? $(this).attr("disabled", "disabled") + : !$(`[name="resolution"] option[selected]`).length && $(this).attr("selected", "selected") + }); + $(".show-transcodate-form").on("click", function(){ const formContainer = $(".transcodate-form-container"); if(formContainer.is(":visible")) { @@ -110,8 +124,104 @@ const formContainer = $(".transcodate-form-container"); formContainer.slideUp(); }); + + const socket = io(); + + // Listen for progress updates + socket.on("progress", (data) => { + document.getElementById("progress").innerText = data.message; + }); + + // Listen for completion + socket.on("completed", (data) => { + document.getElementById("progress").innerText = "Completed!"; + }); + + // Listen for errors + socket.on("error", (data) => { + alert(data.message); + }); + + $(".transcodate-form-container .run-transcodate").on("click", async () => { + const formContiner = $(".transcodate-form-container"); + const fields = formContiner.find("[name]"); + + const data = {}; + for(let field of fields) { + field = $(field); + data[field.attr("name")] = field.val(); + } + + data["crf"] = "" + Math.abs(data["crf"]); + console.log(data); + + const response = await fetch("/process-media", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data) + }); + + if (response.ok) { + document.getElementById("progress").innerText = "Processing started..."; + } else { + const error = await response.json(); + alert(`Error: ${error.error}`); + } + + }); + + $("#codec").on("change", function(){ + const crf = $(`[name="crf"]`); + + const codec = $(this).val(); + crf.attr("min", codecCRFMap[codec][0]); + crf.attr("max", codecCRFMap[codec][1]); + if(parseInt(crf.val()) > codecCRFMap[codec][1]) { + crf.val(codecCRFMap[codec][1]); + } + + crf.parent().find(".crf-range").text(`${codecCRFMap[codec][0]}-${codecCRFMap[codec][1]}`); + + // toggle form of preset + const presetLibxForm = $(`[name="preset-libx"]`); + const presetVP9Form = $(`[name="preset-vp9"]`); + const presetAV1Form = $(`[name="preset-av1"]`); + if(codec == "libx264" || codec == "libx265") { + presetLibxForm.show(); + presetVP9Form.hide(); + presetAV1Form.hide(); + } else if(codec == "libaom-av1"){ + presetLibxForm.hide(); + presetVP9Form.hide(); + presetAV1Form.show(); + } else if(codec == "libvpx-vp9") { + presetLibxForm.hide(); + presetVP9Form.show(); + presetAV1Form.hide(); + } + }); + + $(`[name="crf"]`).on("input", function(){ + const val = parseInt($(this).val()); + + if(isNaN(val)) { + $(this).val(0); + return; + } + + if(val < 0) { + $(this).val(0); + return; + } + + const max = parseInt($(this).attr("max")); + if(val > max) { + $(this).val(max); + } + }); } + // Load media list when the page is ready $(document).ready(function() { loadMediaList(); diff --git a/templates/single.html b/templates/single.html index d1f3340..20e8f85 100644 --- a/templates/single.html +++ b/templates/single.html @@ -1,4 +1,5 @@ {% include "partials/head.html" %} +