fix(savetv): auto-download nur fuer Kino-Profil erlauben
This commit is contained in:
parent
d0b2c182de
commit
ba9c55a916
4 changed files with 213 additions and 3 deletions
|
|
@ -30,6 +30,13 @@ 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.
|
||||||
|
|
||||||
## 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,7 +88,13 @@ def run():
|
||||||
if savetv._is_excluded(title):
|
if savetv._is_excluded(title):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if savetv_country_filter.should_exclude_production_country(title, filminfo_cache):
|
allow_download, quality_reason = savetv_country_filter.classify_auto_quality(
|
||||||
|
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:
|
||||||
|
|
|
||||||
|
|
@ -727,11 +727,23 @@ 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:
|
||||||
|
allow_auto, quality_reason = savetv_country_filter.classify_auto_quality(
|
||||||
|
f.get("STITLE", ""),
|
||||||
|
f.get("STVSTATIONNAME", ""),
|
||||||
|
filminfo_cache,
|
||||||
|
)
|
||||||
|
if not allow_auto:
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"""Save.TV — Filter: Deutschland/Frankreich (Produktionsland via Wikidata-Cache).
|
"""Save.TV — Filter: Produktionsland und Kino-Qualitaet.
|
||||||
|
|
||||||
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 fuer einen Titel wird nicht gefiltert (Film bleibt sichtbar).
|
Ohne Cache-Eintrag wird fuer Auto-Aufnahme/Auto-Download konservativ entschieden:
|
||||||
|
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
|
||||||
"""
|
"""
|
||||||
|
|
@ -25,6 +27,74 @@ _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", "anna", "appaloosa", "bastille day", "ben hur",
|
||||||
|
"blacklight", "blade runner", "bodyguard", "bohemian rhapsody",
|
||||||
|
"bourne", "braveheart", "carlito", "casino", "chinatown",
|
||||||
|
"colombiana", "contraband", "crank", "deadpool", "der staatsfeind",
|
||||||
|
"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()
|
||||||
|
|
@ -65,6 +135,121 @@ 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():
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue