From 381f8cafa7f931a7e9b7b277a7be64ce4fe0ff92 Mon Sep 17 00:00:00 2001 From: data Date: Mon, 17 Feb 2025 13:23:40 +0100 Subject: [PATCH] New Project --- app/main.py | 206 ++++++++++++++++++ app/templates/progress.html | 15 ++ app/templates/webs-ui.html | 15 ++ app/video_class.py | 53 +++++ app/webs/animation-32.png | Bin 0 -> 297 bytes app/webs/bitrate-30.png | Bin 0 -> 392 bytes app/webs/fps-32.png | Bin 0 -> 543 bytes app/webs/q-24.png | Bin 0 -> 345 bytes app/webs/speed-32.png | Bin 0 -> 543 bytes app/webs/ssd-30.png | Bin 0 -> 201 bytes app/webs/webs.css | 95 ++++++++ app/webs/webs.js | 78 +++++++ app/webs/zeit-30.png | Bin 0 -> 472 bytes .../convert_videos_to_local.desktop | 10 + .../convert_videos_to_server.desktop | 10 + kio/servicemenus/ffprobe.desktop | 10 + 16 files changed, 492 insertions(+) create mode 100755 app/main.py create mode 100644 app/templates/progress.html create mode 100644 app/templates/webs-ui.html create mode 100644 app/video_class.py create mode 100644 app/webs/animation-32.png create mode 100644 app/webs/bitrate-30.png create mode 100644 app/webs/fps-32.png create mode 100644 app/webs/q-24.png create mode 100644 app/webs/speed-32.png create mode 100644 app/webs/ssd-30.png create mode 100644 app/webs/webs.css create mode 100644 app/webs/webs.js create mode 100644 app/webs/zeit-30.png create mode 100755 kio/servicemenus/convert_videos_to_local.desktop create mode 100755 kio/servicemenus/convert_videos_to_server.desktop create mode 100755 kio/servicemenus/ffprobe.desktop diff --git a/app/main.py b/app/main.py new file mode 100755 index 0000000..6c78312 --- /dev/null +++ b/app/main.py @@ -0,0 +1,206 @@ +import asyncio +import re +import subprocess +import json + +from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from contextlib import asynccontextmanager + +import app.video_class as vc + +@asynccontextmanager +async def lifespan(_: FastAPI): + await queue_video() + yield + +app = FastAPI(lifespan=lifespan) +app.mount("/webs", StaticFiles(directory="app/webs"), name="webs") +templates = Jinja2Templates(directory="app/templates") + +queue = asyncio.Queue() +read_output_task = None +convert_task = 0 + +# Settings +language = ["ger", "eng"] +process_count = 1 + +# Test +#obj_file_test = vc.Video("/mnt/Media/21 - Spielfilme M/Star Trek/Star Trek 9 - Der Aufstand (1998) 1080p/Star Trek 9 - Der Aufstand (1998) 1080p.mkv") +#video_files = {"/mnt/Media/21 - Spielfilme M/Star Trek/Star Trek 9 - Der Aufstand (1998) 1080p/Star Trek 9 - Der Aufstand (1998) 1080p.mkv": obj_file_test} + +video_files = {} + +#pattern = r"(/[^:]+?\.(?:mkv|webm|avi|mp4))" +pattern = r"(?<=\.mkv\s|\.mp4\s|\.avi\s)|(?<=\.webm\s)" + +progress = {} +async def queue_video(): + global convert_task + for key, obj in video_files.items(): + if process_count > convert_task: + if obj.finished == 0: + convert_task += 1 + asyncio.create_task(video_convert(obj)) + +async def video_convert(obj): + global convert_task + + # Audio ------------------------------------------------------------------------------------------------------------ + command = [ + "ffprobe", "-v", + "error", + "-select_streams", + "a", "-show_entries", + "stream=index,channels,tags:stream_tags=language,tags:format=duration", + "-of", "json", + obj.source_file + ] + + json_data = {} + + try: + result = subprocess.run(command, stdout=subprocess.PIPE, text=True) + json_data = json.loads(result.stdout) + print(json_data) + + obj.duration = json_data.get("format", {"duration":999}).get("duration") + + except Exception as e: + convert_task -= 1 + await queue_video() + obj.error.append(f"ffprobe ---- {e}") + print(obj.error) + + # Konvertierung ---------------------------------------------------------------------------------------------------- + command = [ + "ffmpeg", "-y", "-i", obj.source_file, + "-map", "0:0", + "-c:v", "libsvtav1", + "-preset", "5", + "-crf", "24", + "-g", "240", + "-pix_fmt", "yuv420p", + "-svtav1-params", "tune=0:film-grain=8", + ] + + if "streams" in json_data: + i = 0 + for audio_stream in json_data["streams"]: + if audio_stream.get("tags", {}).get("language", None) in language: + command.extend([ + "-map", f"0:{audio_stream['index']}", + f"-c:a", "libopus", + f"-b:a", "320k", + f"-ac", str(audio_stream['channels']) + ]) + i += 1 + + command.extend(["-map", "0:s?", "-c:s", "copy"]) + command.append(obj.output_file) + + # Prozess + try: + process_video = await asyncio.create_subprocess_exec( + *command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + obj.progress = process_video + if process_video.returncode == 0: + obj.finished = 1 + convert_task -= 1 + await queue_video() + + except Exception as e: + convert_task -= 1 + obj.finished = 2 + await queue_video() + obj.error.append(f"ffmpeg ---- {e}") + +async def read_output(): + while True: + active_processes = [obj for obj in video_files.values() if obj.progress] + + if not active_processes: + await asyncio.sleep(10) # Kein aktives Video -> kurz warten + continue + + for obj in active_processes: + if obj.progress: + line = await obj.progress.stderr.read(2048) + + line_decoded = line.decode().strip() + print(line_decoded) + + frame = re.findall(r"frame=\s*(\d+)", line_decoded) + obj.frame = frame[0] if frame else obj.frame + + fps = re.findall(r"fps=\s*(\d+.\d+)", line_decoded) + obj.fps = fps[0] if fps else obj.fps + + q = re.findall(r"q=\s*(\d+.\d+)", line_decoded) + obj.q = q[0] if q else obj.q + + size = re.findall(r"size=\s*(\d+)", line_decoded) + obj.size = round(int(size[0]) / 1024, 2) if size else obj.size + + time = re.findall(r"time=\s*(\d+:\d+:\d+)", line_decoded) + time_v = time[0] if time else time + obj.time = obj.time_in_sec(time_v) + + bitrate = re.findall(r"bitrate=\s*(\d+)", line_decoded) + obj.bitrate = round(int(bitrate[0]) / 1024, 2) if bitrate else obj.bitrate + + speed = re.findall(r"speed=\s*(\d+\.\d+)", line_decoded) + obj.speed = speed[0] if speed else obj.speed + + json_data = json.dumps(obj.to_dict()) + await queue.put(json_data) + +@app.post("/") +async def receive_video_file(data: dict): + file_paths = data.get("files", [])[0] + var_list_file = re.split(pattern, file_paths) + + for path in var_list_file: + obj_file = vc.Video(path.strip()) + video_files.update({path: obj_file}) + + await queue_video() + #Test + print(video_files) + +@app.get("/progress", response_class=HTMLResponse) +async def display_paths(request: Request): + return templates.TemplateResponse("progress.html", {"request": request, "videos": video_files}) + +@app.get("/webs-ui", response_class=HTMLResponse) +async def display_paths(request: Request): + return templates.TemplateResponse("webs-ui.html", {"request": request, "videos": video_files}) + +@app.websocket("/ws") +async def websocket_v(websocket: WebSocket): + global read_output_task + + await websocket.accept() + + if read_output_task is None or read_output_task.done(): + read_output_task = asyncio.create_task(read_output()) + + try: + while True: + message = await queue.get() + await websocket.send_text(message) + except WebSocketDisconnect: + print("WebSocket disconnected") # Optional: Logging + except Exception as e: + print(f"WebSocket error: {e}") # Fehlerbehandlung + finally: + await websocket.close() + + diff --git a/app/templates/progress.html b/app/templates/progress.html new file mode 100644 index 0000000..7d655a6 --- /dev/null +++ b/app/templates/progress.html @@ -0,0 +1,15 @@ + + + + + Video Liste + + +

