Compare commits
13 Commits
v0.1.69-de
...
nightly-20
| Author | SHA1 | Date | |
|---|---|---|---|
| d414fac022 | |||
| 7a330c9bc0 | |||
| f8d180bcb5 | |||
| d71adcfac7 | |||
| 81750ad148 | |||
| 4409f9432c | |||
| 307df97d74 | |||
| 537f0e23e1 | |||
| ed1f59d3f2 | |||
| a37c45e2ef | |||
| 7f5924b850 | |||
| b370afe167 | |||
| 09d2fc850d |
@@ -1,29 +0,0 @@
|
||||
# Changelog (Nightly)
|
||||
|
||||
## 0.1.61-nightly - 2026-02-23
|
||||
|
||||
- Update-Dialog: feste Auswahl mit `Installieren` / `Abbrechen` (kein vertauschter Yes/No-Dialog mehr).
|
||||
- Versionen im Update-Dialog nach Kanal gefiltert:
|
||||
- Main: nur `x.y.z`
|
||||
- Nightly: nur `x.y.z-nightly`
|
||||
- Installierte Version wird direkt aus `addon.xml` gelesen.
|
||||
- Beim Kanalwechsel wird direkt die neueste Version aus dem gewaehlten Kanal installiert.
|
||||
|
||||
## 0.1.59-nightly - 2026-02-23
|
||||
|
||||
- Enthaelt alle Aenderungen aus `0.1.58`.
|
||||
- Update-Kanal standardmaessig auf `Nightly`.
|
||||
- Nightly-Repo-URL als Standard gesetzt.
|
||||
- Settings-Menue neu sortiert:
|
||||
- Quellen
|
||||
- Metadaten
|
||||
- TMDB Erweitert
|
||||
- Updates
|
||||
- Debug Global
|
||||
- Debug Quellen
|
||||
- Seitengroesse in Listen auf 20 gesetzt.
|
||||
- `topstream_genre_max_pages` entfernt.
|
||||
|
||||
## Hinweis
|
||||
|
||||
- Nightly ist fuer Tests und kann sich kurzfristig aendern.
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,23 +0,0 @@
|
||||
# Changelog (Stable)
|
||||
|
||||
## 0.1.61 - 2026-02-23
|
||||
|
||||
- Menues und Labels weiter vereinheitlicht (ASCII-only, einheitliche Texte pro Plugin).
|
||||
- Update-Bereich ueberarbeitet:
|
||||
- Kanalwechsel mit direkter Installation der neuesten Kanal-Version.
|
||||
- Version-Auswahl mit Changelog-Anzeige und klarer Installieren/Abbrechen-Auswahl.
|
||||
- Anzeige der installierten Version direkt aus lokaler `addon.xml`.
|
||||
- Kanal-spezifischer Versionsfilter (Main nur stable, Nightly nur `-nightly`).
|
||||
- Resolver-/Playback-Flow vereinheitlicht und Hoster-URL-Normalisierung zentralisiert.
|
||||
- Settings aufgeraeumt (strukturierte Kategorien, reduzierte Alt-Optionen).
|
||||
|
||||
## 0.1.58 - 2026-02-23
|
||||
|
||||
- Menuebezeichnungen vereinheitlicht (`Haeufig gesehen`, `Neuste Titel`).
|
||||
- `Neue Titel` und `Neueste Folgen` im Menue zu `Neuste Titel` zusammengelegt.
|
||||
- Hoster-Header-Anpassung zentral nach `resolve_stream_link` eingebaut.
|
||||
- Hinweis bei Cloudflare-Block durch ResolveURL statt stiller Fehlversuche.
|
||||
- Update-Einstellungen erweitert (Kanal, manueller Check, optionaler Auto-Check).
|
||||
- Metadaten-Parsing in AniWorld und Filmpalast nachgezogen (Cover/Plot robuster).
|
||||
- Topstreamfilm-Suche: fehlender `urlencode`-Import behoben.
|
||||
- Einige ungenutzte Funktionen entfernt.
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.61" provider-name="ViewIt">
|
||||
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.57" provider-name="ViewIt">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.0" />
|
||||
<import addon="script.module.requests" />
|
||||
|
||||
1104
addon/default.py
1104
addon/default.py
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from typing import Optional
|
||||
from urllib.parse import parse_qsl, urlencode
|
||||
|
||||
try: # pragma: no cover - Kodi runtime
|
||||
import xbmcaddon # type: ignore[import-not-found]
|
||||
@@ -239,40 +237,3 @@ def dump_response_html(
|
||||
max_files = get_setting_int(addon_id, max_files_setting_id, default=200)
|
||||
_prune_dump_files(log_dir, prefix=filename_prefix, max_files=max_files)
|
||||
_append_text_file(path, content)
|
||||
|
||||
|
||||
def normalize_resolved_stream_url(final_url: str, *, source_url: str = "") -> str:
|
||||
"""Normalisiert hoster-spezifische Header im finalen Stream-Link.
|
||||
|
||||
`final_url` kann ein Kodi-Header-Suffix enthalten: `url|Key=Value&...`.
|
||||
Die Funktion passt nur bekannte Problemfaelle an und laesst sonst alles unveraendert.
|
||||
"""
|
||||
|
||||
url = (final_url or "").strip()
|
||||
if not url:
|
||||
return ""
|
||||
normalized = _normalize_supervideo_serversicuro(url, source_url=source_url)
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_supervideo_serversicuro(final_url: str, *, source_url: str = "") -> str:
|
||||
if "serversicuro.cc/hls/" not in final_url.casefold() or "|" not in final_url:
|
||||
return final_url
|
||||
|
||||
source = (source_url or "").strip()
|
||||
code_match = re.search(
|
||||
r"supervideo\.(?:tv|cc)/(?:e/)?([a-z0-9]+)(?:\\.html)?",
|
||||
source,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if not code_match:
|
||||
return final_url
|
||||
|
||||
code = (code_match.group(1) or "").strip()
|
||||
if not code:
|
||||
return final_url
|
||||
|
||||
media_url, header_suffix = final_url.split("|", 1)
|
||||
headers = dict(parse_qsl(header_suffix, keep_blank_values=True))
|
||||
headers["Referer"] = f"https://supervideo.cc/e/{code}"
|
||||
return f"{media_url}|{urlencode(headers)}"
|
||||
|
||||
@@ -754,7 +754,6 @@ class AniworldPlugin(BasisPlugin):
|
||||
def __init__(self) -> None:
|
||||
self._anime_results: Dict[str, SeriesResult] = {}
|
||||
self._title_url_cache: Dict[str, str] = self._load_title_url_cache()
|
||||
self._title_meta: Dict[str, tuple[str, str]] = {}
|
||||
self._genre_names_cache: Optional[List[str]] = None
|
||||
self._season_cache: Dict[str, List[SeasonInfo]] = {}
|
||||
self._season_links_cache: Dict[str, List[SeasonInfo]] = {}
|
||||
@@ -819,135 +818,8 @@ class AniworldPlugin(BasisPlugin):
|
||||
changed = True
|
||||
if changed and persist:
|
||||
self._save_title_url_cache()
|
||||
if description:
|
||||
old_plot, old_poster = self._title_meta.get(title, ("", ""))
|
||||
self._title_meta[title] = (description.strip() or old_plot, old_poster)
|
||||
return changed
|
||||
|
||||
def _store_title_meta(self, title: str, *, plot: str = "", poster: str = "") -> None:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
return
|
||||
old_plot, old_poster = self._title_meta.get(title, ("", ""))
|
||||
merged_plot = (plot or old_plot or "").strip()
|
||||
merged_poster = (poster or old_poster or "").strip()
|
||||
self._title_meta[title] = (merged_plot, merged_poster)
|
||||
|
||||
@staticmethod
|
||||
def _is_series_image_url(url: str) -> bool:
|
||||
value = (url or "").strip().casefold()
|
||||
if not value:
|
||||
return False
|
||||
blocked = (
|
||||
"/public/img/facebook",
|
||||
"/public/img/logo",
|
||||
"aniworld-logo",
|
||||
"favicon",
|
||||
"/public/img/german.svg",
|
||||
"/public/img/japanese-",
|
||||
)
|
||||
return not any(marker in value for marker in blocked)
|
||||
|
||||
@staticmethod
|
||||
def _extract_style_url(style_value: str) -> str:
|
||||
style_value = (style_value or "").strip()
|
||||
if not style_value:
|
||||
return ""
|
||||
match = re.search(r"url\((['\"]?)(.*?)\1\)", style_value, flags=re.IGNORECASE)
|
||||
if not match:
|
||||
return ""
|
||||
return (match.group(2) or "").strip()
|
||||
|
||||
def _extract_series_metadata(self, soup: BeautifulSoupT) -> tuple[str, str, str]:
|
||||
if not soup:
|
||||
return "", "", ""
|
||||
plot = ""
|
||||
poster = ""
|
||||
fanart = ""
|
||||
|
||||
root = soup.select_one("#series") or soup
|
||||
|
||||
description_node = root.select_one("p.seri_des")
|
||||
if description_node is not None:
|
||||
full_text = (description_node.get("data-full-description") or "").strip()
|
||||
short_text = (description_node.get_text(" ", strip=True) or "").strip()
|
||||
plot = full_text or short_text
|
||||
|
||||
if not plot:
|
||||
for selector in ("meta[property='og:description']", "meta[name='description']"):
|
||||
node = soup.select_one(selector)
|
||||
if node is None:
|
||||
continue
|
||||
content = (node.get("content") or "").strip()
|
||||
if content:
|
||||
plot = content
|
||||
break
|
||||
if not plot:
|
||||
for selector in (".series-description", ".seri_des", ".description", "article p"):
|
||||
node = soup.select_one(selector)
|
||||
if node is None:
|
||||
continue
|
||||
text = (node.get_text(" ", strip=True) or "").strip()
|
||||
if text:
|
||||
plot = text
|
||||
break
|
||||
|
||||
cover = root.select_one("div.seriesCoverBox img[itemprop='image'], div.seriesCoverBox img")
|
||||
if cover is not None:
|
||||
for attr in ("data-src", "src"):
|
||||
value = (cover.get(attr) or "").strip()
|
||||
if value:
|
||||
candidate = _absolute_url(value)
|
||||
if self._is_series_image_url(candidate):
|
||||
poster = candidate
|
||||
break
|
||||
|
||||
if not poster:
|
||||
for selector in ("meta[property='og:image']", "meta[name='twitter:image']"):
|
||||
node = soup.select_one(selector)
|
||||
if node is None:
|
||||
continue
|
||||
content = (node.get("content") or "").strip()
|
||||
if content:
|
||||
candidate = _absolute_url(content)
|
||||
if self._is_series_image_url(candidate):
|
||||
poster = candidate
|
||||
break
|
||||
if not poster:
|
||||
for selector in ("img.seriesCoverBox", ".seriesCoverBox img"):
|
||||
image = soup.select_one(selector)
|
||||
if image is None:
|
||||
continue
|
||||
value = (image.get("data-src") or image.get("src") or "").strip()
|
||||
if value:
|
||||
candidate = _absolute_url(value)
|
||||
if self._is_series_image_url(candidate):
|
||||
poster = candidate
|
||||
break
|
||||
|
||||
backdrop_node = root.select_one("section.title .backdrop, .SeriesSection .backdrop, .backdrop")
|
||||
if backdrop_node is not None:
|
||||
raw_style = (backdrop_node.get("style") or "").strip()
|
||||
style_url = self._extract_style_url(raw_style)
|
||||
if style_url:
|
||||
candidate = _absolute_url(style_url)
|
||||
if self._is_series_image_url(candidate):
|
||||
fanart = candidate
|
||||
|
||||
if not fanart:
|
||||
for selector in ("meta[property='og:image']",):
|
||||
node = soup.select_one(selector)
|
||||
if node is None:
|
||||
continue
|
||||
content = (node.get("content") or "").strip()
|
||||
if content:
|
||||
candidate = _absolute_url(content)
|
||||
if self._is_series_image_url(candidate):
|
||||
fanart = candidate
|
||||
break
|
||||
|
||||
return plot, poster, fanart
|
||||
|
||||
@staticmethod
|
||||
def _season_links_cache_name(series_url: str) -> str:
|
||||
digest = hashlib.sha1((series_url or "").encode("utf-8")).hexdigest()[:20]
|
||||
@@ -1079,43 +951,6 @@ class AniworldPlugin(BasisPlugin):
|
||||
|
||||
return None
|
||||
|
||||
def metadata_for(self, title: str) -> tuple[dict[str, str], dict[str, str], list[object] | None]:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
return {}, {}, None
|
||||
|
||||
info: dict[str, str] = {"title": title}
|
||||
art: dict[str, str] = {}
|
||||
cached_plot, cached_poster = self._title_meta.get(title, ("", ""))
|
||||
if cached_plot:
|
||||
info["plot"] = cached_plot
|
||||
if cached_poster:
|
||||
art = {"thumb": cached_poster, "poster": cached_poster}
|
||||
if "plot" in info and art:
|
||||
return info, art, None
|
||||
|
||||
series = self._find_series_by_title(title)
|
||||
if series is None or not series.url:
|
||||
return info, art, None
|
||||
if series.description and "plot" not in info:
|
||||
info["plot"] = series.description
|
||||
|
||||
try:
|
||||
soup = _get_soup(series.url, session=get_requests_session("aniworld", headers=HEADERS))
|
||||
plot, poster, fanart = self._extract_series_metadata(soup)
|
||||
except Exception:
|
||||
plot, poster, fanart = "", "", ""
|
||||
|
||||
if plot:
|
||||
info["plot"] = plot
|
||||
if poster:
|
||||
art = {"thumb": poster, "poster": poster}
|
||||
if fanart:
|
||||
art["fanart"] = fanart
|
||||
art["landscape"] = fanart
|
||||
self._store_title_meta(title, plot=info.get("plot", ""), poster=poster)
|
||||
return info, art, None
|
||||
|
||||
def _ensure_popular(self) -> List[SeriesResult]:
|
||||
if self._popular_cache is not None:
|
||||
return list(self._popular_cache)
|
||||
|
||||
@@ -603,6 +603,15 @@ class EinschaltenPlugin(BasisPlugin):
|
||||
url = urljoin(base + "/", path.lstrip("/"))
|
||||
return f"{url}?{urlencode({'query': query})}"
|
||||
|
||||
def _api_movies_url(self, *, with_genres: int, page: int = 1) -> str:
|
||||
base = self._get_base_url()
|
||||
if not base:
|
||||
return ""
|
||||
params: Dict[str, str] = {"withGenres": str(int(with_genres))}
|
||||
if page and int(page) > 1:
|
||||
params["page"] = str(int(page))
|
||||
return urljoin(base + "/", "api/movies") + f"?{urlencode(params)}"
|
||||
|
||||
def _genre_page_url(self, *, genre_id: int, page: int = 1) -> str:
|
||||
"""Genre title pages are rendered server-side and embed the movie list in ng-state.
|
||||
|
||||
@@ -762,6 +771,23 @@ class EinschaltenPlugin(BasisPlugin):
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _fetch_new_titles_movies(self) -> List[MovieItem]:
|
||||
# "Neue Filme" lives at `/movies/new` and embeds the list in ng-state (`u: "/api/movies"`).
|
||||
url = self._new_titles_url()
|
||||
if not url:
|
||||
return []
|
||||
try:
|
||||
_, body = self._http_get_text(url, timeout=20)
|
||||
payload = _extract_ng_state_payload(body)
|
||||
movies = _parse_ng_state_movies(payload)
|
||||
_log_debug_line(f"parse_ng_state_movies:count={len(movies)}")
|
||||
if movies:
|
||||
_log_titles(movies, context="new_titles")
|
||||
return movies
|
||||
return []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _fetch_new_titles_movies_page(self, page: int) -> List[MovieItem]:
|
||||
page = max(1, int(page or 1))
|
||||
url = self._new_titles_url()
|
||||
|
||||
@@ -244,7 +244,6 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._title_to_url: Dict[str, str] = {}
|
||||
self._title_meta: Dict[str, tuple[str, str]] = {}
|
||||
self._series_entries: Dict[str, Dict[int, Dict[int, EpisodeEntry]]] = {}
|
||||
self._hoster_cache: Dict[str, Dict[str, str]] = {}
|
||||
self._genre_to_url: Dict[str, str] = {}
|
||||
@@ -723,64 +722,6 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
return hit.url
|
||||
return ""
|
||||
|
||||
def _store_title_meta(self, title: str, *, plot: str = "", poster: str = "") -> None:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
return
|
||||
old_plot, old_poster = self._title_meta.get(title, ("", ""))
|
||||
merged_plot = (plot or old_plot or "").strip()
|
||||
merged_poster = (poster or old_poster or "").strip()
|
||||
self._title_meta[title] = (merged_plot, merged_poster)
|
||||
|
||||
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]:
|
||||
if not soup:
|
||||
return "", ""
|
||||
root = soup.select_one("div#content[role='main']") or soup
|
||||
detail = root.select_one("article.detail") or root
|
||||
plot = ""
|
||||
poster = ""
|
||||
|
||||
# Filmpalast Detailseite: bevorzugt den dedizierten Filmhandlung-Block.
|
||||
plot_node = detail.select_one(
|
||||
"li[itemtype='http://schema.org/Movie'] span[itemprop='description']"
|
||||
)
|
||||
if plot_node is not None:
|
||||
plot = (plot_node.get_text(" ", strip=True) or "").strip()
|
||||
if not plot:
|
||||
hidden_plot = detail.select_one("cite span.hidden")
|
||||
if hidden_plot is not None:
|
||||
plot = (hidden_plot.get_text(" ", strip=True) or "").strip()
|
||||
if not plot:
|
||||
for selector in ("meta[property='og:description']", "meta[name='description']"):
|
||||
node = root.select_one(selector)
|
||||
if node is None:
|
||||
continue
|
||||
content = (node.get("content") or "").strip()
|
||||
if content:
|
||||
plot = content
|
||||
break
|
||||
|
||||
# Filmpalast Detailseite: Cover liegt stabil in `img.cover2`.
|
||||
cover = detail.select_one("img.cover2")
|
||||
if cover is not None:
|
||||
value = (cover.get("data-src") or cover.get("src") or "").strip()
|
||||
if value:
|
||||
candidate = _absolute_url(value)
|
||||
lower = candidate.casefold()
|
||||
if "/themes/" not in lower and "spacer.gif" not in lower and "/files/movies/" in lower:
|
||||
poster = candidate
|
||||
if not poster:
|
||||
thumb_node = detail.select_one("li[itemtype='http://schema.org/Movie'] img[itemprop='image']")
|
||||
if thumb_node is not None:
|
||||
value = (thumb_node.get("data-src") or thumb_node.get("src") or "").strip()
|
||||
if value:
|
||||
candidate = _absolute_url(value)
|
||||
lower = candidate.casefold()
|
||||
if "/themes/" not in lower and "spacer.gif" not in lower and "/files/movies/" in lower:
|
||||
poster = candidate
|
||||
|
||||
return plot, poster
|
||||
|
||||
def remember_series_url(self, title: str, series_url: str) -> None:
|
||||
title = (title or "").strip()
|
||||
series_url = (series_url or "").strip()
|
||||
@@ -801,52 +742,6 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
return _series_hint_value(series_key)
|
||||
return ""
|
||||
|
||||
def metadata_for(self, title: str) -> tuple[dict[str, str], dict[str, str], list[object] | None]:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
return {}, {}, None
|
||||
|
||||
info: dict[str, str] = {"title": title}
|
||||
art: dict[str, str] = {}
|
||||
cached_plot, cached_poster = self._title_meta.get(title, ("", ""))
|
||||
if cached_plot:
|
||||
info["plot"] = cached_plot
|
||||
if cached_poster:
|
||||
art = {"thumb": cached_poster, "poster": cached_poster}
|
||||
if "plot" in info and art:
|
||||
return info, art, None
|
||||
|
||||
detail_url = self._ensure_title_url(title)
|
||||
if not detail_url:
|
||||
series_key = self._series_key_for_title(title) or self._ensure_series_entries_for_title(title)
|
||||
if series_key:
|
||||
seasons = self._series_entries.get(series_key, {})
|
||||
first_entry: Optional[EpisodeEntry] = None
|
||||
for season_number in sorted(seasons.keys()):
|
||||
episodes = seasons.get(season_number, {})
|
||||
for episode_number in sorted(episodes.keys()):
|
||||
first_entry = episodes.get(episode_number)
|
||||
if first_entry is not None:
|
||||
break
|
||||
if first_entry is not None:
|
||||
break
|
||||
detail_url = first_entry.url if first_entry is not None else ""
|
||||
if not detail_url:
|
||||
return info, art, None
|
||||
|
||||
try:
|
||||
soup = _get_soup(detail_url, session=get_requests_session("filmpalast", headers=HEADERS))
|
||||
plot, poster = self._extract_detail_metadata(soup)
|
||||
except Exception:
|
||||
plot, poster = "", ""
|
||||
|
||||
if plot:
|
||||
info["plot"] = plot
|
||||
if poster:
|
||||
art = {"thumb": poster, "poster": poster}
|
||||
self._store_title_meta(title, plot=info.get("plot", ""), poster=poster)
|
||||
return info, art, None
|
||||
|
||||
def is_movie(self, title: str) -> bool:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
|
||||
@@ -1096,7 +1096,7 @@ class SerienstreamPlugin(BasisPlugin):
|
||||
|
||||
name = "Serienstream"
|
||||
version = "1.0.0"
|
||||
POPULAR_GENRE_LABEL = "Haeufig gesehen"
|
||||
POPULAR_GENRE_LABEL = "⭐ Beliebte Serien"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._series_results: Dict[str, SeriesResult] = {}
|
||||
|
||||
@@ -66,9 +66,12 @@ SETTING_LOG_URLS = "log_urls_topstreamfilm"
|
||||
SETTING_DUMP_HTML = "dump_html_topstreamfilm"
|
||||
SETTING_SHOW_URL_INFO = "show_url_info_topstreamfilm"
|
||||
SETTING_LOG_ERRORS = "log_errors_topstreamfilm"
|
||||
SETTING_GENRE_MAX_PAGES = "topstream_genre_max_pages"
|
||||
DEFAULT_TIMEOUT = 20
|
||||
DEFAULT_PREFERRED_HOSTERS = ["supervideo", "dropload", "voe"]
|
||||
MEINECLOUD_HOST = "meinecloud.click"
|
||||
DEFAULT_GENRE_MAX_PAGES = 20
|
||||
HARD_MAX_GENRE_PAGES = 200
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Kodi; ViewIt) AppleWebKit/537.36 (KHTML, like Gecko)",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
@@ -94,7 +97,6 @@ class SearchHit:
|
||||
title: str
|
||||
url: str
|
||||
description: str = ""
|
||||
poster: str = ""
|
||||
|
||||
|
||||
def _normalize_search_text(value: str) -> str:
|
||||
@@ -147,7 +149,6 @@ class TopstreamfilmPlugin(BasisPlugin):
|
||||
self._season_to_episode_numbers: Dict[tuple[str, str], List[int]] = {}
|
||||
self._episode_title_by_number: Dict[tuple[str, int, int], str] = {}
|
||||
self._detail_html_cache: Dict[str, str] = {}
|
||||
self._title_meta: Dict[str, tuple[str, str]] = {}
|
||||
self._popular_cache: List[str] | None = None
|
||||
self._default_preferred_hosters: List[str] = list(DEFAULT_PREFERRED_HOSTERS)
|
||||
self._preferred_hosters: List[str] = list(self._default_preferred_hosters)
|
||||
@@ -344,6 +345,22 @@ class TopstreamfilmPlugin(BasisPlugin):
|
||||
return urljoin(base if base.endswith("/") else base + "/", href)
|
||||
return href
|
||||
|
||||
def _get_setting_bool(self, setting_id: str, *, default: bool = False) -> bool:
|
||||
return get_setting_bool(ADDON_ID, setting_id, default=default)
|
||||
|
||||
def _get_setting_int(self, setting_id: str, *, default: int) -> int:
|
||||
if xbmcaddon is None:
|
||||
return default
|
||||
try:
|
||||
addon = xbmcaddon.Addon(ADDON_ID)
|
||||
getter = getattr(addon, "getSettingInt", None)
|
||||
if callable(getter):
|
||||
return int(getter(setting_id))
|
||||
raw = str(addon.getSetting(setting_id) or "").strip()
|
||||
return int(raw) if raw else default
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def _notify_url(self, url: str) -> None:
|
||||
notify_url(
|
||||
ADDON_ID,
|
||||
@@ -412,7 +429,6 @@ class TopstreamfilmPlugin(BasisPlugin):
|
||||
continue
|
||||
seen.add(hit.title)
|
||||
self._title_to_url[hit.title] = hit.url
|
||||
self._store_title_meta(hit.title, plot=hit.description, poster=hit.poster)
|
||||
titles.append(hit.title)
|
||||
if titles:
|
||||
self._save_title_url_cache()
|
||||
@@ -471,69 +487,6 @@ class TopstreamfilmPlugin(BasisPlugin):
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _pick_image_from_node(self, node: Any) -> str:
|
||||
if node is None:
|
||||
return ""
|
||||
image = node.select_one("img")
|
||||
if image is None:
|
||||
return ""
|
||||
for attr in ("data-src", "src"):
|
||||
value = (image.get(attr) or "").strip()
|
||||
if value and "lazy_placeholder" not in value.casefold():
|
||||
return self._absolute_external_url(value, base=self._get_base_url())
|
||||
srcset = (image.get("data-srcset") or image.get("srcset") or "").strip()
|
||||
if srcset:
|
||||
first = srcset.split(",")[0].strip().split(" ", 1)[0].strip()
|
||||
if first:
|
||||
return self._absolute_external_url(first, base=self._get_base_url())
|
||||
return ""
|
||||
|
||||
def _store_title_meta(self, title: str, *, plot: str = "", poster: str = "") -> None:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
return
|
||||
old_plot, old_poster = self._title_meta.get(title, ("", ""))
|
||||
merged_plot = (plot or old_plot or "").strip()
|
||||
merged_poster = (poster or old_poster or "").strip()
|
||||
self._title_meta[title] = (merged_plot, merged_poster)
|
||||
|
||||
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]:
|
||||
if not soup:
|
||||
return "", ""
|
||||
plot = ""
|
||||
poster = ""
|
||||
for selector in ("meta[property='og:description']", "meta[name='description']"):
|
||||
node = soup.select_one(selector)
|
||||
if node is None:
|
||||
continue
|
||||
content = (node.get("content") or "").strip()
|
||||
if content:
|
||||
plot = content
|
||||
break
|
||||
if not plot:
|
||||
candidates: list[str] = []
|
||||
for paragraph in soup.select("article p, .TPost p, .Description p, .entry-content p"):
|
||||
text = (paragraph.get_text(" ", strip=True) or "").strip()
|
||||
if len(text) >= 60:
|
||||
candidates.append(text)
|
||||
if candidates:
|
||||
plot = max(candidates, key=len)
|
||||
|
||||
for selector in ("meta[property='og:image']", "meta[name='twitter:image']"):
|
||||
node = soup.select_one(selector)
|
||||
if node is None:
|
||||
continue
|
||||
content = (node.get("content") or "").strip()
|
||||
if content:
|
||||
poster = self._absolute_external_url(content, base=self._get_base_url())
|
||||
break
|
||||
if not poster:
|
||||
for selector in ("article", ".TPost", ".entry-content"):
|
||||
poster = self._pick_image_from_node(soup.select_one(selector))
|
||||
if poster:
|
||||
break
|
||||
return plot, poster
|
||||
|
||||
def _clear_stream_index_for_title(self, title: str) -> None:
|
||||
for key in list(self._season_to_episode_numbers.keys()):
|
||||
if key[0] == title:
|
||||
@@ -768,17 +721,7 @@ class TopstreamfilmPlugin(BasisPlugin):
|
||||
continue
|
||||
if is_movie_hint:
|
||||
self._movie_title_hint.add(title)
|
||||
description_tag = item.select_one(".TPMvCn .Description, .Description, .entry-summary")
|
||||
description = (description_tag.get_text(" ", strip=True) or "").strip() if description_tag else ""
|
||||
poster = self._pick_image_from_node(item)
|
||||
hits.append(
|
||||
SearchHit(
|
||||
title=title,
|
||||
url=self._absolute_url(href),
|
||||
description=description,
|
||||
poster=poster,
|
||||
)
|
||||
)
|
||||
hits.append(SearchHit(title=title, url=self._absolute_url(href), description=""))
|
||||
return hits
|
||||
|
||||
def is_movie(self, title: str) -> bool:
|
||||
@@ -851,7 +794,6 @@ class TopstreamfilmPlugin(BasisPlugin):
|
||||
continue
|
||||
seen.add(hit.title)
|
||||
self._title_to_url[hit.title] = hit.url
|
||||
self._store_title_meta(hit.title, plot=hit.description, poster=hit.poster)
|
||||
titles.append(hit.title)
|
||||
if titles:
|
||||
self._save_title_url_cache()
|
||||
@@ -963,8 +905,7 @@ class TopstreamfilmPlugin(BasisPlugin):
|
||||
self._movie_title_hint.add(title)
|
||||
description_tag = item.select_one(".TPMvCn .Description")
|
||||
description = description_tag.get_text(" ", strip=True) if description_tag else ""
|
||||
poster = self._pick_image_from_node(item)
|
||||
hit = SearchHit(title=title, url=self._absolute_url(href), description=description, poster=poster)
|
||||
hit = SearchHit(title=title, url=self._absolute_url(href), description=description)
|
||||
if _matches_query(query, title=hit.title, description=hit.description):
|
||||
hits.append(hit)
|
||||
|
||||
@@ -977,41 +918,11 @@ class TopstreamfilmPlugin(BasisPlugin):
|
||||
continue
|
||||
seen.add(hit.title)
|
||||
self._title_to_url[hit.title] = hit.url
|
||||
self._store_title_meta(hit.title, plot=hit.description, poster=hit.poster)
|
||||
titles.append(hit.title)
|
||||
self._save_title_url_cache()
|
||||
_emit_progress(progress_callback, f"Fertig: {len(titles)} Treffer", 95)
|
||||
return titles
|
||||
|
||||
def metadata_for(self, title: str) -> tuple[dict[str, str], dict[str, str], list[object] | None]:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
return {}, {}, None
|
||||
|
||||
info: dict[str, str] = {"title": title}
|
||||
art: dict[str, str] = {}
|
||||
|
||||
cached_plot, cached_poster = self._title_meta.get(title, ("", ""))
|
||||
if cached_plot:
|
||||
info["plot"] = cached_plot
|
||||
if cached_poster:
|
||||
art = {"thumb": cached_poster, "poster": cached_poster}
|
||||
|
||||
if "plot" in info and art:
|
||||
return info, art, None
|
||||
|
||||
soup = self._get_detail_soup(title)
|
||||
if soup is None:
|
||||
return info, art, None
|
||||
|
||||
plot, poster = self._extract_detail_metadata(soup)
|
||||
if plot:
|
||||
info["plot"] = plot
|
||||
if poster:
|
||||
art = {"thumb": poster, "poster": poster}
|
||||
self._store_title_meta(title, plot=plot, poster=poster)
|
||||
return info, art, None
|
||||
|
||||
def genres(self) -> List[str]:
|
||||
if not REQUESTS_AVAILABLE or BeautifulSoup is None:
|
||||
return []
|
||||
|
||||
@@ -8,16 +8,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
_LAST_RESOLVE_ERROR = ""
|
||||
|
||||
|
||||
def get_last_error() -> str:
|
||||
return str(_LAST_RESOLVE_ERROR or "")
|
||||
|
||||
|
||||
def resolve(url: str) -> Optional[str]:
|
||||
global _LAST_RESOLVE_ERROR
|
||||
_LAST_RESOLVE_ERROR = ""
|
||||
if not url:
|
||||
return None
|
||||
try:
|
||||
@@ -31,14 +23,12 @@ def resolve(url: str) -> Optional[str]:
|
||||
hmf = hosted(url)
|
||||
valid = getattr(hmf, "valid_url", None)
|
||||
if callable(valid) and not valid():
|
||||
_LAST_RESOLVE_ERROR = "invalid url"
|
||||
return None
|
||||
resolver = getattr(hmf, "resolve", None)
|
||||
if callable(resolver):
|
||||
result = resolver()
|
||||
return str(result) if result else None
|
||||
except Exception as exc:
|
||||
_LAST_RESOLVE_ERROR = str(exc or "")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
@@ -46,8 +36,8 @@ def resolve(url: str) -> Optional[str]:
|
||||
if callable(resolve_fn):
|
||||
result = resolve_fn(url)
|
||||
return str(result) if result else None
|
||||
except Exception as exc:
|
||||
_LAST_RESOLVE_ERROR = str(exc or "")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,66 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings>
|
||||
<category label="Quellen">
|
||||
<setting id="serienstream_base_url" type="text" label="SerienStream Basis-URL" default="https://s.to" />
|
||||
<setting id="aniworld_base_url" type="text" label="AniWorld Basis-URL" default="https://aniworld.to" />
|
||||
<setting id="topstream_base_url" type="text" label="TopStream Basis-URL" default="https://topstreamfilm.live" />
|
||||
<setting id="einschalten_base_url" type="text" label="Einschalten Basis-URL" default="https://einschalten.in" />
|
||||
<setting id="filmpalast_base_url" type="text" label="Filmpalast Basis-URL" default="https://filmpalast.to" />
|
||||
<setting id="doku_streams_base_url" type="text" label="Doku-Streams Basis-URL" default="https://doku-streams.com" />
|
||||
</category>
|
||||
|
||||
<category label="Metadaten">
|
||||
<setting id="serienstream_metadata_source" type="enum" label="SerienStream Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
<setting id="aniworld_metadata_source" type="enum" label="AniWorld Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
<setting id="topstreamfilm_metadata_source" type="enum" label="TopStream Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
<setting id="einschalten_metadata_source" type="enum" label="Einschalten Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
<setting id="filmpalast_metadata_source" type="enum" label="Filmpalast Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
<setting id="doku_streams_metadata_source" type="enum" label="Doku-Streams Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
<setting id="tmdb_enabled" type="bool" label="TMDB aktivieren" default="true" />
|
||||
<setting id="tmdb_language" type="text" label="TMDB Sprache (z. B. de-DE)" default="de-DE" />
|
||||
<setting id="tmdb_show_plot" type="bool" label="TMDB Beschreibung anzeigen" default="true" />
|
||||
<setting id="tmdb_show_art" type="bool" label="TMDB Poster und Vorschaubild anzeigen" default="true" />
|
||||
<setting id="tmdb_show_fanart" type="bool" label="TMDB Fanart/Backdrop anzeigen" default="true" />
|
||||
<setting id="tmdb_show_rating" type="bool" label="TMDB Bewertung anzeigen" default="true" />
|
||||
<setting id="tmdb_show_votes" type="bool" label="TMDB Stimmen anzeigen" default="false" />
|
||||
</category>
|
||||
|
||||
<category label="TMDB Erweitert">
|
||||
<setting id="tmdb_api_key" type="text" label="TMDB API Key" default="" />
|
||||
<setting id="tmdb_prefetch_concurrency" type="number" label="TMDB: gleichzeitige Anfragen (1-20)" default="6" />
|
||||
<setting id="tmdb_show_cast" type="bool" label="TMDB Besetzung anzeigen" default="false" />
|
||||
<setting id="tmdb_show_episode_cast" type="bool" label="TMDB Besetzung pro Episode anzeigen" default="false" />
|
||||
<setting id="tmdb_genre_metadata" type="bool" label="TMDB Daten in Genre-Listen anzeigen" default="false" />
|
||||
<setting id="tmdb_log_requests" type="bool" label="TMDB API-Anfragen loggen" default="false" />
|
||||
<setting id="tmdb_log_responses" type="bool" label="TMDB API-Antworten loggen" default="false" />
|
||||
</category>
|
||||
|
||||
<category label="Updates">
|
||||
<setting id="update_channel" type="enum" label="Update-Kanal" default="0" values="Main|Nightly|Custom" />
|
||||
<setting id="apply_update_channel" type="action" label="Update-Kanal jetzt anwenden" action="RunPlugin(plugin://plugin.video.viewit/?action=apply_update_channel)" option="close" />
|
||||
<setting id="auto_update_enabled" type="bool" label="Automatische Updates (beim Start pruefen)" default="false" />
|
||||
<setting id="select_update_version" type="action" label="Version waehlen und installieren" action="RunPlugin(plugin://plugin.video.viewit/?action=select_update_version)" option="close" />
|
||||
<setting id="update_installed_version" type="text" label="Installierte Version" default="-" enable="false" />
|
||||
<setting id="update_available_selected" type="text" label="Verfuegbar (gewaehlter Kanal)" default="-" enable="false" />
|
||||
<setting id="update_available_main" type="text" label="Verfuegbar Main" default="-" enable="false" />
|
||||
<setting id="update_available_nightly" type="text" label="Verfuegbar Nightly" default="-" enable="false" />
|
||||
<setting id="update_active_channel" type="text" label="Aktiver Kanal" default="-" enable="false" />
|
||||
<setting id="update_active_repo_url" type="text" label="Aktive Repo URL" default="-" enable="false" />
|
||||
<setting id="update_info" type="text" label="Updates laufen ueber den normalen Kodi-Update-Mechanismus." default="" enable="false" />
|
||||
<setting id="update_repo_url_main" type="text" label="Main URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml" />
|
||||
<setting id="update_repo_url_nightly" type="text" label="Nightly URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml" />
|
||||
<setting id="update_repo_url" type="text" label="Custom URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml" />
|
||||
<setting id="auto_update_last_ts" type="text" label="Auto-Update letzte Pruefung (intern)" default="0" visible="false" />
|
||||
<setting id="update_version_addon" type="text" label="ViewIT Version" default="-" visible="false" />
|
||||
<setting id="update_version_serienstream" type="text" label="SerienStream Version" default="-" visible="false" />
|
||||
<setting id="update_version_aniworld" type="text" label="AniWorld Version" default="-" visible="false" />
|
||||
<setting id="update_version_einschalten" type="text" label="Einschalten Version" default="-" visible="false" />
|
||||
<setting id="update_version_topstreamfilm" type="text" label="TopStream Version" default="-" visible="false" />
|
||||
<setting id="update_version_filmpalast" type="text" label="Filmpalast Version" default="-" visible="false" />
|
||||
<setting id="update_version_doku_streams" type="text" label="Doku-Streams Version" default="-" visible="false" />
|
||||
</category>
|
||||
|
||||
<category label="Debug Global">
|
||||
<category label="Debug und Logs">
|
||||
<setting id="debug_log_urls" type="bool" label="URLs mitschreiben (global)" default="false" />
|
||||
<setting id="debug_dump_html" type="bool" label="HTML speichern (global)" default="false" />
|
||||
<setting id="debug_show_url_info" type="bool" label="Aktuelle URL anzeigen (global)" default="false" />
|
||||
@@ -68,32 +8,78 @@
|
||||
<setting id="log_max_mb" type="number" label="URL-Log: maximale Dateigroesse (MB)" default="5" />
|
||||
<setting id="log_max_files" type="number" label="URL-Log: Anzahl alter Dateien" default="3" />
|
||||
<setting id="dump_max_files" type="number" label="HTML: maximale Dateien pro Plugin" default="200" />
|
||||
</category>
|
||||
|
||||
<category label="Debug Quellen">
|
||||
<setting id="log_urls_serienstream" type="bool" label="SerienStream: URLs mitschreiben" default="false" />
|
||||
<setting id="dump_html_serienstream" type="bool" label="SerienStream: HTML speichern" default="false" />
|
||||
<setting id="show_url_info_serienstream" type="bool" label="SerienStream: Aktuelle URL anzeigen" default="false" />
|
||||
<setting id="log_errors_serienstream" type="bool" label="SerienStream: Fehler mitschreiben" default="false" />
|
||||
|
||||
<setting id="log_urls_aniworld" type="bool" label="AniWorld: URLs mitschreiben" default="false" />
|
||||
<setting id="dump_html_aniworld" type="bool" label="AniWorld: HTML speichern" default="false" />
|
||||
<setting id="show_url_info_aniworld" type="bool" label="AniWorld: Aktuelle URL anzeigen" default="false" />
|
||||
<setting id="log_errors_aniworld" type="bool" label="AniWorld: Fehler mitschreiben" default="false" />
|
||||
|
||||
<setting id="log_urls_topstreamfilm" type="bool" label="TopStream: URLs mitschreiben" default="false" />
|
||||
<setting id="dump_html_topstreamfilm" type="bool" label="TopStream: HTML speichern" default="false" />
|
||||
<setting id="show_url_info_topstreamfilm" type="bool" label="TopStream: Aktuelle URL anzeigen" default="false" />
|
||||
<setting id="log_errors_topstreamfilm" type="bool" label="TopStream: Fehler mitschreiben" default="false" />
|
||||
|
||||
<setting id="log_urls_serienstream" type="bool" label="Serienstream: URLs mitschreiben" default="false" />
|
||||
<setting id="dump_html_serienstream" type="bool" label="Serienstream: HTML speichern" default="false" />
|
||||
<setting id="show_url_info_serienstream" type="bool" label="Serienstream: Aktuelle URL anzeigen" default="false" />
|
||||
<setting id="log_errors_serienstream" type="bool" label="Serienstream: Fehler mitschreiben" default="false" />
|
||||
<setting id="log_urls_aniworld" type="bool" label="Aniworld: URLs mitschreiben" default="false" />
|
||||
<setting id="dump_html_aniworld" type="bool" label="Aniworld: HTML speichern" default="false" />
|
||||
<setting id="show_url_info_aniworld" type="bool" label="Aniworld: Aktuelle URL anzeigen" default="false" />
|
||||
<setting id="log_errors_aniworld" type="bool" label="Aniworld: Fehler mitschreiben" default="false" />
|
||||
<setting id="log_urls_topstreamfilm" type="bool" label="Topstreamfilm: URLs mitschreiben" default="false" />
|
||||
<setting id="dump_html_topstreamfilm" type="bool" label="Topstreamfilm: HTML speichern" default="false" />
|
||||
<setting id="show_url_info_topstreamfilm" type="bool" label="Topstreamfilm: Aktuelle URL anzeigen" default="false" />
|
||||
<setting id="log_errors_topstreamfilm" type="bool" label="Topstreamfilm: Fehler mitschreiben" default="false" />
|
||||
<setting id="log_urls_einschalten" type="bool" label="Einschalten: URLs mitschreiben" default="false" />
|
||||
<setting id="dump_html_einschalten" type="bool" label="Einschalten: HTML speichern" default="false" />
|
||||
<setting id="show_url_info_einschalten" type="bool" label="Einschalten: Aktuelle URL anzeigen" default="false" />
|
||||
<setting id="log_errors_einschalten" type="bool" label="Einschalten: Fehler mitschreiben" default="false" />
|
||||
|
||||
<setting id="log_urls_filmpalast" type="bool" label="Filmpalast: URLs mitschreiben" default="false" />
|
||||
<setting id="dump_html_filmpalast" type="bool" label="Filmpalast: HTML speichern" default="false" />
|
||||
<setting id="show_url_info_filmpalast" type="bool" label="Filmpalast: Aktuelle URL anzeigen" default="false" />
|
||||
<setting id="log_errors_filmpalast" type="bool" label="Filmpalast: Fehler mitschreiben" default="false" />
|
||||
</category>
|
||||
<category label="TopStream">
|
||||
<setting id="topstream_base_url" type="text" label="Basis-URL" default="https://topstreamfilm.live" />
|
||||
<setting id="topstreamfilm_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
<setting id="topstream_genre_max_pages" type="number" label="Genres: max. Seiten laden" default="20" />
|
||||
</category>
|
||||
<category label="SerienStream">
|
||||
<setting id="serienstream_base_url" type="text" label="Basis-URL" default="https://s.to" />
|
||||
<setting id="serienstream_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
</category>
|
||||
<category label="AniWorld">
|
||||
<setting id="aniworld_base_url" type="text" label="Basis-URL" default="https://aniworld.to" />
|
||||
<setting id="aniworld_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
</category>
|
||||
<category label="Einschalten">
|
||||
<setting id="einschalten_base_url" type="text" label="Basis-URL" default="https://einschalten.in" />
|
||||
<setting id="einschalten_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
</category>
|
||||
<category label="Filmpalast">
|
||||
<setting id="filmpalast_base_url" type="text" label="Basis-URL" default="https://filmpalast.to" />
|
||||
<setting id="filmpalast_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
</category>
|
||||
<category label="Doku-Streams">
|
||||
<setting id="doku_streams_base_url" type="text" label="Basis-URL" default="https://doku-streams.com" />
|
||||
<setting id="doku_streams_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||
</category>
|
||||
<category label="TMDB">
|
||||
<setting id="tmdb_enabled" type="bool" label="TMDB aktivieren" default="true" />
|
||||
<setting id="tmdb_api_key" type="text" label="TMDB API Key" default="" />
|
||||
<setting id="tmdb_language" type="text" label="TMDB Sprache (z. B. de-DE)" default="de-DE" />
|
||||
<setting id="tmdb_prefetch_concurrency" type="number" label="TMDB: gleichzeitige Anfragen (1-20)" default="6" />
|
||||
<setting id="tmdb_show_plot" type="bool" label="TMDB Beschreibung anzeigen" default="true" />
|
||||
<setting id="tmdb_show_art" type="bool" label="TMDB Poster und Vorschaubild anzeigen" default="true" />
|
||||
<setting id="tmdb_show_fanart" type="bool" label="TMDB Fanart/Backdrop anzeigen" default="true" />
|
||||
<setting id="tmdb_show_rating" type="bool" label="TMDB Bewertung anzeigen" default="true" />
|
||||
<setting id="tmdb_show_votes" type="bool" label="TMDB Stimmen anzeigen" default="false" />
|
||||
<setting id="tmdb_show_cast" type="bool" label="TMDB Besetzung anzeigen" default="false" />
|
||||
<setting id="tmdb_show_episode_cast" type="bool" label="TMDB Besetzung pro Episode anzeigen" default="false" />
|
||||
<setting id="tmdb_genre_metadata" type="bool" label="TMDB Daten in Genre-Listen anzeigen" default="false" />
|
||||
<setting id="tmdb_log_requests" type="bool" label="TMDB API-Anfragen loggen" default="false" />
|
||||
<setting id="tmdb_log_responses" type="bool" label="TMDB API-Antworten loggen" default="false" />
|
||||
</category>
|
||||
<category label="Update">
|
||||
<setting id="update_repo_url" type="text" label="Update-URL (addons.xml)" default="http://127.0.0.1:8080/repo/addons.xml" />
|
||||
<setting id="run_update_check" type="action" label="Jetzt nach Updates suchen" action="RunPlugin(plugin://plugin.video.viewit/?action=check_updates)" option="close" />
|
||||
<setting id="update_info" type="text" label="Updates laufen ueber den normalen Kodi-Update-Mechanismus." default="" enable="false" />
|
||||
<setting id="update_version_addon" type="text" label="ViewIT Version" default="-" enable="false" />
|
||||
<setting id="update_version_serienstream" type="text" label="Serienstream Version" default="-" enable="false" />
|
||||
<setting id="update_version_aniworld" type="text" label="Aniworld Version" default="-" enable="false" />
|
||||
<setting id="update_version_einschalten" type="text" label="Einschalten Version" default="-" enable="false" />
|
||||
<setting id="update_version_topstreamfilm" type="text" label="Topstreamfilm Version" default="-" enable="false" />
|
||||
<setting id="update_version_filmpalast" type="text" label="Filmpalast Version" default="-" enable="false" />
|
||||
<setting id="update_version_doku_streams" type="text" label="Doku-Streams Version" default="-" enable="false" />
|
||||
</category>
|
||||
</settings>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Release Flow (Main + Nightly)
|
||||
|
||||
This project uses two release channels:
|
||||
|
||||
- `nightly`: integration and test channel
|
||||
- `main`: stable channel
|
||||
|
||||
## Rules
|
||||
|
||||
- Feature work goes to `nightly` only.
|
||||
- Promote from `nightly` to `main` with `--squash` only.
|
||||
- `main` version has no suffix (`0.1.60`).
|
||||
- `nightly` version uses `-nightly` and is always at least one patch higher than `main` (`0.1.61-nightly`).
|
||||
- Keep changelogs split:
|
||||
- `CHANGELOG-NIGHTLY.md`
|
||||
- `CHANGELOG.md`
|
||||
|
||||
## Nightly publish
|
||||
|
||||
1) Finish changes on `nightly`.
|
||||
2) Bump addon version in `addon/addon.xml` to `X.Y.Z-nightly`.
|
||||
3) Build and publish nightly repo artifacts.
|
||||
4) Push `nightly`.
|
||||
|
||||
## Promote nightly to main
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git merge --squash nightly
|
||||
git commit -m "release: X.Y.Z"
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
1) Set `addon/addon.xml` version to `X.Y.Z` (without `-nightly`).
|
||||
2) Build and publish main repo artifacts.
|
||||
3) Push `main`.
|
||||
4) Optional tag: `vX.Y.Z`.
|
||||
|
||||
## Local ZIPs (separated)
|
||||
|
||||
- Main ZIP output: `dist/local_zips/main/`
|
||||
- Nightly ZIP output: `dist/local_zips/nightly/`
|
||||
Reference in New Issue
Block a user