diff --git a/app.py b/app.py index b5f072d..363b79b 100644 --- a/app.py +++ b/app.py @@ -14,23 +14,33 @@ CONFIG_FILE = 'config.json' config = load_config(CONFIG_FILE) +transcoding_tasks = {} + @app.route('/') def index(): return render_template('index.html') @app.route("/single") def single_media(): - # Получаем путь из строки запроса - path = request.args.get("path", "") # Значение параметра "path" + path = request.args.get("path", "") if not path: return "Path not provided", 400 - # Декодируем путь path_to_mediafile = unquote(path) media_file = get_single_media_by_path(path_to_mediafile) - # return jsonify(media_file) + return render_template('single.html', file=media_file) +@app.route("/single-json") +def single_media_json(): + path = request.args.get("path", "") + if not path: + return "Path not provided", 400 + + path_to_mediafile = unquote(path) + + return jsonify(get_single_media_by_path(path_to_mediafile)) + @app.route('/media-list/clear-cache') def media_list_clear_cache(): return jsonify({"status": media_remove_cache(config["cache_dir"])}) @@ -75,11 +85,18 @@ 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)) + socketio.emit("progress", { + "task": { + "file": file_path + }, + "message": f"Copying the original file to {dest_path}. Waiting" + }) shutil.copy(file_path, dest_path) # Step 2: Start transcoding socketio.start_background_task( target=transcode_file, + transcoding_tasks=transcoding_tasks, socketio=socketio, file_path=file_path, codec=codec, @@ -91,6 +108,18 @@ return jsonify({"status": "processing started", "file": file_path}), 202 +@app.route('/stop-transcoding', methods=['GET']) +def stop_transcoding(): + task_id = request.args.get("task_id") + + if task_id in transcoding_tasks: + task = transcoding_tasks.pop(task_id, None) + if task: + task["process"].terminate() + return jsonify({"status": "stopped", "task_id": task_id}) + + return jsonify({"status": "error", "message": "Task not found"}), 404 + @app.route('/configure', methods=['GET', 'POST']) def configure(): if request.method == 'POST': diff --git a/mediascan.py b/mediascan.py index b3bc0dc..e27f3d1 100644 --- a/mediascan.py +++ b/mediascan.py @@ -47,7 +47,7 @@ streams_command = [ "ffprobe", "-v", "error", "-show_entries", "stream=index,codec_type,codec_name,width,height,bit_rate,channels,channel_layout", - "-show_entries", "stream_tags=language", + "-show_entries", "stream_tags=language,title", "-of", "json", file_path ] @@ -96,6 +96,7 @@ channels = stream.get("channels", "Unknown") channel_layout = stream.get("channel_layout", "Unknown") language = stream.get("tags", {}).get("language", "Unknown") + title = stream.get("tags", {}).get("title", "Unknown") bitrate = stream_bitrate if stream_bitrate else container_bitrate if codec_type == "video": @@ -115,6 +116,7 @@ "channels": channels, "layout": channel_layout, "language": language, + "title": title, "codec": codec_name }) diff --git a/static/css/main.css b/static/css/main.css index 0baa756..2d58f16 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,3 +1,7 @@ +a { + text-decoration: none; +} + /* LOADING SPINNER */ .loading-spinner-container { @@ -59,4 +63,32 @@ #progress { margin-top: 20px; margin-bottom: 0; -} \ No newline at end of file +} + +.progress-circle { +/* transform: rotate(-90deg);*/ +} + +.circle-bg { + stroke-dasharray: 100, 100; +} + +.circle-progress { + transition: stroke-dasharray 0.3s ease; +} + +.list-group-item.task { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.list-group-item.task .file { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; +} + diff --git a/static/js/components/global-transcoding-tasks.js b/static/js/components/global-transcoding-tasks.js new file mode 100644 index 0000000..e4444b1 --- /dev/null +++ b/static/js/components/global-transcoding-tasks.js @@ -0,0 +1,143 @@ +const globalTasks = {}; +const mediaInfoCenter = {}; + +function createEmptyTaskView(taskId) { + const li = document.createElement('li'); + li.classList.add("list-group-item"); + li.classList.add("task"); + li.dataset.taskId = taskId; + + let html = ` +
+
+ + - 99% +
+
+ + +
+ `; + + li.innerHTML = html; + initSingleCircleProgressBar(li.querySelector(".circle-progress-bar")); + return initTaskView(li); +} + +function getTotalActiveTasks() { + return Object.keys(globalTasks).length; +} + +function initTaskView(view) { + view.querySelector(".control .cancel").addEventListener("click", e => { + const taskId = e.currentTarget.dataset.taskId; + $(e.currentTarget).hide(); + $(e.currentTarget).parent().find(".spinner-border").show(); + + $.getJSON(`/stop-transcoding?task_id=${taskId}`, function(resp) { + if(resp.status == "stoped" && resp.task_id == taskId) { + if(typeof globalTasks[resp.task_id] != "undefined") { + // delete globalTasks[resp.task_id]; + } + } else { + // TODO: show error + } + }); + }); + + return view; +} + +function updateExistsView(data, view) { + const taskContainer = $(view); + const fileLink = taskContainer.find(".file a"); + fileLink.text(data.task.file.split("/").at(-1)); + fileLink.attr("href", `/single?path=${ data.task.file }`); + + let time = "00:00:00"; + if(data.message.indexOf("time=") != -1 && data.message.indexOf("time=N/A") == -1) { + time = data.message.split("time=")[1].split(" ")[0]; + } + + const mInf = getMediaInfo(data.task.file); + + if(!mInf) { + return; + } + + const progressPercent = Math.floor(timeToSeconds(time) / parseInt(mInf.info.container.duration) * 100); + + taskContainer.find(".file .progress-percent").text(`- ${ progressPercent }%`); + taskContainer.find(".file .circle-progress-bar")[0].dataset.value = progressPercent; +} + +function globalTranscodingTasksInit() { + const tasksContainer = $(".transcoding-tasks-container > ul"); + + socket.on("progress", data => { + if(typeof data.task.id == "undefined") { + return; + } + + if(typeof globalTasks[data.task.id] == "undefined") { + // create new view + const view = createEmptyTaskView(data.task.id); + tasksContainer.append(view); + globalTasks[data.task.id] = { + "data": data, + "view": view + } + } + + // update exists view + updateExistsView(data, globalTasks[data.task.id].view); + + $(".total-tasks").text(getTotalActiveTasks()); + }); + + socket.on("completed", data => { + if(typeof globalTasks[data.task.id] != "undefined") { + // remove task view + $(globalTasks[data.task.id].view).remove(); + delete globalTasks[data.task.id]; + + $(".total-tasks").text(getTotalActiveTasks()); + } + }); + + socket.on("canceled", data => { + if(typeof globalTasks[data.task.id] != "undefined") { + // remove task view + $(globalTasks[data.task.id].view).remove(); + delete globalTasks[data.task.id]; + $(".total-tasks").text(getTotalActiveTasks()); + } + }); + + socket.on("error", data => { + // show error + }); +} + +function getMediaInfo(path) { + if(typeof mediaInfoCenter[path] == "undefined") { + getSingleMediaFileInfo(path, resp => { + console.log(resp); + mediaInfoCenter[resp.path] = resp; + }); + + return false; + } + + return mediaInfoCenter[path]; +} + +$(document).ready(function() { + if(typeof mediaInfo != "undefined") { + mediaInfoCenter[mediaInfo.path] = mediaInfo; + } + + globalTranscodingTasksInit(); +}); \ No newline at end of file diff --git a/static/js/components/media-list.js b/static/js/components/media-list.js new file mode 100644 index 0000000..bef1e85 --- /dev/null +++ b/static/js/components/media-list.js @@ -0,0 +1,102 @@ +function LSpinnerShow() { + $(".loading-spinner-container").removeClass("d-none"); +} + +function LSpinnerHide() { + $(".loading-spinner-container").addClass("d-none"); +} + +// Function to load media data +function loadMediaList() { + LSpinnerShow(); + $("#media-table").addClass("d-none"); + + const tableBody = $("#media-table tbody"); + tableBody.empty(); + + $.ajax({ + url: "/media-list", + method: "GET", + success: function(data) { + + if (data.length > 0) { + data.forEach((file, index) => { + const videoInfo = file.info.video + ? file.info.video.map((v, i) => !i ? `
  • Resolution: ${v.resolution}, Codec: ${v.codec}, Bitrate: ${v.bitrate}
  • ` : `
  • Resolution: ${v.resolution}, Codec: ${v.codec}
  • `).join("") + : "
  • No video streams
  • "; + + const audioInfo = file.info.audio + ? file.info.audio.map(a => `
  • ${a.title}, Channels: ${a.layout}, Codec: ${a.codec}, Bitrate: ${a.bitrate}, Language: ${a.language}
  • `).join("") + : "
  • No audio streams
  • "; + + const details = ` + Video: + + Audio: + + `; + + const pathEncoded = encodeURIComponent(file.path); + + tableBody.append(` + + ${index + 1} + +
    ${file.name}
    +
    ${file.path}
    +
    ${details}
    + + ${file.size}${file.size_unit} + + `); + }); + } else { + tableBody.append(`No files found`); + } + + if(!$('#media-table_wrapper').length) { + $('#media-table').DataTable({ + columnDefs: [ + { + targets: "_all", + render: function(data, type, row, meta) { + var cell = meta.settings.aoData[meta.row].anCells[meta.col]; + if (type === 'sort' && cell) { + var orderValue = $(cell).attr('data-order'); + return orderValue !== undefined ? orderValue : data; + } + return data; + } + } + ] + }); + } + + LSpinnerHide(); + $("#media-table").removeClass("d-none"); + }, + error: function(xhr, status, error) { + LSpinnerHide(); + console.error("Error loading media list:", error); + } + }); +} + +function rescanMediaLibHandler() { + $("#do-rescan-media-lib").on("click", function(){ + $.getJSON("/media-list/clear-cache").done(function(resp){ + if(resp.status) { + return loadMediaList(); + } + + console.error("Error of rescan media library"); + // TODO: make alert bar + alert("Error of rescan media library"); + }); + }); +} + +$(document).ready(function() { + loadMediaList(); + rescanMediaLibHandler(); +}); \ No newline at end of file diff --git a/static/js/components/single-transcoding.js b/static/js/components/single-transcoding.js new file mode 100644 index 0000000..cf94fd1 --- /dev/null +++ b/static/js/components/single-transcoding.js @@ -0,0 +1,185 @@ +function singleTranscodingInit() { + 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")) { + formContainer.slideUp(); + } else { + formContainer.slideDown(); + } + }); + + $(".close-transcodate-form-container").on("click", function(){ + const formContainer = $(".transcodate-form-container"); + formContainer.slideUp(); + }); + + const progressContainer = $("#progress"); + progressContainer.pushNewMessage = (msg) => { + !progressContainer.is(":visible") && progressContainer.slideDown(); + progressContainer.html(msg); + }; + // Listen for progress updates + socket.on("progress", (data) => { + if(progressContainer.data("file-path") != data.task.file) { + return; + } + + progressContainer.pushNewMessage(data.message); + $(".transcodate-form-container").slideUp(); + $(".show-transcodate-form").hide(); + $(".stop-transcoding").show(); + $(".stop-transcoding").attr("data-task_id", data.task.id); + $(".transcodate-form-container .run-transcodate").hide(); + }); + + // Listen for completion + socket.on("completed", (data) => { + if(progressContainer.data("file-path") != data.task.file) { + return; + } + + progressContainer.pushNewMessage("Completed!"); + $(".show-transcodate-form").show(); + $(".stop-transcoding").hide(); + $(".transcodate-form-container .run-transcodate").show(); + }); + + // Listen for errors + socket.on("error", (data) => { + if(progressContainer.data("file-path") != data.task.file) { + return; + } + + // TODO: push to notification bar + progressContainer.pushNewMessage(data.message); + $(".show-transcodate-form").show(); + $(".stop-transcoding").hide(); + $(".transcodate-form-container .run-transcodate").show(); + }); + + socket.on("canceled", (data) => { + if(progressContainer.data("file-path") != data.task.file) { + return; + } + + progressContainer.pushNewMessage(data.message); + $(".stop-transcoding").hide(); + $(".show-transcodate-form").show(); + $(".transcodate-form-container .run-transcodate").show(); + }); + + $(".transcodate-form-container .run-transcodate").on("click", async () => { + const formContiner = $(".transcodate-form-container"); + const fields = formContiner.find("[name]"); + $(".transcodate-form-container .run-transcodate").hide(); + + 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(); + $(".transcodate-form-container .run-transcodate").show(); + 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); + } + }); + + $(".stop-transcoding").on("click", function(){ + stopTranscoding($(this).data("task_id")); + }); +} + +function stopTranscoding(taskId) { + if(taskId && taskId != "undefined") { + $(".stop-transcoding").hide(); + + $.getJSON(`/stop-transcoding?task_id=${taskId}`, function(resp) { + if(resp.status == "stoped" && resp.task_id == taskId) { + processContainer.slideUp(); + setTimeout(() => processContainer.html(""), 200); + } + }); + } +} + +$(document).ready(function() { + singleTranscodingInit(); +}); \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..872d05b --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,91 @@ +const socket = io(); + +function makeProgressBar(props) { + let html = ` + + + + + `; + + return html; +} + +function initSingleCircleProgressBar(view) { + if(view.alreadyInitProgressBars) { + return ; + } + + $(view).html(makeProgressBar({ + "trackColor": view.dataset.barColor + "33", + "fillColor": view.dataset.barColor, + "strokeWidth": view.dataset.barStroke, + "size": view.dataset.barSize + })); + + setProgressBarVal(view, view.dataset.value); + + const targetElement = view; + + const observer = new MutationObserver((mutationsList) => { + for (const mutation of mutationsList) { + if (mutation.type === "attributes" && mutation.attributeName.startsWith("data-")) { + const changedAttr = mutation.attributeName; + const newValue = targetElement.getAttribute(changedAttr); + + setProgressBarVal(targetElement, newValue); + } + } + }); + + observer.observe(targetElement, { + attributes: true, + attributeFilter: ["data-value"], + }); + + view.alreadyInitProgressBars = true; +} + +function initCircleProgressBars() { + $(".circle-progress-bar[data-value]").each(function(){ + initSingleCircleProgressBar(view); + }); +} + +function setProgressBarVal(target, percent) { + const circle = target.querySelector(".circle-progress"); + const radius = circle.r.baseVal.value; + const circumference = 2 * Math.PI * radius; + const offset = circumference - (percent / 100) * circumference; + + circle.style.strokeDasharray = `${circumference - offset} ${circumference}`; +} + +$(document).ready(function() { + $(".duration-to-minutes[data-duration-in-seconds]").each(function() { + const durationInSec = parseInt($(this).data("duration-in-seconds")); + const durationInMinutes = Math.floor(durationInSec / 60); + const leftSeconds = Math.floor(durationInSec - durationInMinutes * 60); + $(this).text(`${durationInMinutes}m ${leftSeconds}s`); + }); + + initCircleProgressBars(); +}); \ No newline at end of file diff --git a/static/js/lib.js b/static/js/lib.js new file mode 100644 index 0000000..d722c6d --- /dev/null +++ b/static/js/lib.js @@ -0,0 +1,5 @@ +function getSingleMediaFileInfo(filepath, callback) { + $.getJSON(`/single-json?path=${ filepath }`, function(resp) { + callback(resp); + }); +} \ No newline at end of file diff --git a/static/js/media.js b/static/js/media.js deleted file mode 100644 index 460a935..0000000 --- a/static/js/media.js +++ /dev/null @@ -1,230 +0,0 @@ -function LSpinnerShow() { - $(".loading-spinner-container").removeClass("d-none"); -} - -function LSpinnerHide() { - $(".loading-spinner-container").addClass("d-none"); -} - -// Function to load media data -function loadMediaList() { - LSpinnerShow(); - $("#media-table").addClass("d-none"); - - const tableBody = $("#media-table tbody"); - tableBody.empty(); - - $.ajax({ - url: "/media-list", - method: "GET", - success: function(data) { - - if (data.length > 0) { - data.forEach((file, index) => { - const videoInfo = file.info.video - ? file.info.video.map((v, i) => !i ? `
  • Resolution: ${v.resolution}, Codec: ${v.codec}, Bitrate: ${v.bitrate}
  • ` : `
  • Resolution: ${v.resolution}, Codec: ${v.codec}
  • `).join("") - : "
  • No video streams
  • "; - - const audioInfo = file.info.audio - ? file.info.audio.map(a => `
  • Channels: ${a.layout}, Codec: ${a.codec}, Bitrate: ${a.bitrate}, Language: ${a.language}
  • `).join("") - : "
  • No audio streams
  • "; - - const details = ` - Video: - - Audio: - - `; - - const pathEncoded = encodeURIComponent(file.path); - - tableBody.append(` - - ${index + 1} - -
    ${file.name}
    -
    ${file.path}
    -
    ${details}
    - - ${file.size}${file.size_unit} - - `); - }); - } else { - tableBody.append(`No files found`); - } - - if(!$('#media-table_wrapper').length) { - $('#media-table').DataTable({ - columnDefs: [ - { - targets: "_all", - render: function(data, type, row, meta) { - var cell = meta.settings.aoData[meta.row].anCells[meta.col]; - if (type === 'sort' && cell) { - var orderValue = $(cell).attr('data-order'); - return orderValue !== undefined ? orderValue : data; - } - return data; - } - } - ] - }); - } - - LSpinnerHide(); - $("#media-table").removeClass("d-none"); - }, - error: function(xhr, status, error) { - LSpinnerHide(); - console.error("Error loading media list:", error); - } - }); -} - -function rescanMediaLibHandler() { - $("#do-rescan-media-lib").on("click", function(){ - $.getJSON("/media-list/clear-cache").done(function(resp){ - if(resp.status) { - return loadMediaList(); - } - - console.error("Error of rescan media library"); - // TODO: make alert bar - alert("Error of rescan media library"); - }); - }); -} - -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")) { - formContainer.slideUp(); - } else { - formContainer.slideDown(); - } - }); - - $(".close-transcodate-form-container").on("click", function(){ - 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(); - rescanMediaLibHandler(); - transcodateFormHandlers(); -}); \ No newline at end of file diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..baf2706 --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,8 @@ +function timeToSeconds(timeStr) { + let parts = timeStr.split(':'); + let hours = parseInt(parts[0], 10); + let minutes = parseInt(parts[1], 10); + let seconds = parseFloat(parts[2]); + + return hours * 3600 + minutes * 60 + seconds; +} \ No newline at end of file diff --git a/templates/components/global-transcoding-tasks.html b/templates/components/global-transcoding-tasks.html new file mode 100644 index 0000000..f79e5ec --- /dev/null +++ b/templates/components/global-transcoding-tasks.html @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/templates/components/media-list.html b/templates/components/media-list.html new file mode 100644 index 0000000..7b09573 --- /dev/null +++ b/templates/components/media-list.html @@ -0,0 +1,23 @@ +
    + Configure Directories + +
    + +
    + +
    + + + + + + + + + + + + +
    #FileSize
    + + \ No newline at end of file diff --git a/templates/components/single-transcoding.html b/templates/components/single-transcoding.html new file mode 100644 index 0000000..4698aa9 --- /dev/null +++ b/templates/components/single-transcoding.html @@ -0,0 +1,84 @@ +
    + + +
    + + + + + + diff --git a/templates/index.html b/templates/index.html index af8ba01..55f65a6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,27 +2,8 @@

    Media Library Manager

    -
    - Configure Directories - -
    - -
    - -
    - - - - - - - - - - - -
    #FileSize
    + {% include "components/media-list.html" %}
    {% include "partials/footer.html" %} \ No newline at end of file diff --git a/templates/partials/footer.html b/templates/partials/footer.html index 691287b..826f5e0 100644 --- a/templates/partials/footer.html +++ b/templates/partials/footer.html @@ -1,2 +1,3 @@ + \ No newline at end of file diff --git a/templates/partials/head.html b/templates/partials/head.html index fa7efeb..9113f41 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -5,11 +5,28 @@ Media Library Manager + - + + + + - \ No newline at end of file + + +
    +
    +
    + +
    +
    +
    + +{% include "components/global-transcoding-tasks.html" %} diff --git a/templates/single.html b/templates/single.html index 20e8f85..959df38 100644 --- a/templates/single.html +++ b/templates/single.html @@ -1,9 +1,13 @@ {% include "partials/head.html" %} - -
    -

    {{ file.name }}

    + + +
    +

    {{ file.name }}

    Path: {{ file.path }}

    + Back to mediafiles list
    @@ -12,89 +16,10 @@

    Bitrate: {{ file.info.container.bitrate }}

    -

    Duration: {{ file.info.container.duration }}

    +

    Duration:

    Size: {{ file.info.container.size }}

    -
    - -
    - - - -
    + + {% include "components/single-transcoding.html" %}
    @@ -134,16 +59,18 @@ + - + {% for audio in file.info.audio %} + diff --git a/transcodate.py b/transcodate.py index 26ca789..62967fe 100644 --- a/transcodate.py +++ b/transcodate.py @@ -1,7 +1,7 @@ import subprocess import os -def transcode_file(socketio, file_path, codec, resolution, crf, preset, cpu_used): +def transcode_file(transcoding_tasks, socketio, file_path, codec, resolution, crf, preset, cpu_used): width = resolution output_file = file_path.replace('.mkv', '_transcoded.mkv') @@ -11,6 +11,7 @@ command = [ "ffmpeg", "-i", file_path, + "-map", "0", "-c:v", codec, "-vf", f"scale={width}:-1", "-c:a", "copy", @@ -29,20 +30,67 @@ command.append(output_file); - print("\n"); print(" ".join(command)) process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + task = { + "id": "id_" + str(len(transcoding_tasks) + 1), + "file": file_path, + "output": output_file, + "command": command, + "process": process + } + + transcoding_tasks[task["id"]] = task; + for line in process.stdout: # Parse progress from ffmpeg output and send via WebSocket if "frame=" in line: - socketio.emit('progress', {"message": line.strip()}) + socketio.emit('progress', { + "task": { + "id": task["id"], + "file": task["file"], + "command": task["command"] + }, + "message": line.strip() + }) process.wait() # Step 3: Replace the original file with the transcoded file if process.returncode == 0: os.replace(output_file, file_path) - socketio.emit('completed', {"message": f"File {file_path} transcoded successfully"}) - # Optionally delete original + socketio.emit('completed', { + "task": { + "id": task["id"], + "file": task["file"], + "command": task["command"] + }, + "message": f"File {file_path} transcoded successfully" + }) + + os.rename(output_file, file_path) + del transcoding_tasks[task["id"]] else: - socketio.emit('error', {"message": f"Transcoding failed for {file_path}"}) \ No newline at end of file + if(task["id"] in transcoding_tasks): + socketio.emit('error', { + "task": { + "id": task["id"], + "file": task["file"], + "command": task["command"] + }, + "message": f"Transcoding failed for {file_path}" + }) + del transcoding_tasks[task["id"]] + else: + # canceled + socketio.emit('canceled', { + "task": { + "id": task["id"], + "file": task["file"], + "command": task["command"] + }, + "message": f"Transcoding canceled for {file_path}" + }) + + if os.path.exists(output_file): + os.remove(output_file) \ No newline at end of file
    Title Bitrate Channels CodecLanguageLang Layout
    {{ audio.title }} {{ audio.bitrate }} {{ audio.channels }} {{ audio.codec }}