tauri
@@ -0,0 +1,311 @@
|
||||
# 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)
|
||||
@@ -102,6 +102,104 @@ VITE_PLAYNITE_API_TOKEN="your-api-token"
|
||||
1. Ensure XBVR is running and accessible
|
||||
2. Configure the URL in your `.env` file
|
||||
|
||||
## Desktop App (Tauri) — Vollständig offline nutzbar
|
||||
|
||||
Omnyx ist eine einzige Desktop-App — der Nutzer installiert sie und bekommt ein Fenster mit der Benutzeroberfläche. Backend und Frontend sind in einer Anwendung vereint, **kein externer Server nötig**.
|
||||
|
||||
> ⚠️ **Wichtig:** Es entstehen *zwei* verschiedene `.exe`-Dateien (Tauri-Shell + Backend-Sidecar), aber **der Nutzer startet nur die Omnyx-App**. Das Backend läuft automatisch unsichtbar im Hintergrund.
|
||||
|
||||
### Architektur (Entwickler-Perspektive)
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────┐
|
||||
│ Omnyx Desktop App │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ Tauri-WebView │ ← Nutzer sieht │
|
||||
│ │ (React-Frontend) │ DAS │
|
||||
│ │ │ │
|
||||
│ │ API → localhost:3001│ │
|
||||
│ └────────┬────────────┘ │
|
||||
│ │ (unsichtbar) │
|
||||
│ ┌────────▼────────────┐ │
|
||||
│ │ Bun-Sidecar │ ← läuft im │
|
||||
│ │ (Backend, Port 3001)│ Hintergrund │
|
||||
│ │ │ │
|
||||
│ │ SQLite-Datenbank │ │
|
||||
│ │ data/omnyx.db │ │
|
||||
│ └─────────────────────┘ │
|
||||
└───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Für den **Endnutzer** sieht es so aus: Doppelklick auf "Omnyx" → ein Fenster öffnet sich → fertig.
|
||||
|
||||
### Voraussetzungen (zum Bauen)
|
||||
|
||||
- [Bun](https://bun.sh/) ≥ 1.3 (Backend-Kompilierung)
|
||||
- [Rust](https://rustup.rs/) ≥ 1.77 (Tauri-Build)
|
||||
- Node.js ≥ 18
|
||||
|
||||
### One-Click-Build
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run desktop:build
|
||||
```
|
||||
|
||||
Macht alles automatisch: Backend kompilieren → Binary kopieren → Tauri-App bauen → Installer erzeugen.
|
||||
|
||||
### Schritt für Schritt
|
||||
|
||||
#### 1. Backend-Binary kompilieren
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
bun install
|
||||
bun run compile:tauri
|
||||
```
|
||||
|
||||
Erzeugt `backend/dist-exe/omnyx-backend.exe` (~50 MB Single-File) und kopiert es automatisch in `frontend/src-tauri/binaries/`.
|
||||
|
||||
Der Dateiname muss dem [Tauri-Target-Triple](https://v2.tauri.app/develop/sidecars/#target-triples) entsprechen:
|
||||
- Windows: `omnyx-backend-x86_64-pc-windows-msvc.exe`
|
||||
- Linux: `omnyx-backend-x86_64-unknown-linux-gnu`
|
||||
|
||||
#### 2. Desktop-App bauen (Produktion)
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
Erzeugt Installer in `src-tauri/target/release/bundle/`:
|
||||
- `nsis\Omnyx_0.1.0_x64-setup.exe` — Windows-Installer
|
||||
- `msi\Omnyx_0.1.0_x64_en-US.msi` — MSI-Paket
|
||||
|
||||
#### 3. Desktop-App testen (Dev-Modus)
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
Startet ein natives Fenster, Vite-Dev-Server (Port 3000) und Backend-Sidecar (Port 3001) — mit Hot-Reload.
|
||||
|
||||
### Konfiguration
|
||||
|
||||
Für Tauri wird automatisch die Datei `.env.tauri` verwendet (via `--mode tauri`):
|
||||
|
||||
```env
|
||||
# Lokaler Backend-Port
|
||||
VITE_API_URL=http://localhost:3001
|
||||
# Externe Importer (optional — nur bei Netzwerk-Zugriff)
|
||||
VITE_XBVR_URL=http://localhost:4080
|
||||
VITE_STASHAPP_URL=http://localhost:10001
|
||||
```
|
||||
|
||||
Alle Datenbanken und Konfigurationen liegen **lokal** — kein Internet oder Server erforderlich.
|
||||
|
||||
## Usage
|
||||
|
||||
### Browsing Media
|
||||
@@ -126,6 +224,7 @@ VITE_PLAYNITE_API_TOKEN="your-api-token"
|
||||
- `npm run dev` - Start development server
|
||||
- `npm run build` - Build for production
|
||||
- `npm run preview` - Preview production build
|
||||
- `npm run tauri` - Run Tauri desktop app
|
||||
- `npm run lint` - Run TypeScript type checking
|
||||
- `npm run clean` - Remove build artifacts
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@google/genai": "^1.29.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tauri-apps/api": "^2.11.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -28,6 +29,7 @@
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11.2",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/ui": "^4.1.4",
|
||||
@@ -2767,6 +2769,248 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
|
||||
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
|
||||
"integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.11.2",
|
||||
"@tauri-apps/cli-darwin-x64": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.11.2",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.11.2",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.11.2",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz",
|
||||
"integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz",
|
||||
"integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz",
|
||||
"integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz",
|
||||
"integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz",
|
||||
"integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz",
|
||||
"integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz",
|
||||
|
||||
@@ -13,13 +13,16 @@
|
||||
"test:ui": "vitest --ui",
|
||||
"test:run": "vitest run",
|
||||
"docs": "typedoc",
|
||||
"docs:serve": "typedoc && npx serve docs"
|
||||
"docs:serve": "typedoc && npx serve docs",
|
||||
"tauri": "tauri",
|
||||
"desktop:build": "bun scripts/build-desktop.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@google/genai": "^1.29.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tauri-apps/api": "^2.11.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -36,6 +39,7 @@
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11.2",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/ui": "^4.1.4",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { execSync } from "child_process";
|
||||
import { resolve } from "path";
|
||||
|
||||
const backendDir = resolve(import.meta.dirname, "../../backend");
|
||||
const frontendDir = resolve(import.meta.dirname, "..");
|
||||
|
||||
console.log("\n=== 1/2: Backend kompilieren ===\n");
|
||||
execSync("bun run compile:tauri", { cwd: backendDir, stdio: "inherit" });
|
||||
|
||||
console.log("\n=== 2/2: Tauri Desktop-App bauen ===\n");
|
||||
execSync("npx tauri build", { cwd: frontendDir, stdio: "inherit" });
|
||||
|
||||
console.log("\n✅ Fertig! Installer liegt in src-tauri/target/release/bundle/\n");
|
||||
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.6.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.11.2", features = [] }
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-spawn",
|
||||
"shell:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,111 @@
|
||||
use std::process::{Command, Stdio};
|
||||
use std::io::Read;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::Manager;
|
||||
|
||||
fn find_backend() -> Result<std::path::PathBuf, String> {
|
||||
let exe = std::env::current_exe().map_err(|e| format!("current_exe: {}", e))?;
|
||||
let exe_dir = exe.parent().ok_or("no parent dir")?;
|
||||
|
||||
let target_name = "omnyx-backend";
|
||||
let triple_name = format!("{}-x86_64-pc-windows-msvc.exe", target_name);
|
||||
let short_name = format!("{}.exe", target_name);
|
||||
|
||||
// Try paths relative to the executable
|
||||
let candidates = [
|
||||
// Dev: app.exe is in src-tauri/target/debug/, binary in src-tauri/binaries/
|
||||
exe_dir.parent().and_then(|p| p.parent()).and_then(|p| p.parent()).map(|p| p.join("binaries").join(&triple_name)),
|
||||
// Dev: via src-tauri/target/
|
||||
exe_dir.parent().and_then(|p| p.parent()).map(|p| p.join("binaries").join(&triple_name)),
|
||||
// Production: binary renamed by Tauri (no triple)
|
||||
Some(exe_dir.join(&short_name)),
|
||||
// Production: binary with triple in same dir
|
||||
Some(exe_dir.join(&triple_name)),
|
||||
// Production: binary in ./binaries/ relative to app.exe
|
||||
Some(exe_dir.join("binaries").join(&triple_name)),
|
||||
Some(exe_dir.join("binaries").join(&short_name)),
|
||||
];
|
||||
|
||||
for path in candidates.iter().flatten() {
|
||||
if path.exists() {
|
||||
return Ok(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Backend binary not found (searched from {:?})", exe))
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)
|
||||
.setup(|app| {
|
||||
let exe_dir = app.path().resource_dir().unwrap_or_else(|_| std::env::current_exe().map(|p| p.parent().unwrap().to_path_buf()).unwrap_or_default());
|
||||
let exe_path_str = exe_dir.to_string_lossy().to_lowercase();
|
||||
|
||||
// Portable mode: nur wenn auf Laufwerk C: UND in Program Files → APPDATA
|
||||
// Alles andere (D:, E:, USB, etc.) → portable (data neben der Exe)
|
||||
let on_c_drive = exe_path_str.starts_with("c:");
|
||||
let in_program_files = exe_path_str.contains("program files") || exe_path_str.contains("programme");
|
||||
let data_dir = if on_c_drive && in_program_files {
|
||||
std::env::var("APPDATA")
|
||||
.map(|a| format!("{}\\Omnyx", a))
|
||||
.unwrap_or_else(|_| format!("{}\\data", exe_dir.to_string_lossy()))
|
||||
} else {
|
||||
format!("{}\\data", exe_dir.to_string_lossy())
|
||||
};
|
||||
|
||||
match find_backend() {
|
||||
Ok(bin_path) => {
|
||||
log::info!("Starting backend: {:?}", bin_path);
|
||||
log::info!("Data dir: {}", data_dir);
|
||||
|
||||
match Command::new(&bin_path)
|
||||
.env("DATA_DIR", &data_dir)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => {
|
||||
log::info!("Backend started (PID: {})", child.id());
|
||||
|
||||
let mut stdout = child.stdout.unwrap();
|
||||
let mut stderr = child.stderr.unwrap();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut buf = [0u8; 4096];
|
||||
while let Ok(n) = stdout.read(&mut buf) {
|
||||
if n == 0 { break; }
|
||||
log::info!("[backend] {}", String::from_utf8_lossy(&buf[..n]).trim_end());
|
||||
}
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut buf = [0u8; 4096];
|
||||
while let Ok(n) = stderr.read(&mut buf) {
|
||||
if n == 0 { break; }
|
||||
log::warn!("[backend:err] {}", String::from_utf8_lossy(&buf[..n]).trim_end());
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to spawn {:?}: {}", bin_path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Omnyx",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.omnyx.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:3000",
|
||||
"beforeDevCommand": "npm run dev -- --mode tauri",
|
||||
"beforeBuildCommand": "npm run build -- --mode tauri"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Omnyx - Media Discovery",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
"minWidth": 900,
|
||||
"minHeight": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"center": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"binaries/omnyx-backend"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import CategoryBrowseRoute from './components/routes/CategoryBrowseRoute';
|
||||
import { MOCK_MEDIA, DETAIL_MEDIA } from './data';
|
||||
import { Media, Staff, MediaCategory, UserSettings } from './types';
|
||||
import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff, fetchSettings, updateSettings } from './api';
|
||||
import { waitForBackend } from './lib/api/waitForBackend';
|
||||
import { ThemeProvider, useTheme } from './contexts/ThemeContext';
|
||||
import { Search, Plus, LayoutGrid, List, Filter } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -570,6 +571,49 @@ function AppContent() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [backendReady, setBackendReady] = useState(false);
|
||||
const [backendFailed, setBackendFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
waitForBackend().then(ready => {
|
||||
if (ready) setBackendReady(true);
|
||||
else setBackendFailed(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (backendFailed) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-red-500/10 flex items-center justify-center">
|
||||
<span className="text-2xl">!</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-foreground">Backend nicht erreichbar</h2>
|
||||
<p className="text-muted-foreground max-w-md">
|
||||
Der Backend-Dienst auf Port 3001 antwortet nicht. Bitte starten Sie die App neu.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => { setBackendFailed(false); waitForBackend().then(r => r ? setBackendReady(true) : setBackendFailed(true)); }}
|
||||
className="px-4 py-2 bg-[#e8466c] text-white rounded-lg hover:bg-[#d13d60]"
|
||||
>
|
||||
Neu verbinden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!backendReady) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="w-12 h-12 border-4 border-[#e8466c]/30 border-t-[#e8466c] rounded-full animate-spin mx-auto" />
|
||||
<p className="text-muted-foreground">Backend wird gestartet...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
|
||||
@@ -137,11 +137,6 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
|
||||
|
||||
const filteredStaff = useMemo(() => {
|
||||
let list = staffList.filter(s => {
|
||||
// Hide actors without linked media
|
||||
if (!s.filmography || s.filmography.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by enabled categories based on media_types
|
||||
if (s.media_types && s.media_types.length > 0) {
|
||||
const hasEnabledMediaType = s.media_types.some(type => {
|
||||
|
||||
@@ -3,9 +3,16 @@ import { ApiMediaItem, ApiStaff, ApiCastItem, ApiSettingsItem, CreateSettingsInp
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
function normalizeUrl(url: string | null): string {
|
||||
const isLocalBackend = BASE_URL.includes('localhost') || BASE_URL.includes('127.0.0.1');
|
||||
|
||||
function normalizeUrl(url: string | null, type?: 'actor' | 'cover' | 'background' | 'icon'): string {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
if (isLocalBackend) {
|
||||
let proxyUrl = `${BASE_URL}/api/images/proxy?url=${encodeURIComponent(url)}`;
|
||||
if (type) proxyUrl += `&type=${type}`;
|
||||
return proxyUrl;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
const cleanPath = url.startsWith('/') ? url.slice(1) : url;
|
||||
@@ -18,7 +25,7 @@ export function convertApiCastToStaff(apiItem: ApiCastItem): Staff {
|
||||
name: apiItem.name,
|
||||
cleanname: apiItem.cleanname,
|
||||
role: apiItem.occupations?.[0] || 'Actor',
|
||||
photo: normalizeUrl(apiItem.photo) || `https://picsum.photos/seed/cast-${apiItem.id}/200/200`,
|
||||
photo: normalizeUrl(apiItem.photo, 'actor') || `https://picsum.photos/seed/cast-${apiItem.id}/200/200`,
|
||||
bio: apiItem.bio || undefined,
|
||||
birthDate: apiItem.birthDate || undefined,
|
||||
birthPlace: apiItem.birthPlace || undefined,
|
||||
@@ -38,7 +45,7 @@ export function convertApiCastToStaff(apiItem: ApiCastItem): Staff {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
year: item.year,
|
||||
poster: normalizeUrl(item.poster) || `https://picsum.photos/seed/${item.id}/400/600`,
|
||||
poster: normalizeUrl(item.poster, 'cover') || `https://picsum.photos/seed/${item.id}/400/600`,
|
||||
category: item.category,
|
||||
type: item.type,
|
||||
role: item.role,
|
||||
@@ -54,9 +61,9 @@ export function convertApiToMedia(apiItem: ApiMediaItem): Media {
|
||||
id: staffMember.id.toString(),
|
||||
name: staffMember.name,
|
||||
role: staffMember.role,
|
||||
photo: normalizeUrl(staffMember.photo) || `https://picsum.photos/seed/staff-${staffMember.id}/200/200`,
|
||||
photo: normalizeUrl(staffMember.photo, 'actor') || `https://picsum.photos/seed/staff-${staffMember.id}/200/200`,
|
||||
characterName: staffMember.characterName || staffMember.name,
|
||||
characterImage: normalizeUrl(staffMember.characterImage) || normalizeUrl(staffMember.photo) || `https://picsum.photos/seed/staff-${staffMember.id}/200/200`,
|
||||
characterImage: normalizeUrl(staffMember.characterImage, 'icon') || normalizeUrl(staffMember.photo, 'actor') || `https://picsum.photos/seed/staff-${staffMember.id}/200/200`,
|
||||
}));
|
||||
|
||||
let aspectRatio: '2/3' | '16/9' | '1/1' = '2/3';
|
||||
@@ -133,9 +140,9 @@ export function convertApiToMedia(apiItem: ApiMediaItem): Media {
|
||||
id: apiItem.id.toString(),
|
||||
title: apiItem.title,
|
||||
year: apiItem.year?.toString() || 'Unknown',
|
||||
poster: normalizeUrl(apiItem.poster) || `https://picsum.photos/seed/${apiItem.id}/400/600`,
|
||||
poster: normalizeUrl(apiItem.poster, 'cover') || `https://picsum.photos/seed/${apiItem.id}/400/600`,
|
||||
category: mediaCategory,
|
||||
banner: normalizeUrl(apiItem.banner) || undefined,
|
||||
banner: normalizeUrl(apiItem.banner, 'background') || undefined,
|
||||
description: apiItem.description || undefined,
|
||||
rating: apiItem.rating || undefined,
|
||||
genres: apiItem.genres || [],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
let ready: Promise<boolean> | null = null;
|
||||
|
||||
export function waitForBackend(maxRetries = 30, interval = 1000): Promise<boolean> {
|
||||
if (ready) return ready;
|
||||
ready = (async () => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/health`);
|
||||
if (response.ok) return true;
|
||||
} catch {}
|
||||
await new Promise(r => setTimeout(r, interval));
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
return ready;
|
||||
}
|
||||