diff --git a/CHANGELOG-DEV.md b/CHANGELOG-DEV.md index 7dcc851..d5418a4 100644 --- a/CHANGELOG-DEV.md +++ b/CHANGELOG-DEV.md @@ -1,3 +1,7 @@ +## 0.1.86.0-dev - 2026-04-02 + +- dev: bump to 0.1.85.5-dev Settings-Menü benutzerfreundlicher gestaltet + ## 0.1.85.5-dev - 2026-04-02 - dev: bump to 0.1.85.0-dev settings.xml und default.py auf 0.1.84.5-Stand zurueckgesetzt, serienstream_plugin.py aktuell behalten diff --git a/addon/CHANGELOG-USER.md b/addon/CHANGELOG-USER.md new file mode 100644 index 0000000..97fe05d --- /dev/null +++ b/addon/CHANGELOG-USER.md @@ -0,0 +1,23 @@ +## 0.1.86.0 + +**Globale Suche verbessert** +- Jedes Such-Plugin kann in den Einstellungen unter „Globale Suche" einzeln aktiviert oder deaktiviert werden +- YouTube ist nun ebenfalls als optionale Suchquelle wählbar +- Suchergebnisse zeigen jetzt den Anbieter in eckigen Klammern an, z.B. „Breaking Bad [Serienstream]" + +**Einstellungsmenü übersichtlicher** +- Neue Kategorie „Globale Suche" mit Checkboxen für alle Anbieter +- Trakt-Verbindungsstatus direkt in den Einstellungen sichtbar +- Bevorzugter Hoster als Auswahlliste, nur aktiv wenn Autoplay eingeschaltet ist + +## 0.1.84.0 + +**Trakt Weiterschauen** +- Serien, die du bereits angeschaut hast, werden über Trakt als „Weiterschauen"-Liste angezeigt +- Specials (Staffel 0) werden dabei automatisch übersprungen + +## 0.1.82.0 + +**SerienStream Sammlungen** +- Sammlungen auf SerienStream sind jetzt durchsuchbar +- Die Liste wird alphabetisch sortiert und Sonderzeichen in Titeln werden bereinigt diff --git a/addon/addon.xml b/addon/addon.xml index 7711be7..7344974 100644 --- a/addon/addon.xml +++ b/addon/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/addon/default.py b/addon/default.py index f173ef6..b76e76a 100644 --- a/addon/default.py +++ b/addon/default.py @@ -2189,6 +2189,29 @@ def _clean_search_titles(values: list[str]) -> list[str]: return cleaned +_SEARCH_PLUGIN_SETTING_IDS: dict[str, str] = { + "Serienstream": "search_plugin_serienstream", + "Aniworld": "search_plugin_aniworld", + "Topstreamfilm": "search_plugin_topstreamfilm", + "Filmpalast": "search_plugin_filmpalast", + "Moflix": "search_plugin_moflix", + "KKiste": "search_plugin_kkiste", + "HDFilme": "search_plugin_hdfilme", + "Einschalten": "search_plugin_einschalten", + "Doku-Streams": "search_plugin_doku_streams", + "NetzkKino": "search_plugin_netzkino", + "YouTube": "search_plugin_youtube", +} + + +def _plugin_search_enabled(plugin_name: str) -> bool: + """Gibt True zurück wenn das Plugin in der globalen Suche aktiv ist.""" + setting_id = _SEARCH_PLUGIN_SETTING_IDS.get(plugin_name) + if setting_id is None: + return True # unbekannte Plugins standardmäßig einschließen + return _get_setting_bool(setting_id, default=True) + + def _show_search() -> None: _log("Suche gestartet.") dialog = xbmcgui.Dialog() @@ -2213,7 +2236,7 @@ def _show_search_results(query: str) -> None: # grouped: casefold-key → Liste der Plugin-Einträge für diesen Titel grouped: dict[str, list[dict[str, object]]] = {} canceled = False - plugin_entries = list(plugins.items()) + plugin_entries = [(n, p) for n, p in plugins.items() if _plugin_search_enabled(n)] total_plugins = max(1, len(plugin_entries)) with _progress_dialog("Suche laeuft", "Suche startet...") as progress: for plugin_index, (plugin_name, plugin) in enumerate(plugin_entries, start=1): @@ -2318,7 +2341,7 @@ def _show_search_results(query: str) -> None: # Nur ein Plugin → direkt zur Staffel-Ansicht direct_play = bool(first["direct_play"]) list_items.append({ - "label": first["label_base"], + "label": f"{first['label_base']} [{first['plugin_name']}]", "action": "play_movie" if direct_play else "seasons", "params": {"plugin": first["plugin_name"], "title": canonical_title, **dict(first["extra_params"])}, "is_folder": not direct_play, @@ -2329,8 +2352,9 @@ def _show_search_results(query: str) -> None: else: # Mehrere Plugins → Zwischenstufe "Quelle wählen" plugin_list = ",".join(str(e["plugin_name"]) for e in entries) + provider_label = ", ".join(str(e["plugin_name"]) for e in entries) list_items.append({ - "label": first["label_base"], + "label": f"{first['label_base']} [{provider_label}]", "action": "choose_source", "params": {"title": canonical_title, "plugins": plugin_list}, "is_folder": True, @@ -5559,6 +5583,38 @@ def _route_fallback(params: dict[str, str]) -> None: _show_root_menu() +def _maybe_show_changelog() -> None: + """Zeigt beim ersten Start nach einem Update das Benutzer-Changelog an.""" + try: + current_version = str(_get_addon().getAddonInfo("version") or "").strip() + last_shown = _get_setting_string("changelog_last_shown_version").strip() + if not current_version or current_version == last_shown: + return + changelog_path = Path(__file__).parent / "CHANGELOG-USER.md" + if not changelog_path.exists(): + return + text = changelog_path.read_text(encoding="utf-8") + # Ersten Abschnitt (neueste Version) extrahieren + lines = text.splitlines() + section: list[str] = [] + for line in lines: + if line.startswith("## ") and section: + break + section.append(line) + content = "\n".join(section).strip() + if not content: + return + viewer = getattr(xbmcgui.Dialog(), "textviewer", None) + if callable(viewer): + try: + viewer("Was ist neu?", content) + except Exception: + pass + _get_addon().setSetting("changelog_last_shown_version", current_version) + except Exception as exc: + _log(f"Changelog-Anzeige fehlgeschlagen: {exc}", xbmc.LOGWARNING) + + def run() -> None: params = _parse_params() action = params.get("action") @@ -5566,6 +5622,8 @@ def run() -> None: _maybe_run_auto_update_check(action) _maybe_auto_install_resolveurl(action) _sync_trakt_status_setting() + if not action: + _maybe_show_changelog() _router.dispatch(action=action, params=params) diff --git a/addon/resources/settings.xml b/addon/resources/settings.xml index 1d36878..caa3cfd 100644 --- a/addon/resources/settings.xml +++ b/addon/resources/settings.xml @@ -6,6 +6,20 @@ + + + + + + + + + + + + + + @@ -127,4 +141,8 @@ + + + + diff --git a/scripts/build_kodi_zip.sh b/scripts/build_kodi_zip.sh index 96277de..08021a7 100755 --- a/scripts/build_kodi_zip.sh +++ b/scripts/build_kodi_zip.sh @@ -34,6 +34,10 @@ PY ZIP_NAME="${ADDON_ID}-${ADDON_VERSION}.zip" ZIP_PATH="${INSTALL_DIR}/${ZIP_NAME}" +CHANGELOG_USER="${SRC_ADDON_DIR}/CHANGELOG-USER.md" +python3 "${ROOT_DIR}/scripts/update_user_changelog.py" --fill "${ADDON_XML}" "${CHANGELOG_USER}" >&2 +python3 "${ROOT_DIR}/scripts/update_user_changelog.py" --check "${ADDON_XML}" "${CHANGELOG_USER}" >&2 || exit 1 + ADDON_DIR="$("${ROOT_DIR}/scripts/build_install_addon.sh" >/dev/null; echo "${INSTALL_DIR}/${ADDON_ID}")" rm -f "${ZIP_PATH}" diff --git a/scripts/update_user_changelog.py b/scripts/update_user_changelog.py new file mode 100644 index 0000000..02b5849 --- /dev/null +++ b/scripts/update_user_changelog.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +""" +Pflegt CHANGELOG-USER.md vor dem ZIP-Build. + +Modi: + --fill Liest Commits seit letztem Tag, generiert lesbaren Text und ersetzt + den Platzhalter im Abschnitt der aktuellen Version. + Legt den Abschnitt an falls noch nicht vorhanden. + --check Prüft ob ein gefüllter Abschnitt für VERSION existiert. + Exit 0 = OK, Exit 1 = fehlt oder ist Platzhalter. + +Aufruf durch build_kodi_zip.sh: + python3 scripts/update_user_changelog.py --fill addon/addon.xml addon/CHANGELOG-USER.md + python3 scripts/update_user_changelog.py --check addon/addon.xml addon/CHANGELOG-USER.md +""" + +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +PLACEHOLDER_LINE = "(Bitte Changelog-Einträge hier einfügen)" + +# Schlüsselwörter aus Commit-Messages → lesbarer Kategorie-Text +# Reihenfolge bestimmt Priorität (erster Treffer gewinnt). +CATEGORY_RULES: list[tuple[re.Pattern, str]] = [ + (re.compile(r"trakt", re.I), "Trakt"), + (re.compile(r"serienstream|serien.?stream", re.I), "SerienStream"), + (re.compile(r"aniworld", re.I), "AniWorld"), + (re.compile(r"youtube|yt.?dlp", re.I), "YouTube"), + (re.compile(r"tmdb|metadat", re.I), "Metadaten"), + (re.compile(r"suche|search", re.I), "Suche"), + (re.compile(r"setting|einstellung", re.I), "Einstellungen"), + (re.compile(r"update|version", re.I), "Updates"), + (re.compile(r"moflix", re.I), "Moflix"), + (re.compile(r"filmpalast", re.I), "Filmpalast"), + (re.compile(r"kkiste", re.I), "KKiste"), + (re.compile(r"doku.?stream", re.I), "Doku-Streams"), + (re.compile(r"topstream", re.I), "Topstreamfilm"), + (re.compile(r"einschalten", re.I), "Einschalten"), + (re.compile(r"hdfilme", re.I), "HDFilme"), + (re.compile(r"netzkino", re.I), "NetzkKino"), +] + + +def get_version(addon_xml: Path) -> str: + root = ET.parse(addon_xml).getroot() + return root.attrib.get("version", "").strip() + + +def strip_suffix(version: str) -> str: + return re.sub(r"-(dev|nightly|beta|alpha).*$", "", version) + + +def get_prev_tag(current_tag: str) -> str | None: + try: + result = subprocess.run( + ["git", "tag", "--sort=-version:refname"], + capture_output=True, text=True, check=True, + ) + tags = [t.strip() for t in result.stdout.splitlines() if t.strip()] + if current_tag in tags: + idx = tags.index(current_tag) + return tags[idx + 1] if idx + 1 < len(tags) else None + return tags[0] if tags else None + except Exception: + return None + + +def get_commits_since(prev_tag: str | None) -> list[str]: + """Gibt Commit-Subjects seit prev_tag zurück (ohne den Tag-Commit selbst).""" + try: + ref = f"{prev_tag}..HEAD" if prev_tag else "HEAD" + result = subprocess.run( + ["git", "log", ref, "--pretty=format:%s"], + capture_output=True, text=True, check=True, + ) + return [l.strip() for l in result.stdout.splitlines() if l.strip()] + except Exception: + return [] + + +def extract_description(subject: str) -> str: + """Extrahiert den lesbaren Teil aus einer Commit-Message.""" + # "dev: bump to 0.1.86.0-dev Beschreibung hier" → "Beschreibung hier" + m = re.match(r"^(?:dev|nightly|main):\s*bump\s+to\s+[\d.]+(?:-\w+)?\s*(.+)$", subject, re.I) + if m: + return m.group(1).strip() + # "dev: Beschreibung" → "Beschreibung" + m = re.match(r"^(?:dev|nightly|main):\s*(.+)$", subject, re.I) + if m: + return m.group(1).strip() + return subject + + +def split_description(desc: str) -> list[str]: + """Zerlegt kommagetrennte Beschreibungen in Einzelpunkte.""" + parts = [p.strip() for p in re.split(r",\s*(?=[A-ZÄÖÜ\w])", desc) if p.strip()] + return parts if parts else [desc] + + +def categorize(items: list[str]) -> dict[str, list[str]]: + """Gruppiert Beschreibungs-Punkte nach Kategorie.""" + categories: dict[str, list[str]] = {} + for item in items: + cat = "Allgemein" + for pattern, label in CATEGORY_RULES: + if pattern.search(item): + cat = label + break + categories.setdefault(cat, []).append(item) + return categories + + +def build_changelog_text(commits: list[str]) -> str: + """Erzeugt lesbaren Changelog-Text aus Commit-Subjects.""" + all_items: list[str] = [] + for subject in commits: + desc = extract_description(subject) + if desc: + all_items.extend(split_description(desc)) + + if not all_items: + return "- Verschiedene Verbesserungen und Fehlerbehebungen" + + categories = categorize(all_items) + lines: list[str] = [] + for cat, items in categories.items(): + lines.append(f"**{cat}**") + for item in items: + # Ersten Buchstaben groß, Punkt am Ende + item = item[0].upper() + item[1:] if item else item + if not item.endswith((".","!","?")): + item += "." + lines.append(f"- {item}") + lines.append("") + return "\n".join(lines).rstrip() + + +def section_exists(lines: list[str], header: str) -> bool: + return any(line.strip() == header for line in lines) + + +def section_is_placeholder(lines: list[str], header: str) -> bool: + in_section = False + content_lines = [] + for line in lines: + if line.strip() == header: + in_section = True + continue + if in_section: + if line.startswith("## "): + break + stripped = line.strip() + if stripped: + content_lines.append(stripped) + return not content_lines or all(l == PLACEHOLDER_LINE for l in content_lines) + + +def replace_section_content(lines: list[str], header: str, new_content: str) -> list[str]: + """Ersetzt den Inhalt eines bestehenden Abschnitts.""" + result: list[str] = [] + in_section = False + content_written = False + for line in lines: + if line.strip() == header: + result.append(line) + result.append("") + result.extend(new_content.splitlines()) + result.append("") + in_section = True + content_written = True + continue + if in_section: + if line.startswith("## "): + in_section = False + result.append(line) + # Alte Zeilen des Abschnitts überspringen + continue + result.append(line) + return result + + +def insert_section(lines: list[str], header: str, content: str) -> list[str]: + new_section = [header, "", *content.splitlines(), ""] + return new_section + ([""] if lines and lines[0].strip() else []) + lines + + +def main() -> None: + if len(sys.argv) < 4 or sys.argv[1] not in ("--fill", "--check"): + print(f"Usage: {sys.argv[0]} --fill|--check ", file=sys.stderr) + sys.exit(2) + + mode = sys.argv[1] + addon_xml = Path(sys.argv[2]) + changelog = Path(sys.argv[3]) + + version = get_version(addon_xml) + if not version: + print("Fehler: Version konnte nicht aus addon.xml gelesen werden.", file=sys.stderr) + sys.exit(2) + + clean_version = strip_suffix(version) + header = f"## {clean_version}" + lines = changelog.read_text(encoding="utf-8").splitlines() if changelog.exists() else [] + + if mode == "--check": + if not section_exists(lines, header): + print(f"FEHLER: Kein Changelog-Abschnitt für {clean_version} in {changelog}", file=sys.stderr) + sys.exit(1) + if section_is_placeholder(lines, header): + print(f"FEHLER: Changelog-Abschnitt für {clean_version} ist noch ein Platzhalter.", file=sys.stderr) + sys.exit(1) + print(f"OK: Changelog-Abschnitt für {clean_version} vorhanden.") + sys.exit(0) + + # --fill + current_tag = f"v{version}" + prev_tag = get_prev_tag(current_tag) + commits = get_commits_since(prev_tag) + content = build_changelog_text(commits) + + if section_exists(lines, header): + if not section_is_placeholder(lines, header): + print(f"Abschnitt {header} bereits gefüllt – keine Änderung.") + sys.exit(0) + new_lines = replace_section_content(lines, header, content) + else: + new_lines = insert_section(lines, header, content) + + changelog.write_text("\n".join(new_lines) + "\n", encoding="utf-8") + print(f"Changelog für {header} geschrieben.") + sys.exit(0) + + +if __name__ == "__main__": + main()