Compare commits
2 commits
eadd03facc
...
4cce6c8cf8
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cce6c8cf8 | |||
| 381f8cafa7 |
206
app/main.py
Executable 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()
|
||||||
|
|
||||||
|
|
||||||
15
app/templates/progress.html
Normal 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>
|
||||||
15
app/templates/webs-ui.html
Normal 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
|
|
@ -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
|
After Width: | Height: | Size: 297 B |
BIN
app/webs/bitrate-30.png
Normal file
|
After Width: | Height: | Size: 392 B |
BIN
app/webs/fps-32.png
Normal file
|
After Width: | Height: | Size: 543 B |
BIN
app/webs/q-24.png
Normal file
|
After Width: | Height: | Size: 345 B |
BIN
app/webs/speed-32.png
Normal file
|
After Width: | Height: | Size: 543 B |
BIN
app/webs/ssd-30.png
Normal file
|
After Width: | Height: | Size: 201 B |
95
app/webs/webs.css
Normal 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
|
|
@ -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} ➝ ${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
|
After Width: | Height: | Size: 472 B |
10
kio/servicemenus/convert_videos_to_local.desktop
Executable 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
|
||||||
10
kio/servicemenus/convert_videos_to_server.desktop
Executable 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
|
||||||
10
kio/servicemenus/ffprobe.desktop
Executable 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
|
||||||