Compare commits
No commits in common. "74c2dbfa88e05b2e97b06a20cf22ceee3214a0bb" and "d0b2c182de2c7d107e4a623cb2d3bf9a004939b0" have entirely different histories.
74c2dbfa88
...
d0b2c182de
8 changed files with 18 additions and 602 deletions
|
|
@ -20,7 +20,6 @@
|
||||||
| **KI-Video Pipeline** | ki-video/PLAN.md | Lokale Videoproduktion: Qwen + FLUX + XTTS + SadTalker (ki-tower) |
|
| **KI-Video Pipeline** | ki-video/PLAN.md | Lokale Videoproduktion: Qwen + FLUX + XTTS + SadTalker (ki-tower) |
|
||||||
| **Webcam Kambodscha** | — | Dahua 4K → cam.arakavanews.com (CT 600 auf pve-ka-3) |
|
| **Webcam Kambodscha** | — | Dahua 4K → cam.arakavanews.com (CT 600 auf pve-ka-3) |
|
||||||
| **YouTube Desktop PP** | — | Remote Desktop (xrdp) für YT-Uploads (pve-pp-1/pp-2) |
|
| **YouTube Desktop PP** | — | Remote Desktop (xrdp) für YT-Uploads (pve-pp-1/pp-2) |
|
||||||
| **Lokale KI / Vibe Coding** | hardware-ai-local-coding.md | Hardware-Entscheidung MacBook Pro M5 Max 128 GB, NVIDIA-Vergleich, Verkauf Alt-GPUs |
|
|
||||||
|
|
||||||
## Prioritäten
|
## Prioritäten
|
||||||
|
|
||||||
|
|
@ -46,9 +45,8 @@
|
||||||
| Muldenstein | CT 116 (pve-mu-2) | **monitoring-bot** | 100.88.230.74 | Cursor IDE + Tailscale-Gateway + SSH-Hub |
|
| Muldenstein | CT 116 (pve-mu-2) | **monitoring-bot** | 100.88.230.74 | Cursor IDE + Tailscale-Gateway + SSH-Hub |
|
||||||
| Muldenstein | ki-tower | **ki-tower** (in Bau) | — | **Jellyfin-Ersatz** (Medienserver) — KEINE KI-Rolle |
|
| Muldenstein | ki-tower | **ki-tower** (in Bau) | — | **Jellyfin-Ersatz** (Medienserver) — KEINE KI-Rolle |
|
||||||
| Muldenstein | MSI B550 | **test-rig** (in Bau) | — | Test-Bett für alte 3080er/AMD-Karten vor Verkauf |
|
| Muldenstein | MSI B550 | **test-rig** (in Bau) | — | Test-Bett für alte 3080er/AMD-Karten vor Verkauf |
|
||||||
| Muldenstein | AMD-Rig | **gpu-reserve** | — | 8x RX 6600 XT — Verkauf |
|
| Muldenstein | AMD-Rig | **gpu-reserve** | — | 8x RX 6600 XT — Tendenz Verkauf |
|
||||||
| mobil | MacBook Pro 16" | **m5-max** | — | M5 Max 128 GB / 2 TB — lokale KI / Vibe Coding / Jervais (Stand 2026-04-26, siehe hardware-ai-local-coding.md) |
|
| — | — | — | — | **Nur EINE KI-Maschine geplant**: bestehender KI-Server (Windows, RTX 3090). Kein zweiter KI-Rechner vorgesehen. |
|
||||||
| — | — | — | — | **Lokale KI: M5 Max** (mobil) + **CUDA-Pflicht: KI-Server RTX 3090**. Kein 2× 5090, kein zweiter NVIDIA-Build. |
|
|
||||||
|
|
||||||
## Telegram Bots
|
## Telegram Bots
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
# Hardware für lokales Vibe-Coding & lokale KI
|
|
||||||
|
|
||||||
**Stand: 2026-04-26**
|
|
||||||
**Status: Plan beschlossen, Umsetzung läuft (Verkauf Alt-GPUs → MacBook Pro M5 Max 128 GB)**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1 — Entscheidung in einem Satz
|
|
||||||
|
|
||||||
Eine einzige Maschine für lokales Vibe-Coding mit großen Modellen
|
|
||||||
(Qwen 3.6 Coder, GLM, Devstral, Kimi-Coder etc.):
|
|
||||||
**MacBook Pro 16" M5 Max, 128 GB Unified Memory, 2 TB SSD**, gekauft
|
|
||||||
in Hongkong (deutlich günstiger als DE), eingerichtet vor Ort,
|
|
||||||
ohne OVP nach Kambodscha mitgenommen.
|
|
||||||
|
|
||||||
Begründung kurz: Großer **zusammenhängender** Speicher,
|
|
||||||
Apple-Silicon-Effizienz, hohe Bandbreite, MLX-Ökosystem,
|
|
||||||
fertiges leises Gerät. Kein Bastel-Build, keine Kühl-/Strom-Gymnastik,
|
|
||||||
keine VRAM-Fragmentierung.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2 — Warum NICHT NVIDIA-Multi-GPU
|
|
||||||
|
|
||||||
### Harter Fakt zuerst
|
|
||||||
|
|
||||||
Bei aktuellen NVIDIA-Consumer-Karten (RTX 4090, **RTX 5090**) gibt es
|
|
||||||
**kein NVLink mehr**. Das heißt:
|
|
||||||
|
|
||||||
- 2× RTX 5090 = **2× 32 GB getrennter VRAM**, **NICHT** 64 GB Unified.
|
|
||||||
- Modelle, die nicht in 32 GB passen, müssen partitioniert werden
|
|
||||||
(Tensor-Parallel, Pipeline-Parallel) — mit hohen Latenzen über PCIe,
|
|
||||||
Treiber-Friktion und massivem Verlust bei Token-Generation und
|
|
||||||
KV-Cache-Verwaltung.
|
|
||||||
- Für **lokale Single-Model-Inferenz mit großem Kontext** ist das
|
|
||||||
ein **Ausschlusskriterium**, kein "weicher Nachteil".
|
|
||||||
|
|
||||||
### Vollkostenrechnung 2× RTX 5090 (DE-Preise, realistisch 2026)
|
|
||||||
|
|
||||||
| Posten | Preis (€) |
|
|
||||||
|--------------------------------|--------------------|
|
|
||||||
| 2× RTX 5090 | 4.000 – 4.800 |
|
|
||||||
| Mainboard mit 2× PCIe 5.0 x16 | 500 – 800 |
|
|
||||||
| CPU + RAM (>128 GB DDR5 ECC) | 900 – 1.400 |
|
|
||||||
| 2 TB NVMe SSD | 150 – 250 |
|
|
||||||
| 1500 W Platinum Netzteil | 300 – 450 |
|
|
||||||
| Großes Gehäuse + Lüfter | 200 – 400 |
|
|
||||||
| Wasserkühlung (für 2× 5090) | 400 – 800 |
|
|
||||||
| Windows / Linux Setup-Zeit | pauschal viel |
|
|
||||||
| **Summe Hardware** | **6.450 – 8.900** |
|
|
||||||
|
|
||||||
Plus laufende Kosten:
|
|
||||||
|
|
||||||
- Strom unter Last realistisch **600–900 W** sustained.
|
|
||||||
- Hitze, Lautstärke, Standortzwang (nicht im Schlafzimmer, nicht im
|
|
||||||
Hotelzimmer, nicht in Phnom Penh ohne Klimaanlagen-Backup).
|
|
||||||
- Keine Mobilität.
|
|
||||||
|
|
||||||
### Vergleich M5 Max Komplettpaket
|
|
||||||
|
|
||||||
| Posten | Preis (€) |
|
|
||||||
|-------------------------------------|-------------------|
|
|
||||||
| MacBook Pro 16" M5 Max 128 GB / 2 TB (HK Listenpreis, EUR-äquivalent) | ~4.790 |
|
|
||||||
| Externer Ultrawide Monitor (später) | 400 – 900 |
|
|
||||||
| HDMI/Thunderbolt Adapter | 30 – 100 |
|
|
||||||
| **Summe** | **5.220 – 5.790** |
|
|
||||||
|
|
||||||
- Stromaufnahme unter Volllast **~120–150 W**. Eine Größenordnung
|
|
||||||
unter NVIDIA-Doppel-GPU.
|
|
||||||
- Leise, fertig, mobil (Handgepäck-Koffer mit Polsterung).
|
|
||||||
- 128 GB **als ein Pool** — passt für Modelle, die auf 2× 5090 nicht
|
|
||||||
laufen können.
|
|
||||||
|
|
||||||
### Konsequenz
|
|
||||||
|
|
||||||
Für den konkreten Use-Case (lokales Coding-Modell, ein Dev-Workplatz,
|
|
||||||
auch unterwegs nutzbar) ist die NVIDIA-Multi-GPU-Variante
|
|
||||||
**teurer** *und* **schlechter geeignet**. Der „mehr-Rohleistung"-Reflex
|
|
||||||
ist ohne NVLink für große Einzelmodelle irrelevant.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3 — Roadmap
|
|
||||||
|
|
||||||
### Phase 1 — *jetzt*
|
|
||||||
|
|
||||||
1. **Verkauf der Alt-GPUs** (alle mit OVP):
|
|
||||||
- 4× RTX 3080 (Mining-Historie) → Test-Rig MSI B550 zum Durchtesten,
|
|
||||||
dann Verkauf eBay-Kleinanzeigen.
|
|
||||||
- 1× RTX 3090 (kein Mining) → aktuell 700–1.100 € erzielbar.
|
|
||||||
- 8× AMD RX 6600 XT (8 GB) → Verkauf, ROCm auf Navi 23 für LLM
|
|
||||||
irrelevant.
|
|
||||||
- **Außerdem:** MacBook Pro 14" M1 Pro 16 GB mit OVP, kratzerlos,
|
|
||||||
wird kaum genutzt → Verkauf hilft Finanzierung.
|
|
||||||
2. Aus Verkaufserlös + Cashflow PP: **MacBook Pro 16" M5 Max 128 GB**
|
|
||||||
in HK kaufen. US-Tastatur ist OK (extern Tastatur/Maus daheim).
|
|
||||||
3. Einreise nach Kambodscha **ohne OVP**, im gepolsterten Handgepäck.
|
|
||||||
Customs-relevant: persönliches Elektronikgerät, getragen, nicht
|
|
||||||
neu/originalverpackt.
|
|
||||||
|
|
||||||
### Phase 2 — *wenn M5 Ultra erscheint und ein Modell ihn ausreizt*
|
|
||||||
|
|
||||||
- Wahrscheinliche Specs M5 Ultra: ~256 GB Unified Memory,
|
|
||||||
~1.000–1.200 GB/s Bandbreite. Releasezeitpunkt offen, kein Termin.
|
|
||||||
- Ohne passendes Modell (>128 GB sinnvoll) **kein Kauf**.
|
|
||||||
- Plan: M5 Max später ggf. in Zahlung geben oder per Thunderbolt
|
|
||||||
als Co-Worker behalten.
|
|
||||||
- Realistisch: Heute existiert **kein Coder-Modell**, das 256 GB
|
|
||||||
Unified Memory voll auslastet. Erst wenn Qwen / DeepSeek / GLM
|
|
||||||
ein Coder-Modell jenseits aktueller MoE-Größen liefern, das
|
|
||||||
Cursor 4.7-Niveau lokal erreicht, lohnt der Sprung.
|
|
||||||
|
|
||||||
### Phase 3 — *Jervais lokal*
|
|
||||||
|
|
||||||
- Sobald M5 Max steht: Jervais (Assistent + Simultanübersetzung,
|
|
||||||
STT/TTS) lokal auf MLX. Kein Cloud-Token-Verbrauch mehr für
|
|
||||||
Routine-Coding und Übersetzung.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4 — Was M5 Max **nicht** kann
|
|
||||||
|
|
||||||
Damit die Doku ehrlich bleibt:
|
|
||||||
|
|
||||||
- **CUDA-Only-Workloads** (manche Vision-/Diffusion-Pipelines, einige
|
|
||||||
Trainings-Scripts) laufen nicht oder schlecht. Inferenz ist meist
|
|
||||||
per MLX/llama.cpp gelöst, Training größerer Modelle bleibt
|
|
||||||
NVIDIA-Domäne.
|
|
||||||
- **Kein Hot-Swap** von Modellen mit kalten Karten — alles teilt sich
|
|
||||||
den Unified Memory.
|
|
||||||
- **Mobil ist nicht passiv-still** unter Volllast: Lüfter dreht
|
|
||||||
hörbar hoch, dauert aber nicht lange. 16"-Chassis kühlt sustained
|
|
||||||
Loads spürbar besser als 14".
|
|
||||||
- **Geschlossener Deckel**: Reduziert Performance/Cooling, nicht
|
|
||||||
Standard-Modus für lokale LLM-Sessions. Jervais-Zentrale "im Koffer
|
|
||||||
mit zugeklapptem Deckel" funktioniert nur eingeschränkt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5 — Lehre aus der Beratung am 2026-04-26 (selbstkritisch)
|
|
||||||
|
|
||||||
Während des Beratungsgesprächs zur Hardware-Entscheidung hat der
|
|
||||||
Assistent die Frage „M5 Max vs. NVIDIA-Multi-GPU" zunächst mit einem
|
|
||||||
generischen „mehr Rohleistung bei NVIDIA"-Argument relativiert,
|
|
||||||
**obwohl bekannt war**, dass:
|
|
||||||
|
|
||||||
- RTX 4090/5090 **kein NVLink** haben.
|
|
||||||
- Daraus folgt **kein gemeinsamer VRAM-Pool**.
|
|
||||||
- Damit ist 2× 5090 **kein Ersatz** für 128 GB Unified Memory bei
|
|
||||||
großen Einzelmodellen — sondern **architektonisch ungeeignet**.
|
|
||||||
|
|
||||||
Der User hat das zurecht als **Lüge durch falsche Gewichtung**
|
|
||||||
gewertet, nicht als bloßen Irrtum. Bekannte harte Fakten dürfen in
|
|
||||||
einer Kaufberatung nicht weichgespült werden, um „ausgewogen" zu
|
|
||||||
wirken.
|
|
||||||
|
|
||||||
**Konsequenz für künftige Beratung (Pflicht):**
|
|
||||||
|
|
||||||
1. Wenn ein Architektur-Fakt einen Vergleich zum Kippen bringt
|
|
||||||
(hier: NVLink/Unified Memory), wird er **zuerst** genannt und
|
|
||||||
**nicht** unter „Nuancen" eingebettet.
|
|
||||||
2. Vollständige Systemkosten (Mainboard, Netzteil, Kühlung, Strom,
|
|
||||||
Mobilität, Friktion) werden bei GPU-Vergleichen **immer**
|
|
||||||
beziffert, nicht nur die Karte.
|
|
||||||
3. Wenn der User eine binäre Entscheidung anfragt
|
|
||||||
(„links oder rechts"), wird klar gewichtet, nicht relativiert.
|
|
||||||
4. Bei Abweichung zwischen bekanntem Fakt und ausgegebenem Satz:
|
|
||||||
sofortige Korrektur ohne Ausreden.
|
|
||||||
|
|
||||||
Diese Regel ist auch als OpenMemory-Eintrag gespeichert
|
|
||||||
(Hardware-Beratung / Vibe-Coding / lokale KI).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6 — Status der bisherigen „HARDWARE-ZUKUNFT" (überholt)
|
|
||||||
|
|
||||||
Die alte Leitlinie in `homelab.conf`
|
|
||||||
("Es gibt genau EINE KI-Maschine — den bestehenden KI-Server,
|
|
||||||
Windows, RTX 3090") ist **mit dieser Entscheidung überholt**.
|
|
||||||
|
|
||||||
Neue Leitlinie:
|
|
||||||
|
|
||||||
- **Lokale KI für Coding & Jervais** läuft auf dem **MacBook Pro
|
|
||||||
M5 Max 128 GB** (mobil, Phnom Penh + Muldenstein).
|
|
||||||
- **Bestehender KI-Server (Windows + RTX 3090)** bleibt für Workloads,
|
|
||||||
die zwingend CUDA brauchen (SDXL, XTTS, einzelne Pipelines).
|
|
||||||
Keine Erweiterung mehr geplant.
|
|
||||||
- **Alte GPUs** (4× 3080, 8× 6600 XT) → **Verkauf**.
|
|
||||||
- **Kein zweiter NVIDIA-Build, kein 2× 5090, kein Multi-GPU-Rig.**
|
|
||||||
- **M5 Ultra** nur dann, wenn ein konkretes Modell das rechtfertigt.
|
|
||||||
|
|
||||||
`homelab.conf` und `MASTER_INDEX.md` werden parallel angepasst.
|
|
||||||
|
|
@ -30,19 +30,6 @@ Automatische KI-Anreicherung von Save.TV-Filmarchiv mit Beschreibungen, Darstell
|
||||||
- **Downloads:** `/files/<filename>` zum Browser-Download
|
- **Downloads:** `/files/<filename>` zum Browser-Download
|
||||||
- **Wikidata:** Fallback für Jahr-Lookup wenn nicht im KI-Cache
|
- **Wikidata:** Fallback für Jahr-Lookup wenn nicht im KI-Cache
|
||||||
|
|
||||||
### 4. Auto-Qualitätsfilter
|
|
||||||
- **Datei:** `tools/savetv_country_filter.py`
|
|
||||||
- **Zielprofil:** Jellyfin-ähnliche Kinofilme, vor allem Action, Thriller, Krimi, Abenteuer, Sci-Fi, Western, Horror und starke Dramen.
|
|
||||||
- **Blockiert automatisch:** Deutschland/Frankreich-Produktionen, Doku-/Musik-/Familien-/TV-Film-Genres, typische öffentlich-rechtliche TV-Film-Titel und unerkannte ÖR-/Regionaltitel ohne Cache.
|
|
||||||
- **Erlaubt automatisch:** internationale Kinofilme aus USA/UK/Kanada/Australien/Japan/Südkorea/Hongkong mit passendem Genre oder klar erkennbarem Kino-Titel.
|
|
||||||
- **Konsequenz:** Unbekannte RBB/MDR/hr/WDR/BR/SWR/ZDF/ARD/arte/ONE-Titel werden nicht mehr automatisch aufgenommen oder heruntergeladen, bis sie angereichert sind oder eindeutig als Kinofilm erkannt werden.
|
|
||||||
|
|
||||||
### 5. Wunschfilm-Liste
|
|
||||||
- **Datei:** `tools/savetv.py`, Konstante `WISHLIST_MOVIES`
|
|
||||||
- **Ziel:** Hochwertige Klassiker und Reihen automatisch aufnehmen, auch wenn Save.TV-Score oder Qualitätsfilter sie sonst nur vorschlagen würden.
|
|
||||||
- **Aktive Startliste:** `Der Clou`, `Déjà Vu`, `Die üblichen Verdächtigen`, `L.A. Confidential`, `Chinatown`, `French Connection`, `Bullitt`, `Serpico`, `Der Pate`, `GoodFellas`, `Casino`, `Scarface`, `Departed`, Western-Klassiker, Sci-Fi-Klassiker, starke Kriegs-/Politthriller, Gerichts-/Drama-Klassiker sowie Reihen wie `Jesse Stone`, `Columbo`, `Wallander`, `Jack Reacher`, `Bourne`, `Mission Impossible`, `Indiana Jones` und `James Bond`.
|
|
||||||
- **Schutz gegen Fehlgriffe:** Mehrdeutige Ein-Wort-Titel laufen nur mit Kontext. `Sting` wird nur mit Paul-Newman/Robert-Redford/1930er-Hinweisen als `Der Clou` akzeptiert; `Heat` braucht De-Niro/Pacino/Mann-Kontext; `Crash` braucht Haggis/Don-Cheadle/Los-Angeles-Kontext.
|
|
||||||
|
|
||||||
## Erfahrungen / Gotchas
|
## Erfahrungen / Gotchas
|
||||||
|
|
||||||
- `qwen2.5:14b` ist zuverlässiger für JSON-Output als `qwen3:30b-a3b`
|
- `qwen2.5:14b` ist zuverlässiger für JSON-Output als `qwen3:30b-a3b`
|
||||||
|
|
|
||||||
|
|
@ -88,13 +88,7 @@ def run():
|
||||||
if savetv._is_excluded(title):
|
if savetv._is_excluded(title):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
allow_download, quality_reason = savetv_country_filter.classify_auto_quality(
|
if savetv_country_filter.should_exclude_production_country(title, filminfo_cache):
|
||||||
title,
|
|
||||||
tc.get("STVSTATIONNAME", ""),
|
|
||||||
filminfo_cache,
|
|
||||||
)
|
|
||||||
if not allow_download:
|
|
||||||
log.info("Überspringe Download: %s (%s)", title, quality_reason)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if tid in dl_log:
|
if tid in dl_log:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
SaveTV → NAS Sync
|
SaveTV → NAS Sync
|
||||||
Wartet 12h ± 0-30min nach Download auf Hetzner bevor Datei auf NAS kommt.
|
Wartet 12h ± 0-30min nach Download auf Hetzner bevor Datei auf NAS kommt.
|
||||||
"""
|
"""
|
||||||
import os, json, time, random, fcntl, urllib.request, urllib.parse, urllib.error, email.utils
|
import os, json, time, random, urllib.request, urllib.parse, urllib.error, email.utils
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
ZIEL = "/mnt/nas/Filme zum nachbearbeiten"
|
ZIEL = "/mnt/nas/Filme zum nachbearbeiten"
|
||||||
|
|
@ -11,7 +11,6 @@ BASE = "http://138.201.84.95:9443/files"
|
||||||
API = "http://138.201.84.95:9443/api/films"
|
API = "http://138.201.84.95:9443/api/films"
|
||||||
CALLBACK = "http://138.201.84.95:9443/api/nas_synced"
|
CALLBACK = "http://138.201.84.95:9443/api/nas_synced"
|
||||||
LOG = "/var/log/savetv_sync.log"
|
LOG = "/var/log/savetv_sync.log"
|
||||||
LOCKFILE = "/tmp/savetv_sync.lock"
|
|
||||||
|
|
||||||
MIN_ALTER_H = 24
|
MIN_ALTER_H = 24
|
||||||
JITTER_MIN = 30 # ± bis zu 30 Minuten Zufall
|
JITTER_MIN = 30 # ± bis zu 30 Minuten Zufall
|
||||||
|
|
@ -116,10 +115,4 @@ def sync():
|
||||||
log(f"Fertig. {kopiert} neue Filme kopiert.")
|
log(f"Fertig. {kopiert} neue Filme kopiert.")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
with open(LOCKFILE, "w") as lock:
|
|
||||||
try:
|
|
||||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
||||||
except BlockingIOError:
|
|
||||||
log("Sync läuft bereits, überspringe diesen Start.")
|
|
||||||
raise SystemExit(0)
|
|
||||||
sync()
|
sync()
|
||||||
|
|
|
||||||
|
|
@ -43,119 +43,6 @@ DOWNLOAD_FORMAT_SD = 5
|
||||||
AUTO_RECORD_SCORE = 80
|
AUTO_RECORD_SCORE = 80
|
||||||
SUGGEST_SCORE = 60
|
SUGGEST_SCORE = 60
|
||||||
|
|
||||||
|
|
||||||
def _title_pattern(title):
|
|
||||||
"""Titel exakt genug matchen, aber Leerzeichen/Interpunktion tolerant halten."""
|
|
||||||
parts = [re.escape(p) for p in re.findall(r"\w+", title.strip().lower(), re.UNICODE)]
|
|
||||||
return r"(?<!\w)" + r"[\W_]+".join(parts) + r"(?!\w)"
|
|
||||||
|
|
||||||
|
|
||||||
def _wishlist_title(name, *aliases):
|
|
||||||
return {
|
|
||||||
"name": name,
|
|
||||||
"title_patterns": [_title_pattern(a) for a in aliases],
|
|
||||||
"ambiguous_title_patterns": [],
|
|
||||||
"required_context_patterns": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _wishlist_ambiguous(name, aliases, context):
|
|
||||||
return {
|
|
||||||
"name": name,
|
|
||||||
"title_patterns": [],
|
|
||||||
"ambiguous_title_patterns": [_title_pattern(a) for a in aliases],
|
|
||||||
"required_context_patterns": context,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
WISHLIST_MOVIES = [
|
|
||||||
{
|
|
||||||
"name": "Der Clou",
|
|
||||||
"title_patterns": [
|
|
||||||
r"\bder\s+clou\b",
|
|
||||||
r"\bthe\s+sting\b",
|
|
||||||
],
|
|
||||||
# "Sting" alleine ist mehrdeutig; nur mit inhaltlichem Treffer aufnehmen.
|
|
||||||
"ambiguous_title_patterns": [
|
|
||||||
r"\bsting\b",
|
|
||||||
],
|
|
||||||
"required_context_patterns": [
|
|
||||||
r"\bpaul\s+newman\b",
|
|
||||||
r"\brobert\s+redford\b",
|
|
||||||
r"\bjohnny\s+hooker\b",
|
|
||||||
r"\bgondorff\b",
|
|
||||||
r"\blonnegan\b",
|
|
||||||
r"\b1930\b",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
_wishlist_title("Déjà Vu", "Déjà Vu - Wettlauf gegen die Zeit", "Deja Vu - Wettlauf gegen die Zeit"),
|
|
||||||
_wishlist_title("Die üblichen Verdächtigen", "Die üblichen Verdächtigen", "The Usual Suspects"),
|
|
||||||
_wishlist_title("L.A. Confidential", "L.A. Confidential"),
|
|
||||||
_wishlist_title("Chinatown", "Chinatown"),
|
|
||||||
_wishlist_title("French Connection", "French Connection", "The French Connection"),
|
|
||||||
_wishlist_title("Bullitt", "Bullitt"),
|
|
||||||
_wishlist_title("Serpico", "Serpico"),
|
|
||||||
_wishlist_title("Dog Day Afternoon", "Dog Day Afternoon", "Hundstage"),
|
|
||||||
_wishlist_title("Bonnie und Clyde", "Bonnie und Clyde", "Bonnie and Clyde"),
|
|
||||||
_wishlist_title("Der Pate", "Der Pate", "The Godfather"),
|
|
||||||
_wishlist_title("GoodFellas", "GoodFellas", "GoodFellas - Drei Jahrzehnte in der Mafia"),
|
|
||||||
_wishlist_title("Casino", "Casino"),
|
|
||||||
_wishlist_title("Scarface", "Scarface"),
|
|
||||||
_wishlist_title("Carlito's Way", "Carlito's Way", "Carlitos Way"),
|
|
||||||
_wishlist_title("Es war einmal in Amerika", "Es war einmal in Amerika", "Once Upon a Time in America"),
|
|
||||||
_wishlist_title("Departed", "Departed", "Departed - Unter Feinden"),
|
|
||||||
_wishlist_title("Spiel mir das Lied vom Tod", "Spiel mir das Lied vom Tod", "Once Upon a Time in the West"),
|
|
||||||
_wishlist_title("Zwei glorreiche Halunken", "Zwei glorreiche Halunken", "The Good, the Bad and the Ugly"),
|
|
||||||
_wishlist_title("Der mit dem Wolf tanzt", "Der mit dem Wolf tanzt", "Dances with Wolves"),
|
|
||||||
_wishlist_title("Open Range", "Open Range"),
|
|
||||||
_wishlist_title("Erbarmungslos", "Erbarmungslos", "Unforgiven"),
|
|
||||||
_wishlist_title("True Grit", "True Grit"),
|
|
||||||
_wishlist_title("Butch Cassidy and the Sundance Kid", "Butch Cassidy and the Sundance Kid", "Zwei Banditen"),
|
|
||||||
_wishlist_title("Blade Runner", "Blade Runner"),
|
|
||||||
_wishlist_title("Alien", "Alien - Das unheimliche Wesen aus einer fremden Welt"),
|
|
||||||
_wishlist_title("Aliens", "Aliens - Die Rückkehr", "Aliens - Die Rueckkehr"),
|
|
||||||
_wishlist_title("Terminator", "Terminator"),
|
|
||||||
_wishlist_title("Terminator 2", "Terminator 2", "Terminator 2 - Tag der Abrechnung"),
|
|
||||||
_wishlist_title("Matrix", "Matrix", "The Matrix"),
|
|
||||||
_wishlist_title("12 Monkeys", "12 Monkeys", "Twelve Monkeys"),
|
|
||||||
_wishlist_title("Children of Men", "Children of Men"),
|
|
||||||
_wishlist_title("Gattaca", "Gattaca"),
|
|
||||||
_wishlist_title("Minority Report", "Minority Report"),
|
|
||||||
_wishlist_title("Apocalypse Now", "Apocalypse Now"),
|
|
||||||
_wishlist_title("Full Metal Jacket", "Full Metal Jacket"),
|
|
||||||
_wishlist_title("Platoon", "Platoon"),
|
|
||||||
_wishlist_title("Der Soldat James Ryan", "Der Soldat James Ryan", "Saving Private Ryan"),
|
|
||||||
_wishlist_title("Black Hawk Down", "Black Hawk Down"),
|
|
||||||
_wishlist_title("Die Unbestechlichen", "Die Unbestechlichen", "All the President's Men"),
|
|
||||||
_wishlist_title("JFK", "JFK"),
|
|
||||||
_wishlist_title("Argo", "Argo"),
|
|
||||||
_wishlist_title("Zeugin der Anklage", "Zeugin der Anklage", "Witness for the Prosecution"),
|
|
||||||
_wishlist_title("Die zwölf Geschworenen", "Die zwölf Geschworenen", "12 Angry Men"),
|
|
||||||
_wishlist_title("Einer flog über das Kuckucksnest", "Einer flog über das Kuckucksnest", "One Flew Over the Cuckoo's Nest"),
|
|
||||||
_wishlist_title("Die Verurteilten", "Die Verurteilten", "The Shawshank Redemption"),
|
|
||||||
_wishlist_title("Forrest Gump", "Forrest Gump"),
|
|
||||||
_wishlist_title("Rain Man", "Rain Man"),
|
|
||||||
_wishlist_title("Million Dollar Baby", "Million Dollar Baby"),
|
|
||||||
_wishlist_title("Jesse Stone", "Jesse Stone"),
|
|
||||||
_wishlist_title("Columbo", "Columbo"),
|
|
||||||
_wishlist_title("Wallander", "Wallander"),
|
|
||||||
_wishlist_title("Jack Reacher", "Jack Reacher", "Reacher"),
|
|
||||||
_wishlist_title("Bourne", "Bourne"),
|
|
||||||
_wishlist_title("Mission Impossible", "Mission Impossible", "Mission: Impossible"),
|
|
||||||
_wishlist_title("Indiana Jones", "Indiana Jones"),
|
|
||||||
_wishlist_title("James Bond", "James Bond 007", "007"),
|
|
||||||
_wishlist_ambiguous(
|
|
||||||
"Heat",
|
|
||||||
["Heat"],
|
|
||||||
[r"\bdeniro\b", r"\bde\s+niro\b", r"\bal\s+pacino\b", r"\bmann\b", r"\bwaingro\b"],
|
|
||||||
),
|
|
||||||
_wishlist_ambiguous(
|
|
||||||
"Crash",
|
|
||||||
["Crash"],
|
|
||||||
[r"\bpaul\s+haggis\b", r"\bdon\s+cheadle\b", r"\bmatt\s+dillon\b", r"\blos\s+angeles\b"],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
SPAM_SUBCATEGORIES = {
|
SPAM_SUBCATEGORIES = {
|
||||||
"teleshop", "shopping", "dauerwerbesendung", "volksmusik",
|
"teleshop", "shopping", "dauerwerbesendung", "volksmusik",
|
||||||
"casting", "reality", "quiz/spiel", "comic", "zeichentrick",
|
"casting", "reality", "quiz/spiel", "comic", "zeichentrick",
|
||||||
|
|
@ -466,11 +353,6 @@ def _filter_films(telecasts, only_new=False):
|
||||||
if is_highlight:
|
if is_highlight:
|
||||||
score += 10
|
score += 10
|
||||||
|
|
||||||
wishlist_match = _wishlist_match(tc)
|
|
||||||
if wishlist_match:
|
|
||||||
score = max(score, 100)
|
|
||||||
tc["_wishlist_match"] = wishlist_match
|
|
||||||
|
|
||||||
tc["_score"] = score
|
tc["_score"] = score
|
||||||
tc["_start_dt"] = start_dt
|
tc["_start_dt"] = start_dt
|
||||||
films.append(tc)
|
films.append(tc)
|
||||||
|
|
@ -490,23 +372,6 @@ def _mark_seen(films):
|
||||||
_save_seen(seen)
|
_save_seen(seen)
|
||||||
|
|
||||||
|
|
||||||
def _wishlist_match(tc):
|
|
||||||
"""Gibt den Wunschtitel zurueck, wenn eine Sendung sicher passt."""
|
|
||||||
haystack = " ".join(
|
|
||||||
str(tc.get(k) or "")
|
|
||||||
for k in ("STITLE", "STHEMA", "SFULLSUBTITLE", "SDESCRIPTION")
|
|
||||||
).lower()
|
|
||||||
title = str(tc.get("STITLE") or "").lower()
|
|
||||||
for movie in WISHLIST_MOVIES:
|
|
||||||
for pattern in movie["title_patterns"]:
|
|
||||||
if re.search(pattern, title, re.IGNORECASE):
|
|
||||||
return movie["name"]
|
|
||||||
if any(re.search(p, title, re.IGNORECASE) for p in movie["ambiguous_title_patterns"]):
|
|
||||||
if any(re.search(p, haystack, re.IGNORECASE) for p in movie["required_context_patterns"]):
|
|
||||||
return movie["name"]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _record_telecast(telecast_id):
|
def _record_telecast(telecast_id):
|
||||||
"""Aufnahme anlegen."""
|
"""Aufnahme anlegen."""
|
||||||
s = _get_session()
|
s = _get_session()
|
||||||
|
|
@ -862,24 +727,11 @@ def get_new_films():
|
||||||
|
|
||||||
films = _filter_films(telecasts, only_new=True)
|
films = _filter_films(telecasts, only_new=True)
|
||||||
_mark_seen(films)
|
_mark_seen(films)
|
||||||
from tools import savetv_country_filter
|
|
||||||
filminfo_cache = savetv_country_filter.load_filminfo_cache()
|
|
||||||
|
|
||||||
auto_recorded = []
|
auto_recorded = []
|
||||||
suggestions = []
|
suggestions = []
|
||||||
|
|
||||||
for f in films:
|
for f in films:
|
||||||
wishlist_match = f.get("_wishlist_match")
|
|
||||||
allow_auto, quality_reason = savetv_country_filter.classify_auto_quality(
|
|
||||||
f.get("STITLE", ""),
|
|
||||||
f.get("STVSTATIONNAME", ""),
|
|
||||||
filminfo_cache,
|
|
||||||
)
|
|
||||||
if not allow_auto and not wishlist_match:
|
|
||||||
log.info("Auto-Aufnahme übersprungen: %s (%s)",
|
|
||||||
f.get("STITLE"), quality_reason)
|
|
||||||
continue
|
|
||||||
|
|
||||||
score = f["_score"]
|
score = f["_score"]
|
||||||
if score < SUGGEST_SCORE:
|
if score < SUGGEST_SCORE:
|
||||||
continue
|
continue
|
||||||
|
|
@ -892,10 +744,6 @@ def get_new_films():
|
||||||
result = _record_telecast(tid)
|
result = _record_telecast(tid)
|
||||||
f["_record_result"] = result
|
f["_record_result"] = result
|
||||||
auto_recorded.append(f)
|
auto_recorded.append(f)
|
||||||
if wishlist_match:
|
|
||||||
log.info("Wunschfilm-Aufnahme (%s): %s -> %s",
|
|
||||||
wishlist_match, f.get("STITLE"), result)
|
|
||||||
else:
|
|
||||||
log.info("Auto-Aufnahme: %s (Score %d) -> %s",
|
log.info("Auto-Aufnahme: %s (Score %d) -> %s",
|
||||||
f.get("STITLE"), score, result)
|
f.get("STITLE"), score, result)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
"""Save.TV — Filter: Produktionsland und Kino-Qualitaet.
|
"""Save.TV — Filter: Deutschland/Frankreich (Produktionsland via Wikidata-Cache).
|
||||||
|
|
||||||
Nutzt dieselbe .filminfo_cache.json wie savetv_extra_routes (Wikidata countries).
|
Nutzt dieselbe .filminfo_cache.json wie savetv_extra_routes (Wikidata countries).
|
||||||
Ohne Cache-Eintrag wird fuer Auto-Aufnahme/Auto-Download konservativ entschieden:
|
Ohne Cache-Eintrag fuer einen Titel wird nicht gefiltert (Film bleibt sichtbar).
|
||||||
oeffentlich-rechtliche/regional ausgestrahlte Titel bleiben liegen, bis sie
|
|
||||||
angereichert sind oder eindeutig wie internationale Kinofilme aussehen.
|
|
||||||
|
|
||||||
Deaktivieren: Umgebungsvariable SAVETV_FILTER_DE_FR=0
|
Deaktivieren: Umgebungsvariable SAVETV_FILTER_DE_FR=0
|
||||||
"""
|
"""
|
||||||
|
|
@ -27,75 +25,6 @@ _COUNTRY_EXCL = re.compile(
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_DACH_COUNTRY = re.compile(
|
|
||||||
r"\b("
|
|
||||||
r"deutschland|germany|allemagne|bundesrepublik(\s+deutschland)?|"
|
|
||||||
r"west\s+germany|east\s+germany|german\s+democratic\s+republic|"
|
|
||||||
r"rfa|\bbrd\b|\bddr\b|\bgdr\b|"
|
|
||||||
r"oesterreich|österreich|austria|schweiz|switzerland|suisse"
|
|
||||||
r")\b",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
_TRUSTED_CINEMA_COUNTRY = re.compile(
|
|
||||||
r"\b("
|
|
||||||
r"vereinigte\s+staaten|usa|united\s+states|"
|
|
||||||
r"vereinigtes\s+königreich|grossbritannien|großbritannien|"
|
|
||||||
r"great\s+britain|united\s+kingdom|kanada|canada|"
|
|
||||||
r"australien|australia|neuseeland|new\s+zealand|"
|
|
||||||
r"japan|südkorea|suedkorea|south\s+korea|hongkong|hong\s+kong"
|
|
||||||
r")\b",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
_PUBLIC_TV_STATIONS = {
|
|
||||||
"das erste", "ard", "zdf", "zdfneo", "3sat", "arte", "one hd", "one",
|
|
||||||
"rbb", "mdr", "br", "swr", "hr", "wdr", "ndr", "sr", "ard alpha",
|
|
||||||
}
|
|
||||||
|
|
||||||
_JUNK_TITLE = re.compile(
|
|
||||||
r"\b("
|
|
||||||
r"programmänderung|programmänderung!|doku|dokumentation|reportage|"
|
|
||||||
r"kommissar(?:in)?|krimi\s*-|dorfkrimi|krause[s]?|"
|
|
||||||
r"ein\s+fall\s+von\s+liebe|liebe\s+auf|"
|
|
||||||
r"ein\s+sommer\s+an|am\s+kap\s+der\s+liebe|"
|
|
||||||
r"das\s+geheimnis\s+(?:der|des)|willkommen\s+daheim|"
|
|
||||||
r"weihnachten|rosamunde\s+pilcher|inga\s+lindström|"
|
|
||||||
r"udo\s+witte|j\.d\.\s+vance"
|
|
||||||
r")\b",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
_REJECT_GENRE = {
|
|
||||||
"dokumentarfilm", "dokumentation", "musik", "musikfilm", "musical",
|
|
||||||
"familie", "familienfilm", "kinderfilm", "tv-film",
|
|
||||||
}
|
|
||||||
|
|
||||||
_DESIRED_GENRE_WORDS = {
|
|
||||||
"action", "thriller", "krimi", "kriminal", "abenteuer", "science",
|
|
||||||
"fiction", "sci-fi", "mystery", "western", "horror", "historie",
|
|
||||||
"krieg", "war", "drama",
|
|
||||||
}
|
|
||||||
|
|
||||||
_KNOWN_CINEMA_TITLE = {
|
|
||||||
"96 hours", "absolute power", "air america", "alarmstufe rot",
|
|
||||||
"american fighter", "appaloosa", "bastille day", "ben hur",
|
|
||||||
"blacklight", "blade runner", "bodyguard", "bohemian rhapsody",
|
|
||||||
"bourne", "braveheart", "carlito", "casino", "chinatown",
|
|
||||||
"colombiana", "contraband", "crank", "deadpool", "der staatsfeind",
|
|
||||||
"der clou", "the sting",
|
|
||||||
"die bourne identität", "die bourne identitaet", "das vermächtnis",
|
|
||||||
"das vermaechtnis", "g20", "green zone", "hitman", "manchurian",
|
|
||||||
"network", "stirb langsam", "the nice guys", "tom clancy",
|
|
||||||
}
|
|
||||||
|
|
||||||
_ENGLISH_WORDS = {
|
|
||||||
"the", "of", "and", "in", "for", "from", "with", "on", "at", "to",
|
|
||||||
"man", "men", "girl", "boy", "black", "white", "red", "blue", "last",
|
|
||||||
"night", "day", "dead", "death", "kill", "killer", "war", "zone",
|
|
||||||
"sea", "job", "agent", "daughter", "flood", "marine", "carnage",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _enabled() -> bool:
|
def _enabled() -> bool:
|
||||||
v = (os.environ.get("SAVETV_FILTER_DE_FR") or "1").strip().lower()
|
v = (os.environ.get("SAVETV_FILTER_DE_FR") or "1").strip().lower()
|
||||||
|
|
@ -136,121 +65,6 @@ def _countries_for_title(title: str, cache: dict) -> list[str] | None:
|
||||||
return [str(c)]
|
return [str(c)]
|
||||||
|
|
||||||
|
|
||||||
def _info_for_title(title: str, cache: dict) -> dict:
|
|
||||||
if not title or not cache:
|
|
||||||
return {}
|
|
||||||
entry = cache.get(title)
|
|
||||||
if entry is None:
|
|
||||||
nk = _norm_title_key(title)
|
|
||||||
for k, v in cache.items():
|
|
||||||
if isinstance(k, str) and _norm_title_key(k) == nk:
|
|
||||||
entry = v
|
|
||||||
break
|
|
||||||
return entry if isinstance(entry, dict) else {}
|
|
||||||
|
|
||||||
|
|
||||||
def _station_key(station: str) -> str:
|
|
||||||
return re.sub(r"\s+", " ", station or "").strip().lower()
|
|
||||||
|
|
||||||
|
|
||||||
def _is_public_tv_station(station: str) -> bool:
|
|
||||||
s = _station_key(station)
|
|
||||||
return s in _PUBLIC_TV_STATIONS
|
|
||||||
|
|
||||||
|
|
||||||
def _has_country(pattern: re.Pattern, countries: list[str]) -> bool:
|
|
||||||
return any(pattern.search(c or "") for c in countries)
|
|
||||||
|
|
||||||
|
|
||||||
def _genres(info: dict) -> list[str]:
|
|
||||||
raw = info.get("genres") if isinstance(info, dict) else []
|
|
||||||
if isinstance(raw, list):
|
|
||||||
return [str(g).lower() for g in raw if g]
|
|
||||||
if raw:
|
|
||||||
return [str(raw).lower()]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def _has_desired_genre(genres: list[str]) -> bool:
|
|
||||||
joined = " ".join(genres)
|
|
||||||
return any(word in joined for word in _DESIRED_GENRE_WORDS)
|
|
||||||
|
|
||||||
|
|
||||||
def _has_reject_genre(genres: list[str]) -> bool:
|
|
||||||
joined = " ".join(genres)
|
|
||||||
return any(g in joined for g in _REJECT_GENRE)
|
|
||||||
|
|
||||||
|
|
||||||
def _looks_like_cinema_title(title: str) -> bool:
|
|
||||||
t = _norm_title_key(title)
|
|
||||||
if _is_known_cinema_seed(t):
|
|
||||||
return True
|
|
||||||
words = set(re.findall(r"[a-z]+", title.lower()))
|
|
||||||
return len(words & _ENGLISH_WORDS) >= 2
|
|
||||||
|
|
||||||
|
|
||||||
def _is_known_cinema_seed(norm_title: str) -> bool:
|
|
||||||
return any(seed in norm_title for seed in _KNOWN_CINEMA_TITLE)
|
|
||||||
|
|
||||||
|
|
||||||
def classify_auto_quality(
|
|
||||||
title: str,
|
|
||||||
station: str = "",
|
|
||||||
cache: dict | None = None,
|
|
||||||
) -> tuple[bool, str]:
|
|
||||||
"""Entscheidet konservativ, ob ein Titel automatisch aufgenommen/geladen wird.
|
|
||||||
|
|
||||||
Zielprofil aus Jellyfin: internationale Kinofilme, vor allem Action, Thriller,
|
|
||||||
Krimi, Abenteuer, Sci-Fi, Western, Horror und starke Dramen. Deutsche/regionale
|
|
||||||
TV-Produktionen und unerkannte OeR-Titel werden nicht automatisch verarbeitet.
|
|
||||||
"""
|
|
||||||
if not _enabled():
|
|
||||||
return True, "filter-disabled"
|
|
||||||
|
|
||||||
if not title or _JUNK_TITLE.search(title):
|
|
||||||
return False, "title-junk-pattern"
|
|
||||||
|
|
||||||
cache = load_filminfo_cache() if cache is None else cache
|
|
||||||
info = _info_for_title(title, cache)
|
|
||||||
countries = _countries_for_title(title, cache) or []
|
|
||||||
genres = _genres(info)
|
|
||||||
public_tv = _is_public_tv_station(station)
|
|
||||||
known_cinema = _is_known_cinema_seed(_norm_title_key(title))
|
|
||||||
cinema_title = _looks_like_cinema_title(title)
|
|
||||||
|
|
||||||
description = str(info.get("description") or "").strip() if info else ""
|
|
||||||
if info and not known_cinema and len(description) < 40:
|
|
||||||
return False, "weak-enrichment"
|
|
||||||
|
|
||||||
if countries and _has_country(_COUNTRY_EXCL, countries):
|
|
||||||
return False, "country-de-fr"
|
|
||||||
|
|
||||||
if public_tv and countries and _has_country(_DACH_COUNTRY, countries):
|
|
||||||
return False, "public-tv-dach-production"
|
|
||||||
|
|
||||||
if genres and _has_reject_genre(genres):
|
|
||||||
return False, "reject-genre"
|
|
||||||
|
|
||||||
if countries and _has_country(_TRUSTED_CINEMA_COUNTRY, countries):
|
|
||||||
if _has_desired_genre(genres) or cinema_title:
|
|
||||||
return True, "trusted-cinema-profile"
|
|
||||||
return False, "trusted-country-weak-genre"
|
|
||||||
|
|
||||||
if known_cinema:
|
|
||||||
return True, "known-cinema-title"
|
|
||||||
|
|
||||||
if public_tv and not info:
|
|
||||||
return False, "public-tv-needs-enrichment"
|
|
||||||
|
|
||||||
if not info:
|
|
||||||
return False, "needs-enrichment"
|
|
||||||
|
|
||||||
if _has_desired_genre(genres) and cinema_title:
|
|
||||||
return True, "cinema-genre-profile"
|
|
||||||
|
|
||||||
return False, "not-jellyfin-profile"
|
|
||||||
|
|
||||||
|
|
||||||
def should_exclude_production_country(title: str, cache: dict | None = None) -> bool:
|
def should_exclude_production_country(title: str, cache: dict | None = None) -> bool:
|
||||||
"""True, wenn ein Produktionsland DE oder FR ist (laut Wikidata-Cache)."""
|
"""True, wenn ein Produktionsland DE oder FR ist (laut Wikidata-Cache)."""
|
||||||
if not _enabled():
|
if not _enabled():
|
||||||
|
|
|
||||||
44
homelab.conf
44
homelab.conf
|
|
@ -300,20 +300,12 @@ TUNNEL_600_MU3="arakavanews.com|:80|standby"
|
||||||
TUNNEL_601_MU3="rss-manager|:8080|standby"
|
TUNNEL_601_MU3="rss-manager|:8080|standby"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# HARDWARE-ZUKUNFT — Muldenstein/PP (Stand 2026-04-26)
|
# HARDWARE-ZUKUNFT — Muldenstein (Stand 2026-04-20)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# LEITLINIE NEU (ersetzt 2026-04-20):
|
# LEITLINIE: Es gibt genau EINE KI-Maschine — den bestehenden KI-Server
|
||||||
# Lokale KI fuer Coding & Jervais laeuft auf dem MacBook Pro 16"
|
# (Windows, RTX 3090, Tailscale 100.84.255.83). KEIN zweiter KI-Rechner
|
||||||
# M5 Max 128 GB / 2 TB (Kauf in HK, mobil DE+KH). Details und
|
# ist geplant. Wenn irgendwann ein Dual-GPU- oder Upgrade-Setup kommt,
|
||||||
# Begruendung: hardware-ai-local-coding.md im Repo-Root.
|
# ist das ein SEPARATER neuer Build — weder ki-tower noch MSI-B550-Build.
|
||||||
#
|
|
||||||
# Bestehender KI-Server (Windows + RTX 3090, Tailscale 100.84.255.83)
|
|
||||||
# bleibt fuer CUDA-Pflicht-Workloads (SDXL/XTTS/spezifische
|
|
||||||
# Pipelines). KEINE Erweiterung. Alte GPUs (4x 3080, 8x 6600 XT)
|
|
||||||
# werden verkauft. KEIN zweiter NVIDIA-Build, KEIN 2x5090-Rig.
|
|
||||||
# Aktueller M5 Max ist ein einzelnes Geraet, kein Cluster.
|
|
||||||
# M5 Ultra spaeter nur, wenn ein konkretes Coder-Modell den Sprung
|
|
||||||
# rechtfertigt.
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
# --- ki-tower (geplant, in Bau) --- JELLYFIN-ERSATZ
|
# --- ki-tower (geplant, in Bau) --- JELLYFIN-ERSATZ
|
||||||
|
|
@ -348,28 +340,10 @@ TUNNEL_601_MU3="rss-manager|:8080|standby"
|
||||||
# ROCm auf Navi 23 weiter inoffiziell. Fuer LLM praktisch irrelevant.
|
# ROCm auf Navi 23 weiter inoffiziell. Fuer LLM praktisch irrelevant.
|
||||||
# Status: offen, Tendenz Verkauf.
|
# Status: offen, Tendenz Verkauf.
|
||||||
|
|
||||||
# --- Dedizierte zweite KI-Maschine --- ERLEDIGT durch M5 Max
|
# --- Dedizierte zweite KI-Maschine --- NICHT GEPLANT
|
||||||
# Statt zweitem NVIDIA-Build wurde ein MacBook Pro 16" M5 Max
|
# Sollte der Bedarf (Dual-GPU, Mac Studio, RTX Pro 6000) jemals
|
||||||
# 128 GB / 2 TB als lokale KI-Maschine entschieden (siehe
|
# aufkommen, waere das ein KOMPLETT NEUER Build — NICHT ki-tower,
|
||||||
# hardware-ai-local-coding.md). Mobil, leise, 128 GB Unified
|
# NICHT MSI-B550, NICHT KI-Server-Umbau.
|
||||||
# Memory als ein Pool. Zaehlt logisch als die "zweite KI-Maschine".
|
|
||||||
# Standort: Phnom Penh primaer, Muldenstein bei Aufenthalt.
|
|
||||||
# Kein Eintrag in der Tailscale-Server-Tabelle, weil mobil.
|
|
||||||
|
|
||||||
# --- M5 Ultra (zukuenftig, optional) --- NICHT GEPLANT VOR MODELL
|
|
||||||
# Erst kaufen, wenn ein Coder-Modell vorliegt, das Cursor-4.7-Niveau
|
|
||||||
# lokal mit >128 GB Unified Memory erreicht. Bis dahin: kein Kauf.
|
|
||||||
# Pfad spaeter: M5 Max in Zahlung geben oder per Thunderbolt
|
|
||||||
# als Co-Worker behalten.
|
|
||||||
|
|
||||||
# --- Verkauf Alt-GPUs --- AKTIV (Test-Marathon naechste Woche)
|
|
||||||
# 4x RTX 3080 10 GB (Mining-Historie) -> testen auf MSI-B550-Rig,
|
|
||||||
# dann eBay-Kleinanzeigen.
|
|
||||||
# 1x RTX 3090 24 GB (kein Mining) -> aktuell 700-1100 EUR.
|
|
||||||
# 8x AMD RX 6600 XT 8 GB -> Verkauf, ROCm fuer LLM
|
|
||||||
# praktisch irrelevant.
|
|
||||||
# 1x MacBook Pro 14" M1 Pro 16 GB OVP -> Verkauf, finanziert M5 Max
|
|
||||||
# mit.
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# NAMENS-MAPPING (Alt → Neu) — Referenz für Migration
|
# NAMENS-MAPPING (Alt → Neu) — Referenz für Migration
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue