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.
|
3. Lies **ROADMAP.md** — das sagt, in welcher Reihenfolge es gebaut wird.
|
||||||
4. Fang mit Etappe 1 an (Fundament + udev-Erkennung).
|
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.
|
Alles andere steht in den Docs. Viel Spaß beim Bauen.
|
||||||
|
|||||||
+7
-5
@@ -11,15 +11,17 @@
|
|||||||
**Was gebaut wird:**
|
**Was gebaut wird:**
|
||||||
- Docker Compose mit `api`, `worker`, `ui`, `postgres`, `redis`
|
- Docker Compose mit `api`, `worker`, `ui`, `postgres`, `redis`
|
||||||
- Basis-Dockerfiles für jeden Service (Python/FastAPI, Python/Celery, Node/React, PostgreSQL, 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>`
|
- Device-Resolver: ermittelt UUID/Serial des Laufwerks, erzeugt Symlink `/dev/disc/<uuid>`
|
||||||
- Job-Erstellung in Celery-Queue mit Disc-Typ und Device-Pfad
|
- Job-Erstellung in Celery-Queue mit Disc-Typ und Device-Pfad
|
||||||
|
|
||||||
**Fertig wenn:**
|
**Fertig wenn:**
|
||||||
- `docker compose up` startet alle 5 Container
|
- ✅ `docker compose up` startet alle 5 Container
|
||||||
- `makejungles` liest die TOC einer eingelegten Disc
|
- ✅ udev-Regel erkennt Disc-Einwurf
|
||||||
- udev-Event löst Job-Erstellung aus
|
- ✅ udev-Daemon erstellt Celery-Job
|
||||||
- Celery-Worker nimmt den Job entgegen und gibt "Disc erkannt: DVD, Titel: 'xyz'" aus
|
- ✅ 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
|
## 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
|
## Nächste Schritte
|
||||||
|
|
||||||
1. **Etappe 1: Fundament** — Container-Infrastruktur + udev-Erkennung.
|
### Etappe 2: Ripping-Pipeline
|
||||||
- Docker Compose mit 5 Containern (api, worker, ui, postgres, redis)
|
- CD-Ripping via `abcde` → FLAC + AcoustID + MusicBrainz
|
||||||
- udev-Regel + Daemon für Disc-Einwurf-Erkennung
|
- DVD/Blu-ray-Ripping via `makemkvcon` → MKV
|
||||||
- Device-Resolver (UUID/Serial)
|
- Ripping-Status über Celery an SSE-Stream
|
||||||
- Basis-Celery-Job-Erstellung
|
- 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)?
|
- Wie soll die udev-Integration konkret aussehen? (Host-System vs. Container)
|
||||||
- Go oder Python für den udev-Daemon? Go ist schneller, Python ist einfacher.
|
- Soll MakeMKV im Worker-Container integriert werden?
|
||||||
- Welche Port-Nummern? Standard (80/443/5432/6379) oder custom?
|
- Soll HandBrake für Transcoding integriert werden (Etappe 2+)?
|
||||||
- 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 |
|
|
||||||
|
|||||||
@@ -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