From e49216dca50635e60a77973f5ea7e91c629ba412 Mon Sep 17 00:00:00 2001 From: nessi Date: Sat, 15 Aug 2026 12:37:58 +0200 Subject: [PATCH] feat: add category-based grouping for Sonarr series with anime and K-drama tag detection Add tag fetching from Sonarr API and category_for() function to classify series based on normalized tags (anime, kdrama, standard). Parse tag labels/names into normalized strings removing non-alphanumeric characters for matching. Add category and category_label fields to series groups. Update index route to build series_categories list grouping series by category key (anime/Anime, kdrama/K-Dramen, standard/Standard) and pass --- app.py | 25 +++++++++++++++++++++++-- templates/index.html | 3 ++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 844be48..a5d2b63 100644 --- a/app.py +++ b/app.py @@ -372,7 +372,22 @@ def sonarr_groups(): groups = [] queue = api_get(SONARR_URL, SONARR_API_KEY, "queue?includeUnknownSeriesItems=true") queued_episodes = {item.get("episodeId") for item in queue.get("records", [])} + tags = api_get(SONARR_URL, SONARR_API_KEY, "tag") + tag_names = {str(tag.get("id")): str(tag.get("label") or tag.get("name") or "").strip().casefold() for tag in tags} + + def category_for(series): + normalized_tags = { + re.sub(r"[^a-z0-9]+", "", tag_names.get(str(tag_id), "")) + for tag_id in (series.get("tags") or []) + } + if "anime" in normalized_tags: + return "anime", "Anime" + if "kdrama" in normalized_tags: + return "kdrama", "K-Dramen" + return "standard", "Standard" + for series in api_get(SONARR_URL, SONARR_API_KEY, "series"): + category_key, category_label = category_for(series) episodes = api_get(SONARR_URL, SONARR_API_KEY, f"episode?seriesId={series['id']}&includeSeries=false") files = {f["id"]: f for f in api_get(SONARR_URL, SONARR_API_KEY, f"episodefile?seriesId={series['id']}")} items = [] @@ -381,7 +396,8 @@ def sonarr_groups(): if ef: ef = dict(ef); ef["path"] = str(Path(series.get("path") or "") / ef.get("relativePath", "")) label = f"S{episode.get('seasonNumber', 0):02d}E{episode.get('episodeNumber', 0):02d} · {episode.get('title') or '—'}" items.append(media_row(label, None, ef, SONARR_MEDIA_PATH, LOCAL_SERIES_PATH, {"episode":True, "season":episode.get("seasonNumber", 0), "downloading": episode.get("id") in queued_episodes})) - groups.append({"title":series.get("title", "—"), "year":series.get("year"), "episodes":items}) + groups.append({"title":series.get("title", "—"), "year":series.get("year"), "episodes":items, + "category":category_key, "category_label":category_label}) return sorted(groups, key=lambda x: x["title"].lower()) def start_scan(source, force=False): @@ -425,6 +441,10 @@ def index(): episodes = [episode for episode in group["episodes"] if is_german_review_candidate(episode) and not has_german_track(episode)] if episodes: missing_german_series.append({**group, "episodes": episodes}) + series_categories = [ + {"key": key, "label": label, "series": [group for group in series if group["category"] == key]} + for key, label in (("anime", "Anime"), ("kdrama", "K-Dramen"), ("standard", "Standard")) + ] dashboard_stats = { "movie_total": len(rows), "movie_files": sum(1 for row in rows if not row.get("missing")), @@ -435,7 +455,8 @@ def index(): "episode_files": sum(1 for group in series for row in group["episodes"] if not row.get("missing")), "episode_missing": sum(1 for group in series for row in group["episodes"] if row.get("missing") and not row.get("downloading")), } - return render_template("index.html", rows=rows, series=series, missing_german_movies=missing_german_movies, + return render_template("index.html", rows=rows, series=series, series_categories=series_categories, + missing_german_movies=missing_german_movies, missing_german_series=missing_german_series, error=error, dashboard_stats=dashboard_stats, sonarr_enabled=bool(SONARR_URL and SONARR_API_KEY), sab_enabled=bool(SAB_URL and SAB_API_KEY), diff --git a/templates/index.html b/templates/index.html index 968d81d..90621c7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -22,7 +22,7 @@ {% for r in rows %}{{ r.title }}{{ r.year or '' }}{% if r.error %} ⚠{% endif %}{{ r.quality }}{{ r.state.label }}
{% for l in r.audio_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{% for l in r.subtitle_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{{ r.video_codec }}{{ r.size }}{% endfor %} {% if sonarr_enabled %}
{{ series|length }} SerienScan wird vorbereitet …Dashboard: {{ local_series_path }}
-{% for group in series %}
{{ group.title }} ({{ group.year or '—' }}) · {{ group.episodes|length }} Episoden{% for r in group.episodes %}{% endfor %}
EpisodeQualityStatusAudioUntertitelVideoGröße
{{ r.title }}{{ r.quality }}{{ r.state.label }}
{% for l in r.audio_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{% for l in r.subtitle_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{{ r.video_codec }}{{ r.size }}
{% endfor %} +{% for category in series_categories %}{% if category.series %}

{{ category.label }}

{{ category.series|length }} Serien
{% for group in category.series %}
{{ group.title }} ({{ group.year or '—' }}) · {{ group.episodes|length }} Episoden{% for r in group.episodes %}{% endfor %}
EpisodeQualityStatusAudioUntertitelVideoGröße
{{ r.title }}{{ r.quality }}{{ r.state.label }}
{% for l in r.audio_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{% for l in r.subtitle_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{{ r.video_codec }}{{ r.size }}
{% endfor %}
{% endif %}{% endfor %}
{% endif %}
{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }} Einträge ohne DeutschVorhanden oder wird gerade geladen · Staffel 0 ausgeblendet
{% for r in missing_german_movies %}{% endfor %}
FilmQualitätStatusAudioUntertitelVideoGröße
{{ r.title }}{{ r.year or '' }}{{ r.quality }}Ohne Deutsch
{% for l in r.audio_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{% for l in r.subtitle_languages %}{{ l.flag }} {{ l.name }}{% else %}{% endfor %}
{{ r.video_codec }}{{ r.size }}
@@ -53,6 +53,7 @@ if('serviceWorker' in navigator)window.addEventListener('load',()=>navigator.ser