Compare commits
No commits in common. "main" and "9cbce7415d2429d2132afb3b1d985c09f6ddd7f2" have entirely different histories.
main
...
9cbce7415d
165
.gitignore
vendored
|
|
@ -1,165 +0,0 @@
|
||||||
# ---> Python
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
/app/cfg/media_path.yaml
|
|
||||||
109
README.md
|
|
@ -1,109 +0,0 @@
|
||||||
|
|
||||||
# Video-Konvertierungsprogramm
|
|
||||||
|
|
||||||
Dies ist ein einfaches Video-Konvertierungsprogramm, das eine Weboberfläche zur Verfügung stellt, um Videos in verschiedene Formate zu konvertieren. Das Backend läuft auf Python und verwendet WebSockets, um mit der Benutzeroberfläche zu kommunizieren und den Status von Konvertierungen zu verfolgen.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Weboberfläche** zur Anzeige aktueller Konvertierungen, Warteschlangen und Status.
|
|
||||||
- **Echtzeit-Kommunikation** via WebSocket zwischen Frontend und Backend.
|
|
||||||
- **Automatische Konvertierung** bei Dateipfadänderungen (optional, basierend auf den Einstellungen).
|
|
||||||
- **Statistiken** zu konvertierten Videos.
|
|
||||||
- Unterstützung für verschiedene Videoformate und Codecs.
|
|
||||||
|
|
||||||
## Anforderungen
|
|
||||||
|
|
||||||
- Python 3.8+
|
|
||||||
- Abhängigkeiten:
|
|
||||||
- `aiohttp` – Für den HTTP-Server
|
|
||||||
- `websockets` – Für die WebSocket-Kommunikation
|
|
||||||
- Weitere Abhängigkeiten wie z. B. `ffmpeg` für die Video-Konvertierung
|
|
||||||
|
|
||||||
### Installationsanweisungen
|
|
||||||
|
|
||||||
1. **Klonen des Repositories:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/dein-benutzername/video-konverter.git
|
|
||||||
cd video-konverter
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Installiere die Abhängigkeiten:**
|
|
||||||
|
|
||||||
Wenn du noch keine virtuelle Umgebung eingerichtet hast, erstelle eine:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate # Auf Windows: venv\Scriptsctivate
|
|
||||||
```
|
|
||||||
|
|
||||||
Dann installiere alle benötigten Abhängigkeiten:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Installiere ffmpeg:**
|
|
||||||
|
|
||||||
Für die Video-Konvertierung benötigst du `ffmpeg`. Installiere es je nach deinem Betriebssystem:
|
|
||||||
|
|
||||||
- **Ubuntu/Debian:**
|
|
||||||
```bash
|
|
||||||
sudo apt-get install ffmpeg
|
|
||||||
```
|
|
||||||
- **macOS (via Homebrew):**
|
|
||||||
```bash
|
|
||||||
brew install ffmpeg
|
|
||||||
```
|
|
||||||
- **Windows:**
|
|
||||||
Lade `ffmpeg` von [ffmpeg.org](https://ffmpeg.org/download.html) herunter und stelle sicher, dass es im Systempfad verfügbar ist.
|
|
||||||
|
|
||||||
## Nutzung
|
|
||||||
|
|
||||||
1. **Starte den Server:**
|
|
||||||
|
|
||||||
Der WebSocket-Server und der HTTP-Server laufen auf demselben Event-Loop. Um beide zu starten, führe einfach das Python-Skript aus:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Der HTTP-Server ist auf `http://localhost:8080` erreichbar, und die WebSocket-Verbindung läuft unter `ws://localhost:8000`.
|
|
||||||
|
|
||||||
2. **Weboberfläche aufrufen:**
|
|
||||||
|
|
||||||
Öffne deinen Webbrowser und gehe zu `http://localhost:8080`, um die Benutzeroberfläche des Video-Konverters zu sehen. Du kannst Videos hochladen und deren Konvertierungsstatus in Echtzeit verfolgen.
|
|
||||||
|
|
||||||
## Funktionen der Weboberfläche
|
|
||||||
|
|
||||||
- **Aktuelle Konvertierungen**: Zeigt alle laufenden und abgeschlossenen Konvertierungen an.
|
|
||||||
- **Warteschlange**: Zeigt an, welche Videos noch konvertiert werden müssen.
|
|
||||||
- **Start/Pause/Stop**: Ermöglicht es, die Konvertierung zu steuern (je nach Implementierung).
|
|
||||||
- **Statistiken**: Zeigt Informationen zu den konvertierten Videos (z. B. Dateigröße, Format, etc.).
|
|
||||||
|
|
||||||
## Backend
|
|
||||||
|
|
||||||
Das Backend ist für die Verwaltung der Video-Konvertierungen zuständig und kommuniziert über WebSockets mit der Frontend-Weboberfläche.
|
|
||||||
|
|
||||||
### WebSocket-Server
|
|
||||||
|
|
||||||
Der WebSocket-Server wartet auf Verbindungen und sendet Updates zu den Konvertierungen an die Clients. Die Kommunikation erfolgt mit JSON-Nachrichten. Der Server unterstützt folgende Nachrichtenarten:
|
|
||||||
|
|
||||||
- **Datenpfad** (`data_path`): Gibt den Pfad der zu konvertierenden Datei an.
|
|
||||||
- **Befehle** (`data_command`): Für zukünftige Erweiterungen von Steuerbefehlen.
|
|
||||||
|
|
||||||
## Konfiguration
|
|
||||||
|
|
||||||
Die Konfiguration des Programms erfolgt über eine YAML-Datei (z. B. `config.yaml`), die Parameter wie den Serverport, den Pfad zu den Konvertierungswerkzeugen (z. B. `ffmpeg`) und automatische Startoptionen für die Konvertierungen enthält.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- **Problem:** Der WebSocket-Server startet nicht.
|
|
||||||
- **Lösung:** Überprüfe, ob der Port (Standard: `8000`) bereits von einer anderen Anwendung verwendet wird.
|
|
||||||
|
|
||||||
- **Problem:** Videos werden nicht konvertiert.
|
|
||||||
- **Lösung:** Stelle sicher, dass `ffmpeg` korrekt installiert und im Systempfad verfügbar ist.
|
|
||||||
|
|
||||||
## Lizenz
|
|
||||||
|
|
||||||
Dieses Projekt ist unter der MIT-Lizenz lizenziert – siehe [LICENSE](LICENSE) für Details.
|
|
||||||
16
__main__.py
|
|
@ -1,16 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from platform import python_version
|
|
||||||
|
|
||||||
from app.main_server import Server
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print(python_version())
|
|
||||||
obj_server = Server()
|
|
||||||
|
|
||||||
try:
|
|
||||||
asyncio.run(obj_server.start_server())
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logging.warning("Server wurde manuell beendet")
|
|
||||||
except Exception as e:
|
|
||||||
logging.critical(f"Global error: {e}")
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
log_file: "server.log"
|
|
||||||
log_level: DEBUG
|
|
||||||
log_rotation: time
|
|
||||||
path_file: "media_path.yaml"
|
|
||||||
server_ip: "0.0.0.0"
|
|
||||||
extern_http_url: localhost:8080
|
|
||||||
webserver_port: 8080
|
|
||||||
webserver_https: 0
|
|
||||||
extern_websocket_url: localhost:8000
|
|
||||||
websocket_port: 8000
|
|
||||||
websocket_https: 0
|
|
||||||
task_max: 1
|
|
||||||
autostart: true
|
|
||||||
subtitle:
|
|
||||||
language:
|
|
||||||
- ger
|
|
||||||
- eng
|
|
||||||
blacklist:
|
|
||||||
- hdmv_pgs_subtitle
|
|
||||||
- dvd_subtitle
|
|
||||||
audio:
|
|
||||||
language:
|
|
||||||
- ger
|
|
||||||
- eng
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
import logging
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from app.class_file_convert_read_out import Process
|
|
||||||
from app.class_media_file_stat import Stat
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from app.class_media_file import Media
|
|
||||||
|
|
||||||
class Convert:
|
|
||||||
def __init__(self, websocket, cfg, obj_path):
|
|
||||||
self.yaml = cfg
|
|
||||||
self.obj_path = obj_path
|
|
||||||
self.obj_websocket = websocket
|
|
||||||
|
|
||||||
self.active_tasks = set()
|
|
||||||
self.active_process = set()
|
|
||||||
|
|
||||||
async def snake_waiting(self):
|
|
||||||
while True:
|
|
||||||
if len(self.active_tasks) < self.yaml["task_max"] and self.obj_path.count_paths(None) > 0:
|
|
||||||
for obj_id, obj in list(self.obj_path.paths.items()):
|
|
||||||
if obj.status is None:
|
|
||||||
obj.task = asyncio.create_task(self.convert_video(obj))
|
|
||||||
self.active_tasks.add(obj)
|
|
||||||
logging.info(f"Warteschlange started Auftrag - {obj.task}")
|
|
||||||
obj.status = 3
|
|
||||||
|
|
||||||
if len(self.active_tasks) >= self.yaml["task_max"]:
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(self.active_tasks) > 0:
|
|
||||||
logging.info(f"{len(self.active_tasks)} is active.")
|
|
||||||
await asyncio.sleep(500)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.obj_path.count_paths(None) == 0 and len(self.active_tasks) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
print(self.obj_path.paths)
|
|
||||||
|
|
||||||
async def convert_video(self, obj):
|
|
||||||
"""Startet die Videokonvertierung asynchron."""
|
|
||||||
|
|
||||||
obj_process = Process(self.obj_websocket)
|
|
||||||
obj_stat = Stat()
|
|
||||||
|
|
||||||
obj.convert_start = time.time()
|
|
||||||
command = self.convert_cmd(obj)
|
|
||||||
result = None
|
|
||||||
|
|
||||||
logging.info(f"Starte Konvertierung: {command}")
|
|
||||||
await self.obj_websocket.send_websocket(self.obj_path.active_path_to_dict())
|
|
||||||
await self.obj_websocket.send_websocket(self.obj_path.queue_path_to_dict())
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Starte den Subprozess asynchron
|
|
||||||
obj.process = await asyncio.create_subprocess_exec(
|
|
||||||
*command,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE
|
|
||||||
)
|
|
||||||
|
|
||||||
self.active_process.add(obj)
|
|
||||||
obj.process_start = time.time()
|
|
||||||
|
|
||||||
await obj_process.read_out(obj)
|
|
||||||
await obj.process.wait()
|
|
||||||
|
|
||||||
# Prself.obj_websocket.send_websocket(self.obj_path.active_path_to_dict())ozess beendet, Status auswerten
|
|
||||||
if obj.process.returncode == 0:
|
|
||||||
obj.status = 0
|
|
||||||
result = "Finished"
|
|
||||||
else:
|
|
||||||
obj.status = 1
|
|
||||||
result = "Failure"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
obj.status = 2
|
|
||||||
logging.error(f"Fehler in video_convert(): {e}")
|
|
||||||
logging.error(traceback.format_exc())
|
|
||||||
finally:
|
|
||||||
logging.info(f"Prozess {result}({obj.process.returncode}): {obj.source_file_name}")
|
|
||||||
await self.obj_websocket.send_websocket(self.obj_path.active_path_to_dict())
|
|
||||||
await self.obj_websocket.send_websocket(self.obj_path.queue_path_to_dict())
|
|
||||||
self.active_process.discard(obj)
|
|
||||||
self.active_tasks.discard(obj)
|
|
||||||
self.obj_path.save_paths()
|
|
||||||
obj.convert_end = time.time()
|
|
||||||
obj_stat.save_stat(obj)
|
|
||||||
|
|
||||||
def convert_cmd(self, obj):
|
|
||||||
command_convert = [
|
|
||||||
"ffmpeg", "-y", "-i", obj.source_file,
|
|
||||||
# "-init_hw_device", "vaapi=va:/dev/dri/renderD128",
|
|
||||||
"-map", "0:0",
|
|
||||||
"-c:v", "libsvtav1",
|
|
||||||
#"-c:v", "av1_qsv",
|
|
||||||
"-preset", "5",
|
|
||||||
"-crf", "30",
|
|
||||||
"-g", "240",
|
|
||||||
"-pix_fmt", "yuv420p10le",
|
|
||||||
"-svtav1-params", "tune=0:film-grain=8",
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(obj.streams_audio):
|
|
||||||
for audio_stream in obj.streams_audio:
|
|
||||||
if audio_stream.get("tags", {}).get("language", None) in self.yaml["audio"]["language"]:
|
|
||||||
command_convert.extend([
|
|
||||||
"-map", f"0:{audio_stream['index']}",
|
|
||||||
f"-c:a", "libopus",
|
|
||||||
f"-b:a", "320k",
|
|
||||||
f"-ac", str(audio_stream['channels'])
|
|
||||||
])
|
|
||||||
|
|
||||||
# Subtitle-Streams einbinden
|
|
||||||
if len(obj.streams_subtitle):
|
|
||||||
for subtitle_stream in obj.streams_subtitle:
|
|
||||||
if subtitle_stream.get("codec_name") not in self.yaml["subtitle"]["blacklist"]:
|
|
||||||
if subtitle_stream.get("tags", {}).get("language", None) in self.yaml["subtitle"]["language"]:
|
|
||||||
command_convert.extend([
|
|
||||||
"-map", f"0:{subtitle_stream['index']}",
|
|
||||||
])
|
|
||||||
command_convert.append(obj.target_file)
|
|
||||||
|
|
||||||
obj.cmd = command_convert
|
|
||||||
return command_convert
|
|
||||||
|
|
||||||
def snake_update(self):
|
|
||||||
print(self.obj_path.paths)
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class Process:
|
|
||||||
def __init__(self, obj_websocket):
|
|
||||||
self.obj_websocket = obj_websocket
|
|
||||||
self.line_empty = 0
|
|
||||||
|
|
||||||
# Data
|
|
||||||
self.id = None
|
|
||||||
self.fps: float = 0.0
|
|
||||||
self.speed: float = 0.0
|
|
||||||
|
|
||||||
self.quantizer: int = 0
|
|
||||||
self.bitrate: list = [0, "kbits/s"]
|
|
||||||
self.size: list = [0, "KiB"]
|
|
||||||
|
|
||||||
self.time: int = 0
|
|
||||||
self.time_remaining = 0
|
|
||||||
self.loading = 0
|
|
||||||
self.frames: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
async def read_out(self, obj):
|
|
||||||
self.id = obj.id
|
|
||||||
self.line_empty = 0
|
|
||||||
i = 100
|
|
||||||
|
|
||||||
while True:
|
|
||||||
line = await obj.process.stderr.read(1024)
|
|
||||||
line_decoded = line.decode()
|
|
||||||
|
|
||||||
self.process_line_extract(obj, line_decoded)
|
|
||||||
#logging.info(line_decoded)
|
|
||||||
|
|
||||||
if obj.source_frames_total > 0:
|
|
||||||
self.loading = (self.frames / obj.source_frames_total) * 100
|
|
||||||
else:
|
|
||||||
self.loading = 0
|
|
||||||
|
|
||||||
await self.obj_websocket.send_websocket(self.to_dict(obj))
|
|
||||||
|
|
||||||
if self.line_empty > 30:
|
|
||||||
break
|
|
||||||
elif not line:
|
|
||||||
self.line_empty += 1
|
|
||||||
await asyncio.sleep(2)
|
|
||||||
continue
|
|
||||||
elif line:
|
|
||||||
self.line_empty = 0
|
|
||||||
|
|
||||||
if i == 100 or i == 200 or i == 300 or i == 400 or i == 500:
|
|
||||||
logging.info(self.to_dict(obj))
|
|
||||||
self.save_stat_value(obj)
|
|
||||||
elif i == 101 or i == 501:
|
|
||||||
i = 0
|
|
||||||
time = self.time_remaining
|
|
||||||
if time != " ":
|
|
||||||
logging.info(f"Time remaining: {time}")
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
def process_line_extract(self, obj, line:str):
|
|
||||||
# FPS
|
|
||||||
fps = re.findall(r"fps=\s*(\d+.\d*)", line)
|
|
||||||
self.fps = float(fps[0]) if fps else 0.0
|
|
||||||
|
|
||||||
# Quantizer
|
|
||||||
q = re.findall(r"q=\s*(\d+).\d+", line)
|
|
||||||
self.quantizer = int(q[0]) if q else 0
|
|
||||||
|
|
||||||
# Bitrate
|
|
||||||
bitrate = re.findall(r"bitrate=\s*(\d+)", line)
|
|
||||||
self.bitrate[0] = int(bitrate[0]) if bitrate else 0
|
|
||||||
|
|
||||||
# Speed
|
|
||||||
speed = re.findall(r"speed=\s*(\d+\.\d+)", line)
|
|
||||||
self.speed = float(speed[0]) if speed else 0.0
|
|
||||||
|
|
||||||
# File Size
|
|
||||||
size = re.findall(r"size=\s*(\d+)", line)
|
|
||||||
if size and int(size[0]) > self.size[0]:
|
|
||||||
self.size = obj.size_convert("KiB", None, "storage", int(size[0]))
|
|
||||||
obj.process_size = self.size
|
|
||||||
|
|
||||||
# Time
|
|
||||||
media_time = re.findall(r"time=\s*(\d+:\d+:\d+)", line)
|
|
||||||
time_v = media_time[0] if media_time else "00:00:00"
|
|
||||||
if self.time < obj.time_in_sec(time_v):
|
|
||||||
self.time = obj.time_in_sec(time_v)
|
|
||||||
obj.process_time = self.time
|
|
||||||
|
|
||||||
# Frames
|
|
||||||
frame = re.findall(r"frame=\s*(\d+)", line)
|
|
||||||
if frame and int(frame[0]) > self.frames:
|
|
||||||
self.frames = int(frame[0])
|
|
||||||
obj.process_frames = self.frames
|
|
||||||
|
|
||||||
def save_stat_value(self, obj):
|
|
||||||
if self.fps:
|
|
||||||
obj.stat_fps = [obj.stat_fps[0] + self.fps, obj.stat_fps[1] + 1]
|
|
||||||
|
|
||||||
if self.quantizer:
|
|
||||||
obj.stat_quantizer = [obj.stat_quantizer[0] + self.quantizer, obj.stat_quantizer[1] + 1]
|
|
||||||
|
|
||||||
if self.bitrate[0]:
|
|
||||||
obj.stat_bitrate = [obj.stat_bitrate[0] + self.bitrate[0], obj.stat_bitrate[1] + 1]
|
|
||||||
|
|
||||||
if self.speed:
|
|
||||||
obj.stat_speed = [obj.stat_speed[0] + self.speed, obj.stat_speed[1] + 1]
|
|
||||||
|
|
||||||
def to_dict(self, obj):
|
|
||||||
return {"data_flow": {
|
|
||||||
"id": self.id,
|
|
||||||
"frames": self.frames,
|
|
||||||
"fps": self.fps,
|
|
||||||
"quantizer": self.quantizer,
|
|
||||||
"size": self.size,
|
|
||||||
"time": obj.format_time(self.time, "Tage", "Std", "Min", None),
|
|
||||||
"time_remaining": obj.format_time(obj.time_remaining(), None, "Std", "Min", None),
|
|
||||||
"loading": self.loading,
|
|
||||||
"bitrate": self.bitrate,
|
|
||||||
"speed": self.speed
|
|
||||||
}}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import yaml
|
|
||||||
import subprocess
|
|
||||||
from app.class_media_file import Media
|
|
||||||
|
|
||||||
|
|
||||||
class Path:
|
|
||||||
def __init__(self, cfg):
|
|
||||||
"""
|
|
||||||
Receive Paths extract from String and save to a List Variable.
|
|
||||||
Methoden: save_paths / read_paths / delete_path / receive_path
|
|
||||||
|
|
||||||
:param cfg: Global Settings Object
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Settings
|
|
||||||
self.yaml = cfg
|
|
||||||
|
|
||||||
# Autostart
|
|
||||||
# self.read_paths()
|
|
||||||
|
|
||||||
# Variablen
|
|
||||||
self.paths:dict = {}
|
|
||||||
|
|
||||||
def save_paths(self):
|
|
||||||
"""
|
|
||||||
Saves the extrated Paths in a File
|
|
||||||
:return: True or False
|
|
||||||
"""
|
|
||||||
paths:list = []
|
|
||||||
|
|
||||||
for obj in self.paths.values():
|
|
||||||
if obj.status is None:
|
|
||||||
paths.append(obj.source_file)
|
|
||||||
|
|
||||||
paths_extrat_dict = {"paths": paths}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(f"app/cfg/{self.yaml['path_file']}", "w", encoding="utf8") as file:
|
|
||||||
yaml.dump(paths_extrat_dict, file, default_flow_style=False, indent=4)
|
|
||||||
logging.info(f"{len(self.paths)} paths were saved to file")
|
|
||||||
return 1
|
|
||||||
except FileNotFoundError:
|
|
||||||
logging.error(f"File {self.yaml['path_file']} not found")
|
|
||||||
return 0
|
|
||||||
except IOError:
|
|
||||||
logging.critical(f"Error file {self.yaml['path_file']} maybe damaged")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Achtung Abrufen aus Datei und neu einlesen der ffprobe Daten fehlt noch
|
|
||||||
def read_paths(self):
|
|
||||||
"""
|
|
||||||
Read Media Paths from a File
|
|
||||||
:return: True or False
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with open(f"app/cfg/{self.yaml['path_file']}", "r", encoding="utf8") as file:
|
|
||||||
list_paths = yaml.safe_load(file)
|
|
||||||
count = len(list_paths.get("paths", []))
|
|
||||||
|
|
||||||
if count > 0:
|
|
||||||
self.receive_paths(list_paths)
|
|
||||||
|
|
||||||
logging.info(f"{count} paths were read from file")
|
|
||||||
return 1
|
|
||||||
except FileNotFoundError:
|
|
||||||
logging.error(f"File {self.yaml['path_file']} not found")
|
|
||||||
return 0
|
|
||||||
except IOError:
|
|
||||||
logging.critical(f"Error file {self.yaml['path_file']} maybe damaged")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def delete_path(self, obj_id:int):
|
|
||||||
"""
|
|
||||||
Delete a Media Path from a List Variable and overwrite the path file
|
|
||||||
:param obj_id: Path from a Media File which to delete
|
|
||||||
:return: True or False
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
del self.paths[obj_id]
|
|
||||||
self.save_paths()
|
|
||||||
return 1
|
|
||||||
except ValueError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def receive_paths(self, var_paths):
|
|
||||||
"""
|
|
||||||
Splitting the Path String to List Single Media Paths
|
|
||||||
:param var_paths: String or List of a single or more Media Paths
|
|
||||||
:return: True or False
|
|
||||||
"""
|
|
||||||
logging.info(f"Empfangen{var_paths}")
|
|
||||||
|
|
||||||
if isinstance(var_paths, str):
|
|
||||||
pattern = r"(?<=\.mkv\s|\.mp4\s|\.avi\s)|(?<=\.webm\s)"
|
|
||||||
paths = re.split(pattern, var_paths)
|
|
||||||
else:
|
|
||||||
paths = var_paths
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
self.get_with_ffprobe(path)
|
|
||||||
|
|
||||||
print(paths)
|
|
||||||
|
|
||||||
if len(paths):
|
|
||||||
self.save_paths()
|
|
||||||
|
|
||||||
return 1
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def extract_media_info(path:str, select:str):
|
|
||||||
"""
|
|
||||||
Erstellt ffprobe command for selected Streams
|
|
||||||
:param path: path to media file
|
|
||||||
:param select: v (for video), a (for audio), s (for subtitle)
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
command = [
|
|
||||||
"ffprobe", "-v",
|
|
||||||
"error",
|
|
||||||
"-select_streams",
|
|
||||||
f"{select}",
|
|
||||||
"-show_entries",
|
|
||||||
"stream=index,channels,codec_name,codec_type,pix_fmt,level,"
|
|
||||||
"film_grain,r_frame_rate,bit_rate,sample_rate,width,height,size,tags:stream_tags=language,duration",
|
|
||||||
"-show_entries",
|
|
||||||
"format=size,bit_rate,nb_streams,duration",
|
|
||||||
"-of", "json",
|
|
||||||
path
|
|
||||||
]
|
|
||||||
|
|
||||||
result = subprocess.run(command, stdout=subprocess.PIPE, text=True)
|
|
||||||
json_data = json.loads(result.stdout)
|
|
||||||
|
|
||||||
if select == "v":
|
|
||||||
if json_data.get("format") is not list:
|
|
||||||
list_format = [json_data.get("format", "")]
|
|
||||||
else:
|
|
||||||
list_format = json_data.get("format")
|
|
||||||
|
|
||||||
return json_data.get("streams", []), list_format
|
|
||||||
elif select == "a":
|
|
||||||
return json_data.get("streams", [])
|
|
||||||
elif select == "s":
|
|
||||||
return json_data.get("streams", [])
|
|
||||||
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_with_ffprobe(self, path:str):
|
|
||||||
try:
|
|
||||||
path = path.strip()
|
|
||||||
|
|
||||||
if os.path.exists(path):
|
|
||||||
streams_video, streams_format = self.extract_media_info(path, "v")
|
|
||||||
streams_audio = self.extract_media_info(path, "a")
|
|
||||||
streams_subtitle = self.extract_media_info(path, "s")
|
|
||||||
|
|
||||||
obj = Media(path, streams_video, streams_audio, streams_subtitle, streams_format)
|
|
||||||
if not self.search_paths(path):
|
|
||||||
self.paths.update({obj.id: obj})
|
|
||||||
logging.info(obj)
|
|
||||||
else:
|
|
||||||
logging.info(f"File exists in Waiting Snake :D: {path}")
|
|
||||||
|
|
||||||
return 1
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Get Video Information: {e}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def search_paths(self, path):
|
|
||||||
for obj in self.paths.values():
|
|
||||||
if obj.source_file == path:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def count_paths(self, status):
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
for obj in self.paths.values():
|
|
||||||
if obj.status == status:
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
return count
|
|
||||||
|
|
||||||
def active_path_to_dict(self):
|
|
||||||
list_active_paths = {}
|
|
||||||
|
|
||||||
for obj in self.paths.values():
|
|
||||||
if obj.status == 3:
|
|
||||||
list_active_paths.update({obj.id: obj.to_dict_active_paths()})
|
|
||||||
|
|
||||||
return {"data_convert": list_active_paths}
|
|
||||||
|
|
||||||
def queue_path_to_dict(self):
|
|
||||||
list_active_paths = {}
|
|
||||||
|
|
||||||
for obj in self.paths.values():
|
|
||||||
if obj.status is None or obj.status == 1 or obj.status == 2 or obj.status == 3:
|
|
||||||
list_active_paths.update({obj.id: obj.to_dict_active_paths()})
|
|
||||||
|
|
||||||
return {"data_queue": list_active_paths}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
import os
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
|
|
||||||
class Media:
|
|
||||||
_id_counter = 0
|
|
||||||
|
|
||||||
def __init__(self, path, streams_video, streams_audio, streams_subtitle, streams_format):
|
|
||||||
# misc
|
|
||||||
self.id = int(f"{int(time.time() * 1000)}{str(Media._id_counter).zfill(3)}")
|
|
||||||
self.cmd : list = None
|
|
||||||
# source
|
|
||||||
self.source_file: str = path
|
|
||||||
self.source_path: str = os.path.dirname(path)
|
|
||||||
self.source_file_name: str = os.path.basename(self.source_file)
|
|
||||||
self.source_duration: int = self.time_in_sec(streams_format[0].get("duration", "00:00:00"))
|
|
||||||
self.source_size: list = self.size_convert("B", None, "storage", int(streams_format[0].get("size")))
|
|
||||||
self.source_frame_rate: int = self.frame_rate(streams_video)
|
|
||||||
self.source_frames_total: int = self.source_frame_rate * self.source_duration
|
|
||||||
self.source_time: int = 0
|
|
||||||
|
|
||||||
# target
|
|
||||||
self.target_file: str = f"{path.rsplit('.', 1)[0]}.webm"
|
|
||||||
self.target_file_name: str = os.path.basename(self.target_file)
|
|
||||||
self.target_size: int = 0
|
|
||||||
|
|
||||||
# process
|
|
||||||
self.status = None
|
|
||||||
self.process_start: int = 0
|
|
||||||
self.process_end: int = 0
|
|
||||||
self.process_time: int = 0
|
|
||||||
self.process_size: list = [0, "KiB"]
|
|
||||||
self.process_frames: int = 0
|
|
||||||
self.process_time_remaining: int = 0
|
|
||||||
|
|
||||||
# statistic
|
|
||||||
self.stat_fps: list = [0, 0]
|
|
||||||
self.stat_bitrate: list = [0, 0]
|
|
||||||
self.stat_quantizer: list = [0, 0]
|
|
||||||
self.stat_speed: list = [0, 0]
|
|
||||||
|
|
||||||
# raw
|
|
||||||
self.streams_video = streams_video
|
|
||||||
self.streams_audio = streams_audio
|
|
||||||
self.streams_subtitle = streams_subtitle
|
|
||||||
self.streams_format = streams_format
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Media._id_counter += 1
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
def stream_output(stream_list):
|
|
||||||
count = 1
|
|
||||||
string = ""
|
|
||||||
for video_stream in stream_list:
|
|
||||||
string += f"{video_stream.get('codec_type').capitalize()} {count}" if video_stream.get(
|
|
||||||
'codec_type') else "Format"
|
|
||||||
for key, value in video_stream.items():
|
|
||||||
string += f" -- {key}: {value}"
|
|
||||||
|
|
||||||
string += "\n"
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
return string
|
|
||||||
|
|
||||||
# Ausgabe
|
|
||||||
output_string = f"\n{self.source_file}\n"
|
|
||||||
output_string += "------------------------------------\n"
|
|
||||||
output_string += stream_output(self.streams_format)
|
|
||||||
output_string += "------------------------------------\n"
|
|
||||||
output_string += stream_output(self.streams_video)
|
|
||||||
output_string += "------------------------------------\n"
|
|
||||||
output_string += stream_output(self.streams_audio)
|
|
||||||
output_string += "------------------------------------\n"
|
|
||||||
output_string += stream_output(self.streams_subtitle)
|
|
||||||
output_string += "------------------------------------\n"
|
|
||||||
output_string += f"{self.target_file}\n"
|
|
||||||
output_string += "------------------------------------\n"
|
|
||||||
output_string += f"{self.id} -- {self.status}"
|
|
||||||
output_string += "\n************************************\n"
|
|
||||||
|
|
||||||
return output_string
|
|
||||||
|
|
||||||
def to_dict_active_paths(self):
|
|
||||||
return {
|
|
||||||
"source_file_name": self.source_file_name,
|
|
||||||
"source_file": self.source_file,
|
|
||||||
"source_path": self.source_path,
|
|
||||||
"source_duration": self.source_duration,
|
|
||||||
"source_size": self.source_size,
|
|
||||||
"source_frame_rate": self.source_frame_rate,
|
|
||||||
"source_frames_total": self.source_frames_total,
|
|
||||||
"source_time": self.source_time,
|
|
||||||
|
|
||||||
# target
|
|
||||||
"target_file_name": self.target_file_name,
|
|
||||||
"target_file": self.target_file,
|
|
||||||
"target_size": self.target_size,
|
|
||||||
|
|
||||||
#process
|
|
||||||
"status": self.status
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_dict_stat(self):
|
|
||||||
return {self.id: {
|
|
||||||
# source
|
|
||||||
"source_file_name": self.source_file_name,
|
|
||||||
"source_file": self.source_file,
|
|
||||||
"source_duration": self.source_duration,
|
|
||||||
"source_size": self.source_size,
|
|
||||||
"source_frame_rate": self.source_frame_rate,
|
|
||||||
"source_frames_total": self.source_frames_total,
|
|
||||||
"source_time": self.source_time,
|
|
||||||
|
|
||||||
# target
|
|
||||||
"target_file_name": self.target_file_name,
|
|
||||||
"target_file": self.target_file,
|
|
||||||
"target_size": self.target_size,
|
|
||||||
|
|
||||||
# process
|
|
||||||
"status": self.status,
|
|
||||||
"process_start": self.process_start,
|
|
||||||
"process_end": self.process_end,
|
|
||||||
"process_time": self.process_time,
|
|
||||||
"process_size": self.process_size,
|
|
||||||
"process_frames": self.process_frames,
|
|
||||||
"process_time_remaining": self.process_time_remaining,
|
|
||||||
|
|
||||||
# statistic
|
|
||||||
"stat_fps": self.stat_fps,
|
|
||||||
"stat_bitrate": self.stat_bitrate,
|
|
||||||
"stat_quantizer": self.stat_quantizer,
|
|
||||||
"stat_speed": self.stat_speed
|
|
||||||
}}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def frame_rate(video_streams):
|
|
||||||
var_frame_rate = video_streams[0].get("r_frame_rate", "0/0").split("/")
|
|
||||||
if int(var_frame_rate[1]) > 0:
|
|
||||||
int_frame_rate = round(int(var_frame_rate[0]) / int(var_frame_rate[1]))
|
|
||||||
else:
|
|
||||||
int_frame_rate = 0
|
|
||||||
|
|
||||||
return int_frame_rate
|
|
||||||
|
|
||||||
def time_remaining(self):
|
|
||||||
if self.stat_fps[0] > 0:
|
|
||||||
var_time_remaining = (self.source_frames_total - self.process_frames) / (self.stat_fps[0] / self.stat_fps[1])
|
|
||||||
self.process_time_remaining = round(var_time_remaining)
|
|
||||||
elif self.stat_fps[0] == 0:
|
|
||||||
self.process_time_remaining = 0
|
|
||||||
|
|
||||||
return self.process_time_remaining
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_time(seconds:int, str_day, str_hour, str_min, str_s):
|
|
||||||
days = round(seconds // (24 * 3600))
|
|
||||||
seconds %= (24 * 3600)
|
|
||||||
if days and str_day is not None:
|
|
||||||
d = f"{days} {str_day}"
|
|
||||||
else:
|
|
||||||
d = ""
|
|
||||||
|
|
||||||
hours = round(seconds // 3600)
|
|
||||||
seconds %= 3600
|
|
||||||
if hours and str_hour is not None:
|
|
||||||
h = f"{hours} {str_hour}"
|
|
||||||
else:
|
|
||||||
h = ""
|
|
||||||
|
|
||||||
minutes = math.ceil(seconds // 60)
|
|
||||||
seconds %= 60
|
|
||||||
if minutes and str_min is not None:
|
|
||||||
m = f"{minutes} {str_min}"
|
|
||||||
else:
|
|
||||||
m = ""
|
|
||||||
|
|
||||||
if seconds and str_s is not None:
|
|
||||||
s = f"{seconds} {str_s}"
|
|
||||||
else:
|
|
||||||
s = ""
|
|
||||||
|
|
||||||
return f"{d} {h} {m} {s}"
|
|
||||||
|
|
||||||
# Data convert
|
|
||||||
@staticmethod
|
|
||||||
def time_in_sec(time_str: str) -> int:
|
|
||||||
parts = time_str.split(":")
|
|
||||||
|
|
||||||
if len(parts) == 1: # Falls nur Sekunden mit Nachkommastellen vorliegen
|
|
||||||
return int(float(parts[0])) # Erst in float, dann in int umwandeln
|
|
||||||
|
|
||||||
if len(parts) == 3: # Normales HH:MM:SS-Format
|
|
||||||
h, m, s = map(float, parts) # In float umwandeln, falls Nachkommastellen im Sekundenwert sind
|
|
||||||
return int(h * 3600 + m * 60 + s) # Alles in Sekunden umrechnen
|
|
||||||
|
|
||||||
raise ValueError(f"Ungültiges Zeitformat: {time_str}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def size_convert(source: str, target, unit: str, size=0):
|
|
||||||
list_unit: list = []
|
|
||||||
|
|
||||||
if unit == "storage":
|
|
||||||
list_unit = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]
|
|
||||||
elif unit == "data_rate":
|
|
||||||
list_unit = ["bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps"]
|
|
||||||
elif unit == "binary_data_rate":
|
|
||||||
list_unit = ["b/s", "Kib/s", "Mib/s", "Gib/s", "Tib/s", "Pib/s"]
|
|
||||||
|
|
||||||
factor = 1024 # Binäre Umrechnung
|
|
||||||
|
|
||||||
if source not in list_unit:
|
|
||||||
raise ValueError("Ungültige Quell-Einheit!")
|
|
||||||
|
|
||||||
source_index = list_unit.index(source)
|
|
||||||
|
|
||||||
if target:
|
|
||||||
if target not in list_unit:
|
|
||||||
raise ValueError("Ungültige Ziel-Einheit!")
|
|
||||||
target_index = list_unit.index(target)
|
|
||||||
|
|
||||||
if source_index < target_index:
|
|
||||||
return size / (factor ** (target_index - source_index)), target
|
|
||||||
else:
|
|
||||||
return size * (factor ** (source_index - target_index)), target
|
|
||||||
|
|
||||||
# Automatische Umrechnung
|
|
||||||
while size >= 1000 and source_index < len(list_unit) - 1:
|
|
||||||
size /= factor
|
|
||||||
source_index += 1
|
|
||||||
|
|
||||||
return [round(size, 1), list_unit[source_index]]
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import logging
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
import os
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from app.class_media_file import Media
|
|
||||||
|
|
||||||
class Stat:
|
|
||||||
"""
|
|
||||||
Handles reading and writing video conversion statistics
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""
|
|
||||||
Initialize Class with path for statistic file
|
|
||||||
"""
|
|
||||||
self.path = "app/cfg/statistic.yaml"
|
|
||||||
|
|
||||||
def save_stat(self, obj: "Media") -> None:
|
|
||||||
"""
|
|
||||||
Saves the statistic in YAML file
|
|
||||||
:param obj: Media object
|
|
||||||
:type obj: Media
|
|
||||||
"""
|
|
||||||
|
|
||||||
daten = self.read_stat()
|
|
||||||
daten.setdefault("videos", {})
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Neuen Eintrag hinzufügen
|
|
||||||
daten["videos"].update(obj.to_dict_stat())
|
|
||||||
|
|
||||||
with open(self.path, "w", encoding="utf8") as file:
|
|
||||||
yaml.dump(daten, file, default_flow_style=False, indent=4, allow_unicode=True)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Save Stat Failure: {e}")
|
|
||||||
|
|
||||||
def read_stat(self) -> dict[str, any]:
|
|
||||||
"""
|
|
||||||
Read statistic from YAML file
|
|
||||||
:return: Dictionary width statistics
|
|
||||||
:rtype: Dict[str, any]
|
|
||||||
"""
|
|
||||||
# Bestehende Daten laden
|
|
||||||
if os.path.exists(self.path):
|
|
||||||
with open(self.path, "r", encoding="utf8") as file:
|
|
||||||
daten = yaml.safe_load(file) or {}
|
|
||||||
else:
|
|
||||||
daten = {}
|
|
||||||
|
|
||||||
return daten
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import logging
|
|
||||||
import yaml
|
|
||||||
import traceback
|
|
||||||
from logging.handlers import TimedRotatingFileHandler, RotatingFileHandler
|
|
||||||
|
|
||||||
# === ANSI-Farbcodes für Konsole ===
|
|
||||||
RESET = "\x1b[0m" # Farbe zurücksetzen
|
|
||||||
RED = "\x1b[31m" # Fehler: Rot
|
|
||||||
YELLOW = "\x1b[33m" # Warnung: Gelb
|
|
||||||
GREEN = "\x1b[32m" # Info: Grün
|
|
||||||
BLUE = "\x1b[34m" # Debug: Blau
|
|
||||||
|
|
||||||
class CustomFormatter(logging.Formatter):
|
|
||||||
def format(self, record):
|
|
||||||
if record.levelno >= logging.ERROR and record.exc_info:
|
|
||||||
record.msg = f"{record.msg}\n{traceback.format_exc()}"
|
|
||||||
|
|
||||||
# Farbe nach Level
|
|
||||||
if record.levelno == logging.DEBUG:
|
|
||||||
record.msg = f"{BLUE}{record.msg}{RESET}"
|
|
||||||
elif record.levelno == logging.INFO:
|
|
||||||
record.msg = f"{GREEN}{record.msg}{RESET}"
|
|
||||||
elif record.levelno == logging.WARNING:
|
|
||||||
record.msg = f"{YELLOW}{record.msg}{RESET}"
|
|
||||||
elif record.levelno >= logging.ERROR:
|
|
||||||
record.msg = f"{RED}{record.msg}{RESET}"
|
|
||||||
|
|
||||||
return super().format(record)
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
def __init__(self):
|
|
||||||
self.yaml = None
|
|
||||||
self.read()
|
|
||||||
|
|
||||||
def read(self):
|
|
||||||
with open("app/cfg/settings.yaml", "r", encoding="utf-8") as file:
|
|
||||||
self.yaml = yaml.safe_load(file)
|
|
||||||
|
|
||||||
|
|
||||||
def write(self):
|
|
||||||
with open("app/cfg/settings.yaml", "w", encoding="utf8") as file:
|
|
||||||
yaml.dump(self.yaml, file, default_flow_style=False, indent=4)
|
|
||||||
|
|
||||||
def set_logging(self):
|
|
||||||
|
|
||||||
log_level = self.yaml.get("log_level", "INFO")
|
|
||||||
log_file = self.yaml.get("log_file", "app.log")
|
|
||||||
log_mode = self.yaml.get("log_rotation", "size") # "time" oder "size"
|
|
||||||
|
|
||||||
handlers = [logging.StreamHandler()]
|
|
||||||
|
|
||||||
# Handler je nach Modus erstellen
|
|
||||||
if log_mode == "time":
|
|
||||||
log_handler = TimedRotatingFileHandler(
|
|
||||||
f"app/logs/{log_file}", when="midnight", interval=1, backupCount=7, encoding="utf-8"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log_handler = RotatingFileHandler(
|
|
||||||
f"app/logs/{log_file}", maxBytes=5 * 1024 * 1024, backupCount=5, encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
handlers.append(log_handler)
|
|
||||||
|
|
||||||
# Logger konfigurieren
|
|
||||||
logging.basicConfig(
|
|
||||||
level=log_level,
|
|
||||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
||||||
handlers=handlers
|
|
||||||
)
|
|
||||||
|
|
||||||
# Traceback-Formatter setzen
|
|
||||||
formatter = CustomFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
||||||
for handler in logging.getLogger().handlers:
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import websockets
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
from asyncio import CancelledError
|
|
||||||
from websockets import InvalidUpgrade, ConnectionClosed, ConnectionClosedError
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from app.class_settings import Settings
|
|
||||||
from app.class_file_path import Path
|
|
||||||
from app.class_file_convert import Convert
|
|
||||||
from app.class_media_file_stat import Stat
|
|
||||||
|
|
||||||
var_convert_active = False
|
|
||||||
|
|
||||||
class Server:
|
|
||||||
def __init__(self):
|
|
||||||
self.websocket = None
|
|
||||||
self.clients = set()
|
|
||||||
obj_settings = Settings()
|
|
||||||
obj_settings.set_logging()
|
|
||||||
|
|
||||||
self.yaml = obj_settings.yaml
|
|
||||||
self.obj_path = Path(self.yaml)
|
|
||||||
self.obj_convert = Convert(self, self.yaml, self.obj_path)
|
|
||||||
|
|
||||||
async def start_convert(self):
|
|
||||||
global var_convert_active
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not var_convert_active and self.yaml['autostart']:
|
|
||||||
await self.obj_convert.snake_waiting()
|
|
||||||
var_convert_active = True
|
|
||||||
else:
|
|
||||||
self.obj_convert.snake_update()
|
|
||||||
finally:
|
|
||||||
self.set_var_convert_active(False)
|
|
||||||
|
|
||||||
async def send_websocket(self, message):
|
|
||||||
message_json = json.dumps(message)
|
|
||||||
|
|
||||||
for client in self.clients.copy():
|
|
||||||
try:
|
|
||||||
await client.send(message_json)
|
|
||||||
except websockets.exceptions.ConnectionClosed:
|
|
||||||
self.clients.remove(client)
|
|
||||||
|
|
||||||
async def handle_client(self, websocket):
|
|
||||||
self.websocket = websocket
|
|
||||||
|
|
||||||
global var_convert_active
|
|
||||||
|
|
||||||
if websocket not in self.clients:
|
|
||||||
self.clients.add(websocket)
|
|
||||||
await self.send_websocket(self.obj_path.active_path_to_dict())
|
|
||||||
await self.send_websocket(self.obj_path.queue_path_to_dict())
|
|
||||||
|
|
||||||
try:
|
|
||||||
async for message in websocket:
|
|
||||||
data = json.loads(message)
|
|
||||||
|
|
||||||
if data.get("data_path"):
|
|
||||||
self.obj_path.receive_paths(data.get("data_path"))
|
|
||||||
await self.send_websocket(self.obj_path.active_path_to_dict())
|
|
||||||
await self.send_websocket(self.obj_path.queue_path_to_dict())
|
|
||||||
await self.start_convert()
|
|
||||||
|
|
||||||
elif data.get("data_command"):
|
|
||||||
if data["data_command"]["cmd"] == "delete":
|
|
||||||
self.obj_path.delete_path(data["data_command"]["id"])
|
|
||||||
await self.send_websocket(self.obj_path.queue_path_to_dict())
|
|
||||||
elif data.get("data_message"):
|
|
||||||
logging.info(f"Server hat Empfangen: {data.get('data_message')}")
|
|
||||||
|
|
||||||
except (ConnectionClosedError, ConnectionClosed):
|
|
||||||
logging.warning("Client Verbindung geschlossen")
|
|
||||||
except InvalidUpgrade:
|
|
||||||
logging.warning("Ungültiger Websocket Upgrade versuch")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Unerwarteter Fehler {e}")
|
|
||||||
finally:
|
|
||||||
self.clients.discard(websocket)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_var_convert_active(value: bool):
|
|
||||||
global var_convert_active
|
|
||||||
|
|
||||||
var_convert_active = value
|
|
||||||
|
|
||||||
async def server_websocket(self):
|
|
||||||
self.obj_path.read_paths()
|
|
||||||
asyncio.create_task(self.start_convert())
|
|
||||||
|
|
||||||
server = await websockets.serve(self.handle_client, self.yaml['server_ip'], self.yaml['websocket_port'])
|
|
||||||
logging.info(f"Websocket Server läuft auf IP: {self.yaml['server_ip']} Port: {self.yaml['websocket_port']}")
|
|
||||||
await server.wait_closed()
|
|
||||||
|
|
||||||
# WebServer --------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
@staticmethod
|
|
||||||
async def handle_index(request):
|
|
||||||
return web.FileResponse("./client/index.html")
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
async def handle_ip(self, request):
|
|
||||||
url = self.yaml["extern_websocket_url"]
|
|
||||||
port = self.yaml["websocket_port"]
|
|
||||||
websocket_https = self.yaml["websocket_https"]
|
|
||||||
|
|
||||||
return web.json_response({"extern_websocket_url": url, "websocket_port": port, "websocket_https": websocket_https})
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
@staticmethod
|
|
||||||
async def handle_stat(request):
|
|
||||||
obj_stat = Stat()
|
|
||||||
|
|
||||||
return web.json_response(obj_stat.read_stat())
|
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
async def handle_cmd(self, request):
|
|
||||||
command = self.obj_convert.active_tasks # deine oben gepostete Funktion
|
|
||||||
|
|
||||||
html = ""
|
|
||||||
|
|
||||||
for obj in command:
|
|
||||||
# ffmpeg-Befehl als String (mit Shell-Escapes, falls Leerzeichen etc.)
|
|
||||||
cmd_str = " ".join([shlex.quote(arg) for arg in obj.cmd])
|
|
||||||
|
|
||||||
html += f"{cmd_str}<br /><br />"
|
|
||||||
|
|
||||||
return web.Response(text=html, content_type="text/html")
|
|
||||||
|
|
||||||
async def server_http(self):
|
|
||||||
app = web.Application()
|
|
||||||
app.router.add_get("/", self.handle_index)
|
|
||||||
app.router.add_get("/api/ip", self.handle_ip)
|
|
||||||
app.router.add_get("/api/stats", self.handle_stat)
|
|
||||||
app.router.add_get("/api/cmd", self.handle_cmd)
|
|
||||||
app.router.add_static("/client/", path="./client", name="client")
|
|
||||||
runner = web.AppRunner(app)
|
|
||||||
await runner.setup()
|
|
||||||
site = web.TCPSite(runner, "0.0.0.0", self.yaml["webserver_port"])
|
|
||||||
await site.start()
|
|
||||||
logging.info(f"HTTP Server läuft auf Port {self.yaml['webserver_port']}")
|
|
||||||
|
|
||||||
# Start Server -----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def start_server(self):
|
|
||||||
try:
|
|
||||||
await asyncio.gather(
|
|
||||||
self.server_websocket(),
|
|
||||||
self.server_http()
|
|
||||||
)
|
|
||||||
except CancelledError:
|
|
||||||
logging.warning("Server wurde durch Keyboard Interrupt gestoppt.")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Unerwarteter Fehler beim Starten des Servers {e}")
|
|
||||||
34
client.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* @returns {Promise<{server_ip: string, server_port: number}>}
|
||||||
|
*/
|
||||||
|
async function getServerConfig() {
|
||||||
|
const response = await fetch('/api/ip');
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
getServerConfig()
|
||||||
|
.then(data => {
|
||||||
|
const websocketIp = data.server_ip;
|
||||||
|
const websocketPort = data.server_port;
|
||||||
|
const ws = new WebSocket(`ws://${websocketIp}:${websocketPort}`);
|
||||||
|
|
||||||
|
ws.onopen = function() {
|
||||||
|
console.log("WebSocket ist geöffnet");
|
||||||
|
ws.send(JSON.stringify({"data_message": "Server Adresse: " + websocketIp + ":" + websocketPort}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(messageEvent) {
|
||||||
|
console.log(messageEvent.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function() {
|
||||||
|
console.log("WebSocket wurde geschlossen");
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(errorEvent) {
|
||||||
|
console.error("WebSocket-Fehler: ", errorEvent);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching settings:', error);
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
|
@ -1,48 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Webm VC</title>
|
|
||||||
<link rel="stylesheet" href="/client/index_style.css">
|
|
||||||
<link rel="icon" href="/client/icons/favicon.ico" type="image/x-icon">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- === Menü === -->
|
|
||||||
<header>
|
|
||||||
<h1>Video Konverter AV1/Opus - Webm</h1>
|
|
||||||
<nav>
|
|
||||||
<img src="/client/icons/stat-100.png" onclick="openPopup()" alt="Statisiken" width="50" style="cursor: pointer;">
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- === Aktive Konvertierungen === -->
|
|
||||||
<section id="active-conversions">
|
|
||||||
<h2>Aktive Konvertierungen</h2>
|
|
||||||
<!-- Wird dynamisch mit JS gefüllt -->
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- === Warteschleife === -->
|
|
||||||
<header>
|
|
||||||
<h1>Warteschlange</h1>
|
|
||||||
</header>
|
|
||||||
<section id="queue">
|
|
||||||
<!-- Wird dynamisch mit JS gefüllt -->
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- === Statistikbereich === -->
|
|
||||||
<div id="popup" class="popup">
|
|
||||||
<div class="popup-content">
|
|
||||||
<div class="flex-row">
|
|
||||||
<h2>Allgemeine Statistiken</h2>
|
|
||||||
<img src="/client/icons/close-144.png" onclick="closePopup()" alt="Statisiken" width="15" style="cursor: pointer;">
|
|
||||||
</div>
|
|
||||||
<p>Hier könnten Diagramme, Durchschnittswerte etc. angezeigt werden.</p>
|
|
||||||
<section id="stat"></section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/client/media_conversion.js"></script>
|
|
||||||
<script src="/client/media_stat.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
/* === Allgemeines Design === */
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background-color: #121212;
|
|
||||||
color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #90caf9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Menü oben === */
|
|
||||||
header {
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav button {
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav button:hover {
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Bereich: Aktive Konvertierungen === */
|
|
||||||
#active-conversions {
|
|
||||||
padding: 1rem;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
#active-conversions h2 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card {
|
|
||||||
background-color: #1f1f1f;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card h3 {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card-values {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr auto;
|
|
||||||
grid-auto-rows: auto;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card-values-items {
|
|
||||||
padding: 5px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card .actions button {
|
|
||||||
background-color: #444;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card .actions button:hover {
|
|
||||||
background-color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-button {
|
|
||||||
grid-column: 4;
|
|
||||||
grid-row: 1 / span 3; /* oder wie viele Zeilen du hast */
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
background-color: #333;
|
|
||||||
color: #eee;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
display: none;
|
|
||||||
width: max-content;
|
|
||||||
max-width: 300px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card:hover .tooltip {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Bereich: Warteschleife === */
|
|
||||||
|
|
||||||
#queue {
|
|
||||||
padding: 1rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue_wait-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 80%;
|
|
||||||
background-color: #1f1f1f;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card_wait-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu_wait {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: auto; /* ← Schiebt das Ding ganz nach unten */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#queue h2 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversion_wait_icons {
|
|
||||||
width: 25px;
|
|
||||||
height: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status_icons {
|
|
||||||
width: 25px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu_wait {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between; /* Abstand dazwischen */
|
|
||||||
align-items: center; /* Vertikal zentriert, optional */
|
|
||||||
margin-top: auto; /* ← Schiebt das Ding ganz nach unten */
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
width: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rechts {
|
|
||||||
width: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Statistik-Bereich === */
|
|
||||||
#statistics {
|
|
||||||
padding: 1rem;
|
|
||||||
border-top: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
#statistics h2 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversion_icons {
|
|
||||||
width: 50px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.conversion_url {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: rgba(0,0,0,0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-content {
|
|
||||||
background-color: #000000;
|
|
||||||
margin: 15% auto;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid #444;
|
|
||||||
width: 90%;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
color: #aaa;
|
|
||||||
float: right;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queue-card {
|
|
||||||
background-color: #1f1f1f;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
header h1 {
|
|
||||||
font-size: 1.4rem; /* vorher 1.75rem */
|
|
||||||
}
|
|
||||||
|
|
||||||
nav button {
|
|
||||||
padding: 0.4rem 0.8rem; /* etwas kleiner */
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card {
|
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card h3 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card-values-items {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversion_url {
|
|
||||||
font-size: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-content {
|
|
||||||
width: 90%; /* vorher 90% */
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversion_icons {
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
/**
|
|
||||||
* @returns {Promise<{server_ip: string, server_port: number}>}
|
|
||||||
*/
|
|
||||||
|
|
||||||
let videoActive = {};
|
|
||||||
let videoQueue = {};
|
|
||||||
let ws = null
|
|
||||||
|
|
||||||
async function getServerConfig() {
|
|
||||||
const response = await fetch('/api/ip');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMediaStat() {
|
|
||||||
const response = await fetch('/api/stats');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
getServerConfig()
|
|
||||||
.then(data => {
|
|
||||||
const externWebsocketUrl = data.extern_websocket_url;
|
|
||||||
const websocketHttps = data.websocket_https
|
|
||||||
|
|
||||||
let connect;
|
|
||||||
if (websocketHttps === 1){
|
|
||||||
connect = `wss://${externWebsocketUrl}`
|
|
||||||
} else {
|
|
||||||
connect = `ws://${externWebsocketUrl}`
|
|
||||||
}
|
|
||||||
|
|
||||||
ws = new WebSocket(connect);
|
|
||||||
|
|
||||||
ws.onopen = function() {
|
|
||||||
console.log("WebSocket ist geöffnet");
|
|
||||||
ws.send(JSON.stringify({"data_message": "Server Adresse: " + externWebsocketUrl}));
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = function(messageEvent) {
|
|
||||||
try{
|
|
||||||
console.log(messageEvent.data);
|
|
||||||
let packet = JSON.parse(messageEvent.data);
|
|
||||||
|
|
||||||
if (packet.data_flow !== undefined){
|
|
||||||
updateVideoElement(packet);
|
|
||||||
} else if(packet.data_convert !== undefined){
|
|
||||||
deleteVideoElement(packet)
|
|
||||||
createVideoElement(packet);
|
|
||||||
} else if(packet.data_queue !== undefined){
|
|
||||||
createWaitingSnake(packet);
|
|
||||||
}
|
|
||||||
} catch (e){
|
|
||||||
console.error("Error parsing data flow");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = function() {
|
|
||||||
console.log("WebSocket wurde geschlossen");
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = function(errorEvent) {
|
|
||||||
console.error("WebSocket-Fehler: ", errorEvent);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error fetching settings:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendCommand(command, id){
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
const payload = {"data_command": {"cmd": command, "id": id}};
|
|
||||||
|
|
||||||
ws.send(JSON.stringify(payload));
|
|
||||||
} else {
|
|
||||||
console.warn("WebSocket ist nicht verbunden")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteVideoElement(packet) {
|
|
||||||
for (let key in videoActive){
|
|
||||||
if (!(key in packet.data_convert)){
|
|
||||||
const elem = document.getElementById(key);
|
|
||||||
if(elem){
|
|
||||||
elem.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete videoActive[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVideoElement(packet){
|
|
||||||
const active_Conversions = document.getElementById('active-conversions');
|
|
||||||
|
|
||||||
Object.keys(packet.data_convert).forEach(key => {
|
|
||||||
const video = packet.data_convert[key];
|
|
||||||
|
|
||||||
if(!videoActive[key]){
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'video-card';
|
|
||||||
card.id = key
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<h3 title="${video.source_path}" align="center">${video.source_file_name} - ${video.target_file_name}</h3>
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-bar"></div>
|
|
||||||
</div>
|
|
||||||
<br/>
|
|
||||||
<div class="video-card-values">
|
|
||||||
<div title="Anzahl der verarbeiteten Frames" class="video-card-values-items"><b>Frames:</b> <span class="frames">0</span> Anz</div>
|
|
||||||
<div title="Dateigröße" class="video-card-values-items"><b>Größe:</b> <span class="size">0</span> <span class="size_unit"></span></div>
|
|
||||||
<div title="Verarbeitungsgeschwindigkeit in Frames/Sekunde" class="video-card-values-items"><b>FPS:</b> <span class="fps">0</span></div>
|
|
||||||
<div title="Qualitätswert: je niedriger, desto besser, nur bei bestimmten Codecs sichtbar" class="video-card-values-items"><b>Quanitzer:</b> <span class="quantizer">0</span></div>
|
|
||||||
<div title="Menge an Daten pro Sekunde, die für das Video verwendet wird" class="video-card-values-items"><b>Bitrate:</b> <span class="bitrate">0</span> <span class="bitrate_unit"></span></div>
|
|
||||||
<div title="Verarbeitungsgeschwindigkeit im Vergleich zur Echtzeit (1.0x = Echtzeit)" class="video-card-values-items"><b>Speed:</b> <span class="speed">0</span></div>
|
|
||||||
<div title="Verbleibende Zeit" class="video-card-values-items"><b>Verbleibend:</b> <span class="time_remaining">0</span></div>
|
|
||||||
<div title="Position im Film" class="video-card-values-items"><b>Zeit:</b> <span class="time">0</span></div>
|
|
||||||
<div class="video-card-values-items delete-button"><!--<img src="/client/icons/muell-128.png" class="conversion_icons">--></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
|
|
||||||
active_Conversions.appendChild(card)
|
|
||||||
videoActive[key] = video;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWaitingSnake(packet){
|
|
||||||
const queue = document.getElementById('queue');
|
|
||||||
|
|
||||||
for (let key in videoQueue) {
|
|
||||||
if (!(key in packet.data_queue) || (videoQueue[key] && videoQueue[key].status !== packet.data_queue[key]?.status)) {
|
|
||||||
const elem = document.getElementById(key);
|
|
||||||
if (elem) {
|
|
||||||
elem.remove();
|
|
||||||
delete videoQueue[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(packet.data_queue).forEach(key => {
|
|
||||||
const video = packet.data_queue[key];
|
|
||||||
|
|
||||||
if(!videoQueue[key]){
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'queue_wait-card';
|
|
||||||
card.id = key
|
|
||||||
|
|
||||||
if(video.status === 1 || video.status === 2){
|
|
||||||
status_img = `<img src="/client/icons/fehler-96.png" class="status_icons">`
|
|
||||||
} else if(video.status === 3) {
|
|
||||||
status_img = `<img src="/client/icons/wait.gif" class="status_icons">`
|
|
||||||
} else {
|
|
||||||
status_img = `<img src="/client/icons/wait-100.png" class="status_icons">`
|
|
||||||
}
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="card_wait-inner">
|
|
||||||
<h3 title="${video.source_path}" align="center">${video.source_file_name}</h3>
|
|
||||||
<div id="menu_wait">
|
|
||||||
<div class="links">${status_img}</div>
|
|
||||||
<div class="rechts"><img src="/client/icons/muell-128.png" class="conversion_wait_icons" onclick="sendCommand('delete', ${key})"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
|
|
||||||
queue.appendChild(card)
|
|
||||||
videoQueue[key] = video;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateVideoElement(packet){
|
|
||||||
let video = packet.data_flow;
|
|
||||||
let container = document.getElementById(video.id);
|
|
||||||
|
|
||||||
container.querySelector(".frames").textContent = video.frames ?? 0;
|
|
||||||
container.querySelector(".size").textContent = video.size[0] || 0;
|
|
||||||
container.querySelector(".size_unit").textContent = video.size[1] || "KB";
|
|
||||||
container.querySelector(".fps").textContent = video.fps ?? 0;
|
|
||||||
container.querySelector(".quantizer").textContent = video.quantizer ?? 0;
|
|
||||||
container.querySelector(".bitrate").textContent = video.bitrate[0] || 0;
|
|
||||||
container.querySelector(".bitrate_unit").textContent = video.bitrate[1] || 0;
|
|
||||||
container.querySelector(".speed").textContent = video.speed ?? 0;
|
|
||||||
container.querySelector(".time_remaining").textContent = video.time_remaining ?? "0 Min";
|
|
||||||
container.querySelector(".time").textContent = video.time ?? "0 Min";
|
|
||||||
|
|
||||||
let progressBar = container.querySelector(".progress-bar");
|
|
||||||
let progress = video.loading; // Annahme: `loading` kommt als Zahl von 0 bis 100
|
|
||||||
progressBar.style.width = progress + "%"
|
|
||||||
|
|
||||||
if (videoActive[video.id].status === 3) {
|
|
||||||
progressBar.style.background = "linear-gradient(90deg, #4caf50, #00c853)";
|
|
||||||
} else if(videoActive[video.id].status === 0) {
|
|
||||||
progressBar.style.background = "#4caf50";
|
|
||||||
} else if(videoActive[video.id].status === 1) {
|
|
||||||
progressBar.style.background = "#ff0000";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
/**
|
|
||||||
* @returns {Promise<{server_ip: string, server_port: number}>}
|
|
||||||
*/
|
|
||||||
async function getServerConfig() {
|
|
||||||
const response = await fetch('/api/ip');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMediaStat() {
|
|
||||||
const response = await fetch('/api/stats');
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleStatHidden() {
|
|
||||||
const section_stat = document.getElementById("stat")
|
|
||||||
section_stat.hidden = !section_stat.hidden
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPopup() {
|
|
||||||
document.getElementById("popup").style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function closePopup() {
|
|
||||||
document.getElementById("popup").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
getMediaStat()
|
|
||||||
.then(data => {
|
|
||||||
console.log("Antwort von /api/stats:", data); // Debug-Ausgabe
|
|
||||||
|
|
||||||
if (!data || typeof data !== 'object' || !data.videos || typeof data.videos !== 'object') {
|
|
||||||
throw new Error("Ungültiges Antwortformat oder fehlende 'videos'-Eigenschaft");
|
|
||||||
}
|
|
||||||
|
|
||||||
const stat_Container = document.getElementById('stat');
|
|
||||||
|
|
||||||
Object.keys(data.videos).forEach(key => {
|
|
||||||
const video = data.videos[key];
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'video-card';
|
|
||||||
card.id = key
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<h3 title="${video.source_file}">${video.source_file_name}</h3>
|
|
||||||
<div class="actions">
|
|
||||||
<button>Löschen</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip">
|
|
||||||
Quelle: ${video.source_file}<br>
|
|
||||||
Dauer: ${video.source_duration}<br>
|
|
||||||
Größe: ${video.source_size}<br>
|
|
||||||
FPS: ${video.source_frame_rate}<br>
|
|
||||||
Frames: ${video.source_frames_total}<br>
|
|
||||||
Start: ${video.process_start}<br>
|
|
||||||
Ende: ${video.process_end}<br>
|
|
||||||
Status: ${video.status}<br>
|
|
||||||
Zeit: ${video.process_time}<br>
|
|
||||||
Größe (Ziel): ${video.target_size}<br>
|
|
||||||
FPS (aktuell): ${video.stat_fps}<br>
|
|
||||||
Bitrate: ${video.stat_bitrate}<br>
|
|
||||||
Speed: ${video.stat_speed}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
stat_Container.appendChild(card)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Abrufen der Medienstatistik:', error);
|
|
||||||
});
|
|
||||||
10
index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Websocket Client</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="/client/client.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
SERVER="ws://192.168.155.110:8000/"
|
|
||||||
|
|
||||||
for FILE in "$@"; do
|
|
||||||
JSON=$(printf '{"data_path": "%s"}' "$FILE")
|
|
||||||
echo "$JSON" | websocat "$SERVER"
|
|
||||||
|
|
||||||
done
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
Type=Service
|
|
||||||
ServiceTypes=KonqPopupMenu/Plugin
|
|
||||||
MimeType=video/*
|
|
||||||
Actions=sendToWebSocket
|
|
||||||
|
|
||||||
[Desktop Action sendToWebSocket]
|
|
||||||
Name=Video to New Server
|
|
||||||
Exec=/home/data/.local/share/kio/servicemenus/send.path.sh %F
|
|
||||||
Icon=video-x-generic
|
|
||||||
10
package.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "client.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
PyYAML~=6.0.2
|
|
||||||
websockets~=15.0
|
|
||||||
asyncio~=3.4.3
|
|
||||||
aiohttp~=3.11.16
|
|
||||||