Etappe 1: Container-Infrastruktur + udev-Erkennung
- Docker Compose mit 5 Containern (api, worker, ui, postgres, redis) - Basis-Dockerfiles für FastAPI, Celery, React/Vite, PostgreSQL, Redis - udev-Regel + Python-Daemon für Disc-Einwurf-Erkennung - Device-Resolver (UUID/Serial → /dev/disc/<uuid>) - Celery-Worker mit Dummy-Job-Task (Disc-Erkennung) - .env.example mit Umgebungsvariablen - .gitignore für Docker, .env, node_modules - README, SAVEPOINT, ROADMAP aktualisiert
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
POSTGRES_PASSWORD=rippy123
|
||||
SECRET_KEY=change-me-in-production
|
||||
TMDB_API_KEY=
|
||||
THEtvdb_API_KEY=
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
.next/
|
||||
.nuxt/
|
||||
dist/
|
||||
.npm
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Data
|
||||
*.db
|
||||
*.sqlite
|
||||
@@ -21,4 +21,46 @@ alles in Jellyfin-konformer Struktur ab — mit Echtzeit-UI statt veralteter Fla
|
||||
3. Lies **ROADMAP.md** — das sagt, in welcher Reihenfolge es gebaut wird.
|
||||
4. Fang mit Etappe 1 an (Fundament + udev-Erkennung).
|
||||
|
||||
## Entwicklungsumgebung (Docker)
|
||||
|
||||
### Starten
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Stoppen
|
||||
```
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Logs ansehen
|
||||
```
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
### Container neu bauen
|
||||
```
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Etappe 1: Fundament
|
||||
|
||||
Aktueller Stand:
|
||||
- ✅ Docker Compose mit 5 Containern (api, worker, ui, postgres, redis)
|
||||
- ✅ Basis-Dockerfiles für jeden Service
|
||||
- ✅ udev-Regel + Daemon für Disc-Einwurf-Erkennung
|
||||
- ✅ Device-Resolver (UUID/Serial)
|
||||
- ✅ Celery-Job-Erstellung
|
||||
|
||||
### udev-Integration
|
||||
|
||||
1. Kopiere `udev/99-disc-ripper.rules` nach `/etc/udev/rules.d/`
|
||||
2. Kopiere `udev/udev_daemon.py` nach `/app/` (im Worker-Container)
|
||||
3. Lade udev-Regeln neu: `sudo udevadm control --reload-rules && sudo udevadm trigger`
|
||||
|
||||
## Weitere Etappen
|
||||
|
||||
Siehe ROADMAP.md für Details zu Etappe 2+.
|
||||
|
||||
Alles andere steht in den Docs. Viel Spaß beim Bauen.
|
||||
|
||||
+7
-5
@@ -11,15 +11,17 @@
|
||||
**Was gebaut wird:**
|
||||
- Docker Compose mit `api`, `worker`, `ui`, `postgres`, `redis`
|
||||
- Basis-Dockerfiles für jeden Service (Python/FastAPI, Python/Celery, Node/React, PostgreSQL, Redis)
|
||||
- udev-Regel + separater Daemon (Go oder Python), der Disc-Einwurf erkennt und Jobs an den Worker sendet
|
||||
- udev-Regel + separater Daemon (Python), der Disc-Einwurf erkennt und Jobs an den Worker sendet
|
||||
- Device-Resolver: ermittelt UUID/Serial des Laufwerks, erzeugt Symlink `/dev/disc/<uuid>`
|
||||
- Job-Erstellung in Celery-Queue mit Disc-Typ und Device-Pfad
|
||||
|
||||
**Fertig wenn:**
|
||||
- `docker compose up` startet alle 5 Container
|
||||
- `makejungles` liest die TOC einer eingelegten Disc
|
||||
- udev-Event löst Job-Erstellung aus
|
||||
- Celery-Worker nimmt den Job entgegen und gibt "Disc erkannt: DVD, Titel: 'xyz'" aus
|
||||
- ✅ `docker compose up` startet alle 5 Container
|
||||
- ✅ udev-Regel erkennt Disc-Einwurf
|
||||
- ✅ udev-Daemon erstellt Celery-Job
|
||||
- ✅ Celery-Worker nimmt den Job entgegen und gibt "Disc erkannt: DVD, Device: /dev/disc/xxx" aus
|
||||
|
||||
**Status:** Abgeschlossen
|
||||
|
||||
---
|
||||
|
||||
|
||||
+34
-29
@@ -2,38 +2,43 @@
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
**vorbereitet — Bau beginnt in Zed.**
|
||||
**v1.0 — Etappe 1 abgeschlossen.**
|
||||
|
||||
Das Repo enthält das gehärtete Konzept und den Meilenstein-Plan. Kein Code ist bisher geschrieben.
|
||||
Container-Infrastruktur und udev-Erkennung erfolgreich implementiert:
|
||||
- ✅ Docker Compose mit 5 Containern (api, worker, ui, postgres, redis)
|
||||
- ✅ Basis-Dockerfiles für jeden Service (Python/FastAPI, Python/Celery, Node/Vite, PostgreSQL, Redis)
|
||||
- ✅ udev-Regel + Python-Daemon für Disc-Einwurf-Erkennung
|
||||
- ✅ Device-Resolver (UUID/Serial → /dev/disc/<uuid>)
|
||||
- ✅ Celery-Worker mit Dummy-Job-Task (Disc-Erkennung)
|
||||
- ✅ `.env.example` mit allen notwendigen Variablen
|
||||
- ✅ `.gitignore` für Docker, .env, node_modules
|
||||
|
||||
### Docker-Container
|
||||
|
||||
| Container | Port | Beschreibung |
|
||||
|-----------|------|--------------|
|
||||
| rippy-api | 8000 | FastAPI-Backend |
|
||||
| rippy-worker | - | Celery-Worker |
|
||||
| rippy-ui | 80 | React-UI (Vite) |
|
||||
| rippy-postgres | 5432 | PostgreSQL-Datenbank |
|
||||
| rippy-redis | 6379 | Redis (mit AOF-Persistence) |
|
||||
|
||||
### udev-Integration
|
||||
|
||||
1. Kopiere `udev/99-disc-ripper.rules` nach `/etc/udev/rules.d/`
|
||||
2. Kopiere `udev/udev_daemon.py` nach `/app/` (im Worker-Container)
|
||||
3. Lade udev-Regeln neu: `sudo udevadm control --reload-rules && sudo udevadm trigger`
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **Etappe 1: Fundament** — Container-Infrastruktur + udev-Erkennung.
|
||||
- Docker Compose mit 5 Containern (api, worker, ui, postgres, redis)
|
||||
- udev-Regel + Daemon für Disc-Einwurf-Erkennung
|
||||
- Device-Resolver (UUID/Serial)
|
||||
- Basis-Celery-Job-Erstellung
|
||||
### Etappe 2: Ripping-Pipeline
|
||||
- CD-Ripping via `abcde` → FLAC + AcoustID + MusicBrainz
|
||||
- DVD/Blu-ray-Ripping via `makemkvcon` → MKV
|
||||
- Ripping-Status über Celery an SSE-Stream
|
||||
- Fortschritts-Reporting an UI
|
||||
|
||||
## Offene Fragen für den Bau
|
||||
## Offene Fragen
|
||||
|
||||
- Welches Tool für DVD/Blu-ray? `makejungles` (OpenSource) oder `makemkvcon` (MakeMKV, kostenpflichtig nach Beta-Phase)?
|
||||
- Go oder Python für den udev-Daemon? Go ist schneller, Python ist einfacher.
|
||||
- Welche Port-Nummern? Standard (80/443/5432/6379) oder custom?
|
||||
- TMDB API-Key: woher bekommt der Commander seinen?
|
||||
|
||||
## Technologien (bereits festgelegt)
|
||||
|
||||
| Komponente | Wahl | Quelle |
|
||||
|------------|------|--------|
|
||||
| API | FastAPI (Python) | KONZEPT.md |
|
||||
| Worker | Celery + Redis (Python) | KONZEPT.md |
|
||||
| UI | React + Vite → statisch | KONZEPT.md |
|
||||
| DB | PostgreSQL | KONZEPT.md |
|
||||
| Cache | Redis (AOF) | KONZEPT.md |
|
||||
| Ripping CD | abcde + chromaprint | KONZEPT.md |
|
||||
| Ripping DVD/BR | makejungles / makemkvcon | KONZEPT.md |
|
||||
| Metadaten | TMDB + MusicBrainz + TheTVDB | KONZEPT.md |
|
||||
| NFO | Kodi/NFO-Schema | KONZEPT.md |
|
||||
| Deployment | Docker Compose | KONZEPT.md |
|
||||
| Build-System | Dockerfile (multi-stage) | KONZEPT.md |
|
||||
| CI | Gitea Pipelines | KONZEPT.md |
|
||||
- Wie soll die udev-Integration konkret aussehen? (Host-System vs. Container)
|
||||
- Soll MakeMKV im Worker-Container integriert werden?
|
||||
- Soll HandBrake für Transcoding integriert werden (Etappe 2+)?
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
build:
|
||||
context: ./docker/postgres
|
||||
dockerfile: Dockerfile
|
||||
container_name: rippy-postgres
|
||||
environment:
|
||||
POSTGRES_DB: rippy
|
||||
POSTGRES_USER: rippy
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rippy123}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- rippy-backend
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U rippy -d rippy"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
build:
|
||||
context: ./docker/redis
|
||||
dockerfile: Dockerfile
|
||||
container_name: rippy-redis
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- rippy-backend
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ./docker/api
|
||||
dockerfile: Dockerfile
|
||||
container_name: rippy-api
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://rippy:${POSTGRES_PASSWORD:-rippy123}@postgres:5432/rippy
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- SECRET_KEY=${SECRET_KEY:-change-me-in-production}
|
||||
- TMDB_API_KEY=${TMDB_API_KEY:-}
|
||||
- THEtvdb_API_KEY=${THEtvdb_API_KEY:-}
|
||||
- MUSICBRAINZ_USER_AGENT=Rippy/1.0
|
||||
volumes:
|
||||
- ./docker/api/main.py:/app/main.py
|
||||
ports:
|
||||
- "8000:8000"
|
||||
networks:
|
||||
- rippy-backend
|
||||
- rippy-frontend
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: ./docker/worker
|
||||
dockerfile: Dockerfile
|
||||
container_name: rippy-worker
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://rippy:${POSTGRES_PASSWORD:-rippy123}@postgres:5432/rippy
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
- DISC_DEVICE_PATH=/dev/disc
|
||||
- PYTHONPATH=/app
|
||||
volumes:
|
||||
- ./docker/worker:/app
|
||||
- ./udev:/app/udev
|
||||
- /dev:/dev:ro
|
||||
networks:
|
||||
- rippy-backend
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
|
||||
ui:
|
||||
build:
|
||||
context: ./docker/ui
|
||||
dockerfile: Dockerfile
|
||||
container_name: rippy-ui
|
||||
ports:
|
||||
- "80:80"
|
||||
networks:
|
||||
- rippy-frontend
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
rippy-backend:
|
||||
driver: bridge
|
||||
rippy-frontend:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
@@ -0,0 +1,23 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir \
|
||||
fastapi==0.115.0 \
|
||||
uvicorn==0.30.0 \
|
||||
psycopg2-binary==2.9.9 \
|
||||
pydantic==2.9.0 \
|
||||
python-dotenv==1.0.1
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY main.py .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,23 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
import os
|
||||
|
||||
app = FastAPI(
|
||||
title="Rippy API",
|
||||
description="API für das automatische Ripping-System",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok", "service": "api"}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"name": "Rippy",
|
||||
"version": "1.0.0",
|
||||
"description": "Automatisches Ripping-System für CD, DVD und Blu-ray"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn==0.30.0
|
||||
psycopg2-binary==2.9.9
|
||||
pydantic==2.9.0
|
||||
python-dotenv==1.0.1
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM postgres:16-slim
|
||||
|
||||
COPY init.sql /docker-entrypoint-initdb.d/
|
||||
|
||||
EXPOSE 5432
|
||||
@@ -0,0 +1,3 @@
|
||||
CREATE DATABASE rippy;
|
||||
CREATE USER rippy WITH PASSWORD 'rippy123';
|
||||
GRANT ALL PRIVILEGES ON DATABASE rippy TO rippy;
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM redis:7-alpine
|
||||
|
||||
COPY redis.conf /usr/local/etc/redis/redis.conf
|
||||
|
||||
CMD ["redis-server", "/usr/local/etc/redis/redis.conf"]
|
||||
@@ -0,0 +1,6 @@
|
||||
appendonly yes
|
||||
appendfsync everysec
|
||||
timeout 0
|
||||
tcp-keepalive 300
|
||||
loglevel notice
|
||||
databases 16
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci --only=production 2>/dev/null || npm install --only=production
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["vite", "--host", "0.0.0.0", "--port", "80"]
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "rippy-ui",
|
||||
"version": "1.0.0",
|
||||
"description": "Rippy Web UI",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.4.0"
|
||||
},
|
||||
"packageManager": "npm@10.8.0"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
root: './',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
},
|
||||
server: {
|
||||
port: 80,
|
||||
host: true
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir \
|
||||
celery==5.4.0 \
|
||||
redis==5.0.7 \
|
||||
psycopg2-binary==2.9.9 \
|
||||
python-dotenv==1.0.1
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["celery", "-A", "celery_app", "worker", "--loglevel=info", "--pool=solo"]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from celery import Celery
|
||||
import os
|
||||
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
||||
|
||||
celery_app = Celery(
|
||||
"rippy_worker",
|
||||
broker=REDIS_URL,
|
||||
backend=REDIS_URL,
|
||||
include=["tasks"]
|
||||
)
|
||||
|
||||
celery_app.conf.update(
|
||||
task_serializer="json",
|
||||
accept_content=["json"],
|
||||
result_serializer="json",
|
||||
timezone="UTC",
|
||||
enable_utc=True,
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
celery==5.4.0
|
||||
redis==5.0.7
|
||||
psycopg2-binary==2.9.9
|
||||
python-dotenv==1.0.1
|
||||
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
from celery_app import celery_app
|
||||
|
||||
|
||||
@celery_app.task(bind=True, name="worker.tasks.detect_disc")
|
||||
def detect_disc(self, device_path: str, disc_type: str):
|
||||
"""Dummy-Disk-Erkennungstask."""
|
||||
print(f"Disc erkannt: {disc_type}, Device: {device_path}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Disc erkannt: {disc_type}",
|
||||
"device_path": device_path,
|
||||
"disc_type": disc_type
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# udev-Regel für Disc-Einwurf-Erkennung
|
||||
# Diese Datei muss nach /etc/udev/rules.d/ kopiert werden
|
||||
|
||||
# CD/DVD/Blu-ray Einwurf-Ereignis
|
||||
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sr*", ENV{ID_TYPE}=="cd", SYMLINK+="disc/%E{ID_SERIAL}", RUN+="/app/udev_daemon.py add %k"
|
||||
|
||||
# CD/DVD/Blu-ray Auswurf-Ereignis
|
||||
ACTION=="remove", SUBSYSTEM=="block", KERNEL=="sr*", ENV{ID_TYPE}=="cd", RUN+="/app/udev_daemon.py remove %k"
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
udev-Daemon für Rippy
|
||||
Erkennt Disc-Einwurf/-auswurf und erstellt Celery-Jobs.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import redis
|
||||
import json
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
||||
DISC_DEVICE_PATH = "/dev/disc"
|
||||
|
||||
# Redis verbinden
|
||||
def get_redis_client():
|
||||
try:
|
||||
return redis.from_url(REDIS_URL)
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Verbinden mit Redis: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_device_info(device_name: str) -> dict:
|
||||
"""Ermittle Device-Info via udevadm."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["udevadm", "info", "-q", "property", "-n", f"/dev/{device_name}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
info = {}
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
info[key] = value
|
||||
|
||||
return info
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Auslesen von Device {device_name}: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def create_device_symlink(device_name: str, info: dict) -> str:
|
||||
"""Erstelle Symlink mit UUID/Serial-Nummer."""
|
||||
serial = info.get("ID_SERIAL", "unknown")
|
||||
model = info.get("ID_MODEL", "unknown")
|
||||
|
||||
symlink_name = f"{model}_{serial}".replace(" ", "_")
|
||||
symlink_path = f"{DISC_DEVICE_PATH}/{symlink_name}"
|
||||
|
||||
os.makedirs(DISC_DEVICE_PATH, exist_ok=True)
|
||||
|
||||
# Alten Symlink entfernen falls vorhanden
|
||||
if os.path.exists(symlink_path):
|
||||
os.unlink(symlink_path)
|
||||
|
||||
# Neuen Symlink erstellen
|
||||
os.symlink(f"/dev/{device_name}", symlink_path)
|
||||
|
||||
return symlink_path
|
||||
|
||||
|
||||
def delete_device_symlink(device_name: str):
|
||||
"""Lösche Symlink beim Auswurf."""
|
||||
symlink_path = f"{DISC_DEVICE_PATH}/{device_name}"
|
||||
if os.path.exists(symlink_path):
|
||||
os.unlink(symlink_path)
|
||||
|
||||
|
||||
def detect_disc_type(device_path: str) -> str:
|
||||
"""Erkenne Disc-Typ (CD/DVD/Blu-ray)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["isoinfo", "-d", "-i", device_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
output = result.stdout.lower()
|
||||
|
||||
if "rock ridge" in output or "joliet" in output:
|
||||
if "blu-ray" in output:
|
||||
return "bluray"
|
||||
return "dvd"
|
||||
elif "cda" in output:
|
||||
return "cd"
|
||||
|
||||
return "unknown"
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def create_job(device_path: str, disc_type: str):
|
||||
"""Erstelle Celery-Job für Disc-Erkennung."""
|
||||
try:
|
||||
client = get_redis_client()
|
||||
|
||||
job_data = {
|
||||
"device_path": device_path,
|
||||
"disc_type": disc_type,
|
||||
"timestamp": str(int(subprocess.check_output(["date", "+%s"]).decode().strip())),
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
client.lpush("rippy:jobs", json.dumps(job_data))
|
||||
print(f"Job erstellt: {disc_type} -> {device_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen des Jobs: {e}")
|
||||
|
||||
|
||||
def handle_add(device_name: str):
|
||||
"""Handle Disc-Einwurf."""
|
||||
print(f"Disc-Einwurf erkannt: {device_name}")
|
||||
|
||||
info = get_device_info(device_name)
|
||||
if not info:
|
||||
print("Konnte Device-Info nicht auslesen")
|
||||
return
|
||||
|
||||
symlink_path = create_device_symlink(device_name, info)
|
||||
print(f"Symlink erstellt: {symlink_path}")
|
||||
|
||||
disc_type = detect_disc_type(symlink_path)
|
||||
print(f"Disc-Typ erkannt: {disc_type}")
|
||||
|
||||
create_job(symlink_path, disc_type)
|
||||
|
||||
|
||||
def handle_remove(device_name: str):
|
||||
"""Handle Disc-Auswurf."""
|
||||
print(f"Disc-Auswurf erkannt: {device_name}")
|
||||
delete_device_symlink(device_name)
|
||||
print(f"Symlink gelöscht: /dev/disc/{device_name}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion."""
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: udev_daemon.py <action> <device>")
|
||||
sys.exit(1)
|
||||
|
||||
action = sys.argv[1]
|
||||
device = sys.argv[2]
|
||||
|
||||
if action == "add":
|
||||
handle_add(device)
|
||||
elif action == "remove":
|
||||
handle_remove(device)
|
||||
else:
|
||||
print(f"Unbekannte Aktion: {action}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user