312 lines
9.8 KiB
Markdown
312 lines
9.8 KiB
Markdown
# Omnyx Backend Migrationsplan
|
|
|
|
## Ziel
|
|
|
|
Migration von PHP + MySQL zu Bun + Hono + Drizzle ORM für eine Hybrid-Architektur:
|
|
- **Lokal:** SQLite (Desktop/Mobile, offline-fähig)
|
|
- **Server:** PostgreSQL (Docker, Multi-User)
|
|
- **Desktop:** Tauri + Bun-Sidecar
|
|
- **Mobile:** Capacitor + SQLite via WASM
|
|
|
|
## Tech Stack
|
|
|
|
| Komponente | Technologie |
|
|
|---|---|
|
|
| **Runtime** | Bun (TypeScript-nativ, bun:sqlite built-in) |
|
|
| **Server** | Hono (leicht, Zod-Validation) |
|
|
| **ORM** | Drizzle ORM (bun:sqlite / node-postgres) |
|
|
| **DB lokal** | bun:sqlite |
|
|
| **DB Server** | PostgreSQL (Docker) |
|
|
| **Desktop** | Tauri + Bun-Sidecar |
|
|
| **Mobile** | Capacitor + @libsql/client-web (WASM) |
|
|
|
|
## Projektstruktur (separates Repo)
|
|
|
|
```
|
|
omnyx-backend/
|
|
├── src/
|
|
│ ├── index.ts ← Hono-Server + Router
|
|
│ ├── db/
|
|
│ │ ├── schema.ts ← Drizzle-Schema (alle Tabellen)
|
|
│ │ ├── index.ts ← DB-Connection (bun:sqlite / pg)
|
|
│ │ └── migrate.ts ← Migration runner
|
|
│ ├── routes/
|
|
│ │ ├── media.ts ← CRUD /api/media
|
|
│ │ ├── cast.ts ← CRUD /api/cast
|
|
│ │ └── settings.ts ← CRUD /api/settings
|
|
│ ├── middleware/
|
|
│ │ ├── cors.ts
|
|
│ │ └── error.ts
|
|
│ └── lib/
|
|
│ └── json-fields.ts ← Helper für JSON-Array-Felder
|
|
├── scripts/
|
|
│ └── migrate-from-php.ts ← Import-Skript von alter MySQL-DB
|
|
├── package.json
|
|
├── tsconfig.json
|
|
├── Dockerfile
|
|
└── docker-compose.yml
|
|
```
|
|
|
|
## Datenbank-Tabellen
|
|
|
|
### media
|
|
| Spalte | Typ | Anmerkung |
|
|
|--------|-----|-----------|
|
|
| id | integer | PK autoIncrement |
|
|
| title | text | NOT NULL |
|
|
| year | integer | NOT NULL |
|
|
| poster | text | nullable |
|
|
| banner | text | nullable |
|
|
| description | text | nullable |
|
|
| rating | real | nullable |
|
|
| category | text | nullable |
|
|
| type | text | default 'Movie' |
|
|
| status | text | default 'Released' |
|
|
| aspect_ratio | text | nullable |
|
|
| runtime | integer | nullable |
|
|
| director | text | nullable |
|
|
| writer | text | nullable |
|
|
| release_date | text | nullable |
|
|
| source | text | nullable |
|
|
| created_at | text | default datetime() |
|
|
| updated_at | text | default datetime() |
|
|
| genres | text | JSON-Array |
|
|
| tags | text | JSON-Array |
|
|
| studios | text | JSON-Array |
|
|
| categories | text | JSON-Array |
|
|
| series | text | JSON-Array |
|
|
| platforms | text | JSON-Array |
|
|
| developers | text | JSON-Array |
|
|
| completion_status | text | nullable |
|
|
| play_count | integer | default 0 |
|
|
| last_activity | text | nullable |
|
|
| playtime | integer | default 0 |
|
|
|
|
### episodes
|
|
| Spalte | Typ |
|
|
|--------|-----|
|
|
| id | integer PK |
|
|
| media_id | integer FK → media |
|
|
| season | integer |
|
|
| episode_number | integer |
|
|
| title | text |
|
|
| description | text |
|
|
| air_date | text |
|
|
| duration | integer |
|
|
| thumbnail | text |
|
|
|
|
### tracks
|
|
| Spalte | Typ |
|
|
|--------|-----|
|
|
| id | integer PK |
|
|
| media_id | integer FK → media |
|
|
| track_number | integer |
|
|
| title | text |
|
|
| duration | text (format: "M:SS") |
|
|
| artist | text |
|
|
|
|
### cast (staff)
|
|
| Spalte | Typ |
|
|
|--------|-----|
|
|
| id | integer PK |
|
|
| name | text NOT NULL |
|
|
| cleanname | text nullable |
|
|
| photo | text nullable |
|
|
| bio | text nullable |
|
|
| birth_date | text nullable |
|
|
| birth_place | text nullable |
|
|
| occupations | text (JSON-Array) |
|
|
| created_at | text |
|
|
| updated_at | text |
|
|
| media_types | text (JSON-Array) |
|
|
| bust_size | integer nullable |
|
|
| cup_size | text nullable |
|
|
| waist_size | integer nullable |
|
|
| hip_size | integer nullable |
|
|
| height | integer nullable |
|
|
| weight | integer nullable |
|
|
| hair_color | text nullable |
|
|
| eye_color | text nullable |
|
|
| ethnicity | text nullable |
|
|
|
|
### adult_specifics
|
|
| Spalte | Typ |
|
|
|--------|-----|
|
|
| id | integer PK |
|
|
| cast_id | integer FK → cast |
|
|
| tattoos | text nullable |
|
|
| piercings | text nullable |
|
|
| measurements | text nullable |
|
|
| shoe_size | integer nullable |
|
|
|
|
### media_cast_staff (Junction)
|
|
| Spalte | Typ |
|
|
|--------|-----|
|
|
| media_id | integer FK |
|
|
| cast_id | integer FK |
|
|
| role | text |
|
|
| character_name | text nullable |
|
|
| character_image | text nullable |
|
|
| PK | (media_id, cast_id) |
|
|
|
|
### cast_media_filmography (Junction)
|
|
| Spalte | Typ |
|
|
|--------|-----|
|
|
| cast_id | integer FK |
|
|
| media_id | integer FK |
|
|
| role | text |
|
|
| character_name | text nullable |
|
|
|
|
### settings
|
|
| Spalte | Typ |
|
|
|--------|-----|
|
|
| id | integer PK |
|
|
| enabled_categories | text (JSON-Array) |
|
|
| items_per_page | integer default 20 |
|
|
| grid_item_size | integer default 5 |
|
|
| default_view | text default 'grid' |
|
|
| show_adult_content | integer (boolean) default 0 |
|
|
| auto_play_trailers | integer (boolean) default 0 |
|
|
| language | text default 'en' |
|
|
| theme | text default 'system' |
|
|
| jellyfin_library_mappings | text nullable |
|
|
| page_title | text nullable |
|
|
| favicon | text nullable |
|
|
| custom_colors | text nullable (JSON) |
|
|
| created_at | text |
|
|
| updated_at | text |
|
|
|
|
## API-Endpoints (1:1 von PHP)
|
|
|
|
### Media
|
|
```
|
|
GET /api/media?page=1&limit=10000 → PaginatedResponse<ApiMediaItem>
|
|
GET /api/media/:id → ApiResponse<ApiMediaItem>
|
|
POST /api/media → ApiResponse<ApiMediaItem>
|
|
PUT /api/media/:id → ApiResponse<ApiMediaItem>
|
|
DELETE /api/media/:id → ApiResponse<{message}>
|
|
```
|
|
|
|
### Cast
|
|
```
|
|
GET /api/cast?page=1&limit=100000 → PaginatedResponse<ApiCastItem>
|
|
GET /api/cast/:id → ApiResponse<ApiCastItem>
|
|
GET /api/cast/:id/media → PaginatedResponse<ApiMediaItem>
|
|
POST /api/cast → ApiResponse<ApiCastItem>
|
|
PUT /api/cast/:id → ApiResponse<ApiCastItem>
|
|
DELETE /api/cast/:id → ApiResponse<{message}>
|
|
```
|
|
|
|
### Settings
|
|
```
|
|
GET /api/settings → ApiResponse<ApiSettingsItem>
|
|
POST /api/settings → ApiResponse<ApiSettingsItem>
|
|
PUT /api/settings → ApiResponse<ApiSettingsItem>
|
|
```
|
|
|
|
## Response-Format (identisch zu PHP)
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": { ... }
|
|
}
|
|
```
|
|
|
|
## DB-Connection (Driver-Abstraktion)
|
|
|
|
```typescript
|
|
if (process.env.DATABASE_URL) {
|
|
// Server-Modus → PostgreSQL
|
|
const { Pool } = await import('pg');
|
|
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
db = drizzle(new Pool({ connectionString: process.env.DATABASE_URL }));
|
|
} else {
|
|
// Lokaler Modus → bun:sqlite
|
|
const { Database } = await import('bun:sqlite');
|
|
const { drizzle } = await import('drizzle-orm/bun-sqlite');
|
|
const sqlite = new Database('./data/omnyx.db', { create: true });
|
|
sqlite.exec('PRAGMA journal_mode=WAL;');
|
|
db = drizzle(sqlite);
|
|
}
|
|
```
|
|
|
|
## Desktop-Bundle (Tauri)
|
|
|
|
- Bun-Binary via `bun build --compile` (~50MB Single-File)
|
|
- Tauri startet Binary als Sidecar
|
|
- React-Frontend (produktions-Build) in Tauri-WebView
|
|
- API auf `localhost:3001`
|
|
|
|
## Mobile (Capacitor + WASM SQLite)
|
|
|
|
- SQLite läuft direkt im WebView via `@libsql/client/web`
|
|
- Drizzle-Queries identisch zum Backend
|
|
- Kein separater Server nötig
|
|
- Optional: Sync mit Server bei Internet-Verbindung
|
|
|
|
## Docker-Deployment
|
|
|
|
```yaml
|
|
services:
|
|
backend:
|
|
build: .
|
|
environment:
|
|
DATABASE_URL: postgres://user:pass@db:5432/omnyx
|
|
db:
|
|
image: postgres:16-alpine
|
|
```
|
|
|
|
## Status (23.05.2026)
|
|
|
|
| Phase | Beschreibung | Status |
|
|
|---|---|---|
|
|
| P1 | Backend-Projekt (Bun + Hono + Drizzle) | ✅ **Fertig** |
|
|
| P2 | Hono-Routen (Media, Cast, Settings) | ✅ **Fertig** |
|
|
| P3 | Tests + TypeScript-Lint | ✅ **Fertig** |
|
|
| P4 | Docker-Config aktualisiert | ✅ **Fertig** |
|
|
| P5 | Tauri Desktop (Bun-Sidecar) | ✅ **Fertig** |
|
|
| P6 | PostgreSQL-Support + Deployment | ⏳ Ausstehend |
|
|
| P7 | Sync-Logik (Hybrid) | ⏳ Optional |
|
|
| P8 | Capacitor Mobile (lokales SQLite) | ⏳ Ausstehend |
|
|
|
|
### Aktuelle Struktur
|
|
|
|
```
|
|
G:\kyoo\
|
|
├── backend/ ← Bun + Hono + Drizzle
|
|
│ ├── src/
|
|
│ │ ├── index.ts ← Hono-Server (Port 3001)
|
|
│ │ ├── db/
|
|
│ │ │ ├── schema.ts ← Drizzle-Schema (9 Tabellen)
|
|
│ │ │ ├── index.ts ← DB-Connection (bun:sqlite / pg-fähig)
|
|
│ │ │ └── migrate.ts ← Auto-Migration (CREATE TABLE IF NOT EXISTS)
|
|
│ │ ├── routes/
|
|
│ │ │ ├── media.ts ← CRUD /api/media
|
|
│ │ │ ├── cast.ts ← CRUD /api/cast + POST /adult
|
|
│ │ │ ├── settings.ts ← CRUD /api/settings
|
|
│ │ │ └── images.ts ← Proxy-/Cache-Endpunkt /api/images/proxy
|
|
│ │ ├── lib/converters.ts ← Type-Converter
|
|
│ │ └── types.ts ← API-Types
|
|
│ ├── uploads/ ← Gecachte Bilder (offline-fähig)
|
|
│ ├── dist-exe/ ← Compiled binary (~50MB)
|
|
│ ├── Dockerfile ← Bun-basiert
|
|
│ ├── package.json
|
|
│ └── README.md
|
|
├── frontend/ ← React SPA + Tauri-Desktop-Shell
|
|
│ ├── src-tauri/ ← Tauri v2 Desktop-Konfiguration
|
|
│ │ ├── src/lib.rs ← Sidecar-Spawn (omnyx-backend)
|
|
│ │ ├── binaries/ ← Kompilierte Binaries pro Target
|
|
│ │ ├── tauri.conf.json ← Window 1280x800, externalBin
|
|
│ │ ├── capabilities/ ← shell:default, shell:allow-spawn
|
|
│ │ └── Cargo.toml ← tauri-plugin-shell
|
|
│ └── .env.tauri ← VITE_API_URL=http://localhost:3001
|
|
└── docker-compose.yml
|
|
```
|
|
|
|
### Nächste Schritte
|
|
|
|
1. **PostgreSQL-Support** (P6) — pg-Treiber für Drizzle, docker-compose mit Postgres
|
|
2. **Capacitor Mobile** (P8) — Android-App mit embedded SQLite via WASM
|
|
3. **Sticky Footer Bug** — Footer klebt nicht am unteren Rand bei kurzen Seiten
|
|
4. **PUT /api/cast/:id** um adult-Felder erweitern (height, weight etc. aktuell nicht updatbar)
|