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
|
||||
- **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
|
||||
|
||||
- `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):
|
||||
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
|
||||
|
||||
if tid in dl_log:
|
||||
|
|
|
|||
|
|
@ -727,11 +727,23 @@ def get_new_films():
|
|||
|
||||
films = _filter_films(telecasts, only_new=True)
|
||||
_mark_seen(films)
|
||||
from tools import savetv_country_filter
|
||||
filminfo_cache = savetv_country_filter.load_filminfo_cache()
|
||||
|
||||
auto_recorded = []
|
||||
suggestions = []
|
||||
|
||||
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"]
|
||||
if score < SUGGEST_SCORE:
|
||||
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).
|
||||
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
|
||||
"""
|
||||
|
|
@ -25,6 +27,74 @@ _COUNTRY_EXCL = re.compile(
|
|||
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:
|
||||
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)]
|
||||
|
||||
|
||||
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:
|
||||
"""True, wenn ein Produktionsland DE oder FR ist (laut Wikidata-Cache)."""
|
||||
if not _enabled():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue