Compare commits
No commits in common. "4cce6c8cf8df219571212d3524a7e25b24ead073" and "eadd03faccc0d6abee5119d0737bff1c031e27c5" have entirely different histories.
4cce6c8cf8
...
eadd03facc
206
app/main.py
|
|
@ -1,206 +0,0 @@
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
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)
|
|
||||||
|
Before Width: | Height: | Size: 297 B |
|
Before Width: | Height: | Size: 392 B |
|
Before Width: | Height: | Size: 543 B |
|
Before Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 543 B |
|
Before Width: | Height: | Size: 201 B |
|
|
@ -1,95 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
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} ➝ ${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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 472 B |
|
|
@ -1,10 +0,0 @@
|
||||||
[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
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
[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
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
[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
|
|
||||||