diff --git a/app.py b/app.py index 363b79b..f90bfb9 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ from flask import Flask, render_template, request, jsonify import os 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 mediascan import scan_medialib, 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 @@ -14,12 +14,21 @@ CONFIG_FILE = 'config.json' config = load_config(CONFIG_FILE) +GStorage = { + "scaning_state": "inaction", # values: inaction, inprogress + "outdated_cache": [] +} + transcoding_tasks = {} + +# ROUTING + @app.route('/') def index(): return render_template('index.html') + @app.route("/single") def single_media(): path = request.args.get("path", "") @@ -31,6 +40,7 @@ return render_template('single.html', file=media_file) + @app.route("/single-json") def single_media_json(): path = request.args.get("path", "") @@ -41,30 +51,26 @@ 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"])}) + media_remove_cache(config["cache_dir"], GStorage) + scan_medialib(config, GStorage, socketio) + return jsonify({"status": True}) + @app.route('/media-list', methods=['GET']) def media_list(): filtered_files = media_get_from_cache(config["cache_dir"]) if filtered_files == None: - media_files = list_media_files(config.get("directories", [])) + filtered_files = scan_medialib(config, GStorage, socketio) - allowed_formats = config.get("allowed_formats", []) - filtered_files = filter_media_files(media_files, allowed_formats) - - # Add detailed media info - for file in filtered_files: - file["size"], file["size_unit"], file["size_bytes"] = human_readable_size(file["size"]) - file_info = get_media_info_with_ffprobe(file["path"]) - if file_info: - file["info"] = file_info - else: - file["info"] = {"error": "Failed to retrieve media info"} + if GStorage["scaning_state"] == "inprogress" and not len(filtered_files): + response = {"status": "scaning", "data": []}; + else: + response = {"status": True, "data": filtered_files} + return jsonify(response) - media_create_cache(config["cache_dir"], filtered_files) - return jsonify(filtered_files) @app.route('/process-media', methods=['POST']) def process_media(): @@ -108,6 +114,7 @@ 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") @@ -120,6 +127,7 @@ return jsonify({"status": "error", "message": "Task not found"}), 404 + @app.route('/configure', methods=['GET', 'POST']) def configure(): if request.method == 'POST': @@ -131,5 +139,6 @@ return jsonify({"status": "success"}) return jsonify(config) + if __name__ == '__main__': app.run(debug=True) diff --git a/config.json b/config.json index 294b168..45fe8a5 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,6 @@ { "directories": [ - "/home/gmikcon/media-storage/firstraid5", - "/home/gmikcon/media-storage/NetTera1", - "/home/gmikcon/media-storage/secraid5_storage" + "/home/gmikcon/media-storage/NetTera1" ], "allowed_formats": [".mp4", ".m4p", ".m4v", ".mkv", ".webm", ".flv", ".vob", ".ogv", ".amv", ".mng", ".avi", ".mov", ".viv"], "cache_dir": "cache", diff --git a/mediascan.py b/mediascan.py index e27f3d1..b9188f7 100644 --- a/mediascan.py +++ b/mediascan.py @@ -3,6 +3,7 @@ import ffmpeg import subprocess + def load_config(config_file): if os.path.exists(config_file): with open(config_file, 'r') as f: @@ -10,16 +11,23 @@ else: return {"directories": []} + def save_config(config_file, config): with open(config_file, 'w') as f: json.dump(config, f, indent=4) + def media_create_cache(cache_dir, data): filepath = f"{cache_dir}/mediascan.json" with open(filepath, 'w') as f: json.dump(data, f, indent=2) -def media_remove_cache(cache_dir): + +def media_remove_cache(cache_dir, GStorage): + GStorage["outdated_cache"] = media_get_from_cache(cache_dir) + if GStorage["outdated_cache"] == None: + GStorage["outdated_cache"] = [] + filepath = f"{cache_dir}/mediascan.json" if not os.path.exists(filepath): return True @@ -150,6 +158,7 @@ print(f"Directory does not exist: {directory}") return media_files + def filter_media_files(media_files, allowed_formats): """ Фильтрует список файлов по допустимым форматам. @@ -164,6 +173,7 @@ ] return filtered_files + def human_readable_size(size_bytes): if size_bytes < 1024: return [f"{size_bytes}", "B", f"{size_bytes}"] @@ -174,6 +184,7 @@ else: return [f"{size_bytes / 1024**3:.2f}", "GB", f"{size_bytes}"] + def get_single_media_by_path(path): media_info = get_media_info_with_ffprobe(path) size, size_unit, size_bytes = human_readable_size(os.path.getsize(path)) @@ -186,4 +197,44 @@ "info": media_info } - return media_file \ No newline at end of file + return media_file + + +def bg_scan_medialib(socketio, config, GStorage): + GStorage["scaning_state"] = "inprogress"; + media_files = list_media_files(config.get("directories", [])) + + allowed_formats = config.get("allowed_formats", []) + filtered_files = filter_media_files(media_files, allowed_formats) + + # Add detailed media info + for file in filtered_files: + file["size"], file["size_unit"], file["size_bytes"] = human_readable_size(file["size"]) + file_info = get_media_info_with_ffprobe(file["path"]) + if file_info: + file["info"] = file_info + else: + file["info"] = {"error": "Failed to retrieve media info"} + + GStorage["scaning_state"] = "inaction" + + media_create_cache(config["cache_dir"], filtered_files) + + socketio.emit("medialib-scaning-complete", { + "data": filtered_files + }); + pass + + +def scan_medialib(config, GStorage, socketio): + if GStorage["scaning_state"] == "inprogress": + return GStorage["outdated_cache"] + + socketio.start_background_task( + target = bg_scan_medialib, + socketio=socketio, + config = config, + GStorage = GStorage + ) + + pass \ No newline at end of file diff --git a/scss/pages/single.scss b/scss/pages/single.scss new file mode 100644 index 0000000..92e47ed --- /dev/null +++ b/scss/pages/single.scss @@ -0,0 +1,5 @@ +#page-single { + #single-transcoding-progress { + + } +} \ No newline at end of file diff --git a/static/js/components/global-transcoding-tasks.js b/static/js/components/global-transcoding-tasks.js index e4444b1..478a022 100644 --- a/static/js/components/global-transcoding-tasks.js +++ b/static/js/components/global-transcoding-tasks.js @@ -56,18 +56,14 @@ 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]; - } - + let time = getTimeFromTranscodingProgressMessage(data.message); const mInf = getMediaInfo(data.task.file); if(!mInf) { return; } - const progressPercent = Math.floor(timeToSeconds(time) / parseInt(mInf.info.container.duration) * 100); + const progressPercent = calcTranscodingProgress(time, mInf.info.container.duration); taskContainer.find(".file .progress-percent").text(`- ${ progressPercent }%`); taskContainer.find(".file .circle-progress-bar")[0].dataset.value = progressPercent; diff --git a/static/js/components/media-list.js b/static/js/components/media-list.js index bef1e85..58dfc87 100644 --- a/static/js/components/media-list.js +++ b/static/js/components/media-list.js @@ -1,102 +1,129 @@ function LSpinnerShow() { - $(".loading-spinner-container").removeClass("d-none"); + $(".loading-spinner-container").removeClass("d-none"); } function LSpinnerHide() { - $(".loading-spinner-container").addClass("d-none"); + $(".loading-spinner-container").addClass("d-none"); } // Function to load media data function loadMediaList() { - LSpinnerShow(); - $("#media-table").addClass("d-none"); + LSpinnerShow(); + $("#media-table").addClass("d-none"); + $("#media-table_wrapper").addClass("d-none"); - const tableBody = $("#media-table tbody"); - tableBody.empty(); + const tableBody = $("#media-table tbody"); + tableBody.empty(); - $.ajax({ - url: "/media-list", - method: "GET", - success: function(data) { + $.ajax({ + url: "/media-list", + method: "GET", + success: function(resp) { + if(resp.status == "scaning" && !resp.data.length) { + return; + } - if (data.length > 0) { - data.forEach((file, index) => { - const videoInfo = file.info.video - ? file.info.video.map((v, i) => !i ? `