Vorhandene Videos

+ + + \ No newline at end of file diff --git a/app/templates/webs-ui.html b/app/templates/webs-ui.html new file mode 100644 index 0000000..0a51bc8 --- /dev/null +++ b/app/templates/webs-ui.html @@ -0,0 +1,15 @@ + + + + + + Live Fortschritt + + + +

Video-Konvertierung Fortschritt

+
+ + + + diff --git a/app/video_class.py b/app/video_class.py new file mode 100644 index 0000000..6193aa8 --- /dev/null +++ b/app/video_class.py @@ -0,0 +1,53 @@ +import os + + +class Video: + def __init__(self, path): + self.id = id(self) + self.source_file = path + self.output_file = f"{path.rsplit(".", 1)[0]}.webm" + self.frame = None + self.fps = None + self.q = None + self.size = None + self.time = 0 + self.bitrate = None + self.speed = None + self.finished = 0 + self.progress = None + self.duration = 1 + self.loading = 0 + self.error = [] + + def to_dict(self): + self.calc_loading() + + return { + "source": os.path.basename(self.source_file), + "target": os.path.basename(self.output_file), + "path": os.path.dirname(self.source_file), + "id": self.id, + "frame": self.frame, + "fps": self.fps, + "q": self.q, + "size": self.size, + "time": self.time, + "bitrate": self.bitrate, + "speed": self.speed, + "finished": self.finished, + "duration": self.duration, + "loading": self.loading + } + + @staticmethod + def time_in_sec(time): + if time != 0: + h_m_s = str(time).split(":") + time_in_s = int(h_m_s[0]) * 3600 + int(h_m_s[1]) * 60 + int(h_m_s[2]) + else: + time_in_s = 0 + + return time_in_s + + def calc_loading(self): + self.loading = round(self.time / float(self.duration) * 100, None) diff --git a/app/webs/animation-32.png b/app/webs/animation-32.png new file mode 100644 index 0000000000000000000000000000000000000000..4392cc4abd9cc2ad0f8151b0a9aaed89a94298b6 GIT binary patch literal 297 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND0th` z#WAE}&fDpGIS(5MxXD+reOKgG@n)HHY_3?>Yn=)5+u3-huo^jt1q2AJ*uc#8+s{$& z%fUveyS61(wH#rKbT2T53Fro7GFlzbezbsfL)Rn!1%8|pT(>c7cqDd*QGLgp#PhnW z4Z%JqtXUiSKSup<`06lO=)foUn@kOBSgQ|kJo=KI5XP{9*{+Mzl7I46=H!{&2~+s2 zPKCRAuWe+QFi zu0YFNf`W>OS{ZZ$qK$uzL@pfRBDsIc6(V_InsYv8<~uWo5ktrdF7b{Yc2KXfU=4fd z4~RRoswg^+gHYrdtC++*Zo>HknpsJ@I1Ubq(hkWVG5zgs0!QJ#heleGuMXD>7TroX zolViiGd>Tx-LymtLGY6IG+4q%h{YW36kbE_cm$bGcb2h{bD_Cdx2YH#L4TDdXoPvh zqQfw!Br|x*d%6MCaDvk;Q8R2}ocubBAl}rqgwuVDsel&lQ~qYVrQZfCE_!Fl|Ds|pZMCaRGRMdG?qQfkwSRCt{2mdh>#K@^6+J+oyJmyHO9g(o0v@eUH#ouws-iIsbV z7ZGF_f(LNf&5X4Nz>cJ)l7EuQq`SKMLL!{xPdZ(7&i|jPQ>P04&EIB$7a)kii{|EP zy}+jjc@v-V$<3A%kT+p`ey7a<*T8#;Z^W=79mAO&A0gfD2Cn)i42XF-|pL3>X3SfJb0eFyFhtFfb0(V`9CA2?!jx0Xl)h zmVR&`*ysO1b8ll}dJCv2;AMh6uE?j6hk!9Rfr+dFD%x|OHf)M`v}HR=j+OHg zFsDr7$nSs-Rk!_r8(7RDU;ucE7&`&FfIgAi$GC|H9nsVR3cxyW95U8z`Syj39|3EM zkx9S`uoEMoCuDp_zJ>4Ff>Vhw%VmtLqJE*7Jd literal 0 HcmV?d00001 diff --git a/app/webs/q-24.png b/app/webs/q-24.png new file mode 100644 index 0000000000000000000000000000000000000000..a83610cd2cdbeb93ea6e5d0d1ca47372516dce94 GIT binary patch literal 345 zcmV-f0jBM|b~K|EsOxjVGZF41pz($~gdPj|R>GeF=KYdcups&=LG> zCYxY%tRP0L4CLjs#aki4i@?e%!!`62$M6fj0&V3um0Mqfc|TL!rmw(6IdvV}uHue- z1=JB7tLO_RMM|VQ>(zv64`o8+yw@AesCMPxfU^*1tAA~0_!_MIup~I rC{!hzrHC!$xgurBWU9<`y+II8B3?kah!E9)00000NkvXXu0mjfg8PXQ literal 0 HcmV?d00001 diff --git a/app/webs/speed-32.png b/app/webs/speed-32.png new file mode 100644 index 0000000000000000000000000000000000000000..1fa8b2a6446859ad7b7661b2e95a78f2946935bb GIT binary patch literal 543 zcmV+)0^t3LP)RCt{2mdz^$Q5431u@oB-A)$OM@NbY-56TmKT z0laG1^A0c$RR2ZbDqsDMv3Z zWZdkr>y!u!-r=!r5$AcFOE}2yVJOZe9Fy_7=1KZMA#moj{$3}2S5xKm)!!dCzQ002ovPDHLkV1iQB=P&>O literal 0 HcmV?d00001 diff --git a/app/webs/ssd-30.png b/app/webs/ssd-30.png new file mode 100644 index 0000000000000000000000000000000000000000..e039be8a99c3966d43ae4e26ef6bb3465c351eb3 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3oCO|{#S9GG!XV7ZFl&wkP_Ww5 z#WAE}&fBR6IU5XkT<`PrDOYW`XgHUDcX3coe=7?!>umP_6C6G*RME81w=Q6QxmZcS z%U|%~fi15tNvJ1x7CwIB-^(w)&+iALpy9ofJ$v^ZO-PyNYu;OE&X-{HVsiDISxq08 wS&5!Ku&5)t(r~MS`1-CLQ^g-w|B;tl_4=&2&(@}3pfeaeUHx3vIVCg!0B+SslK=n! literal 0 HcmV?d00001 diff --git a/app/webs/webs.css b/app/webs/webs.css new file mode 100644 index 0000000..cdcd712 --- /dev/null +++ b/app/webs/webs.css @@ -0,0 +1,95 @@ +body { + background-color: #121212; + color: #ffffff; + font-family: Arial, sans-serif; + text-align: center; + margin: 0; + padding: 20px; +} + +h1 { + font-size: 24px; +} + +#videos { + display: flex; + flex-direction: column; + gap: 15px; + max-width: 1500px; + margin: auto; +} + +.video { + background: #1e1e1e; + padding: 15px; + border-radius: 8px; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; +} + +.video-header { + font-weight: bold; + font-size: 16px; + margin-bottom: 5px; +} + +.progress-container { + width: 100%; + background: #333; + border-radius: 5px; + margin-top: 10px; + height: 10px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + width: 0%; + background: linear-gradient(90deg, #4caf50, #00c853); + transition: width 0.5s ease-in-out; +} + +.finished { + color: #4caf50; + font-weight: bold; +} + +.table_video_info{ + border: None; + width: 100%; +} + +th, td { + width: 16%; /* Jede Zelle nimmt 33% der Breite der Tabelle ein */ + padding: 16px; + text-align: left; /* Optional: Text ausrichten */ +} + +.icons { + width: 25px; + float: right; +} + +.label { + text-align: left; + vertical-align: middle; +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #3498db; /* Blue */ + border-radius: 50%; + width: 20px; + height: 20px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.path { + font-size: 12px; +} \ No newline at end of file diff --git a/app/webs/webs.js b/app/webs/webs.js new file mode 100644 index 0000000..73113e1 --- /dev/null +++ b/app/webs/webs.js @@ -0,0 +1,78 @@ +let ws = new WebSocket("ws://127.0.0.1:8000/ws"); +let videoQueue = {}; // Hier speichern wir alle laufenden Videos + +ws.onmessage = function(event) { + try { + let message = JSON.parse(event.data); + + // Falls das Video noch nicht existiert, erstelle ein neues Element + if (!videoQueue[message.id]) { + videoQueue[message.id] = createVideoElement(message.id, message.source, message.target, message.path); + } + + // Update der UI + updateVideoElement(message.id, message); + } catch (e) { + console.error("Fehler beim Parsen der WebSocket-Nachricht:", e, event.data); + } +}; + +function createVideoElement(id, source, target, path) { + let container = document.createElement("div"); + container.className = "video"; + container.id = id; + + container.innerHTML = ` +
${source}    ➝    ${target}
+
+
+
+
+
${path}
+ + + + + + + + + + + + + + + + +
0 Anz
0 fps
0 Q
0 MB
0 Mb/s
0 x
+
+ `; + + document.getElementById("videos").appendChild(container); + return container; +} + +function updateVideoElement(id, data) { + let container = document.getElementById(id); + if (!container) return; + + container.querySelector(".frame").textContent = data.frame || '---'; + container.querySelector(".fps").textContent = data.fps || '---'; + container.querySelector(".q").textContent = data.q || '---'; + container.querySelector(".size").textContent = data.size || '---'; + container.querySelector(".bitrate").textContent = data.bitrate || '---'; + container.querySelector(".speed").textContent = data.speed || '---'; + + let progressBar = container.querySelector(".progress-bar"); + let progress = data.loading; // Annahme: `loading` kommt als Zahl von 0 bis 100 + progressBar.style.width = progress + "%" + + if (data.finished) { + progressBar.style.background = "#4caf50"; + container.querySelector(".finished").textContent = "Fertig"; + } else { + progressBar.style.background = "linear-gradient(90deg, #4caf50, #00c853)"; + container.querySelector(".finished").textContent = "Läuft"; + } +} diff --git a/app/webs/zeit-30.png b/app/webs/zeit-30.png new file mode 100644 index 0000000000000000000000000000000000000000..bb13515e6d66109ece958c61179a715084631c5e GIT binary patch literal 472 zcmV;}0Vn>6P)%;til zaMZG&T0!3iK7@M-!jFIWFTiu0!M-kTFk{>~CYN_K9iNkTRP@*b75tgvR9>4dlNKit z$>xNT3j0Kb-iNp|>Q-A!E4~0cWEBn*EgS?oD)>n-oSQrz+n$GE4^;4XfK6aYqt809 z2V7cn*{6nyFzlI7Ofl>Q;@U0Hi06Vawyz@d3Vdt4eOKr4KHkoO5Bvg*9H=+^XuCZC O0000