New Project

This commit is contained in:
Eduard Wisch 2025-02-17 13:23:40 +01:00
commit 381f8cafa7
16 changed files with 492 additions and 0 deletions

206
app/main.py Executable file
View file

@ -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()

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Video Liste</title>
</head>
<body>
<h1>Vorhandene Videos</h1>
<ul>
{% for key, video in videos.items() %}
<li>{{ video.source_file }} <b>{{video.finished}}</b></li>
{% endfor %}
</ul>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Fortschritt</title>
<link rel="stylesheet" href="/webs/webs.css">
</head>
<body>
<h1>Video-Konvertierung Fortschritt</h1>
<div id="videos"></div>
<script src="/webs/webs.js"></script>
</body>
</html>

53
app/video_class.py Normal file
View file

@ -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)

BIN
app/webs/animation-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

BIN
app/webs/bitrate-30.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

BIN
app/webs/fps-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

BIN
app/webs/q-24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

BIN
app/webs/speed-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

BIN
app/webs/ssd-30.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

95
app/webs/webs.css Normal file
View file

@ -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;
}

78
app/webs/webs.js Normal file
View file

@ -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 = `
<div class="video-header">${source}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;${target}</div>
<div class="progress-container">
<div class="progress-bar"></div>
</div>
<div class="video-info">
<div><span class="path">${path}</span></div>
<table class="table_video_info">
<tr>
<th><img src="/webs/animation-32.png" class="icons"></th><th class="label"><div title="Anzahl Frames"><span class="frame">0</span> Anz</div></th>
<th><img src="/webs/fps-32.png" class="icons"></th><th class="label"><div title="Frames / Sekunde"><span class="fps">0</span> fps</div></th>
<th><img src="/webs/q-24.png" class="icons"></th><th class="label"><div title="Quantizer"><span class="q">0 </span> Q</div></th>
</tr>
<tr>
<th><img src="/webs/ssd-30.png" class="icons"></th><th class="label"><div title="Dateigröße"><span class="size">0 </span> MB</div></th>
<th><img src="/webs/bitrate-30.png" class="icons"></th><th class="label"><div title="Bitrate"><span class="bitrate">0 </span> Mb/s</div></th>
<th><img src="/webs/speed-32.png" class="icons"></th><th class="label"><div title="Speed"><span class="speed">0</span> x</div></th>
</tr>
<tr>
<th></th><th></th>
<th></th><th></th>
<th></th><th><div class="loader"><span class="finished"></span></div></th>
</tr>
</table>
</div>
`;
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";
}
}

BIN
app/webs/zeit-30.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

View file

@ -0,0 +1,10 @@
[Desktop Entry]
Type=Service
ServiceTypes=KonqPopupMenu/Plugin
MimeType=video/*
Actions=convert_videos
[Desktop Action convert_videos]
Name=Convert Local Videos Server
Exec=curl -X POST -H "Content-Type: application/json" -d '{"files": ["%F"]}' http://localhost:8000/
Icon=video-x-generic

View file

@ -0,0 +1,10 @@
[Desktop Entry]
Type=Service
ServiceTypes=KonqPopupMenu/Plugin
MimeType=video/*
Actions=convert_videos
[Desktop Action convert_videos]
Name=Convert Videos Server
Exec=curl -X POST -H "Content-Type: application/json" -d '{"files":["%U"]}' http://192.168.155.110:8000/
Icon=video-x-generic

View file

@ -0,0 +1,10 @@
[Desktop Entry]
Type=Service
ServiceTypes=KonqPopupMenu/Plugin
MimeType=video/*;audio/*;
Actions=FFProbeInfo;
[Desktop Action FFProbeInfo]
Name=FFprobe Info anzeigen
Icon=dialog-information
Exec=ffprobe -v error -show_format -show_streams "%f" | kdialog --textbox - --title "FFprobe Ausgabe" --geometry 800x600