From effa03b2a547c44482439af690ea74894fcd0909 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 30 May 2026 09:08:54 -0500 Subject: [PATCH] Enhancementvod): optimize xc_get_series for performance and reduce API response time --- CHANGELOG.md | 2 +- apps/output/views.py | 81 ++++++++++++++++++++++++++++---------------- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78dea04e..4e9a3762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Performance -- **`get_vod_streams` XC API response time reduced significantly for large libraries.** The endpoint previously queried `Movie` with a `Prefetch` that loaded all active relations per movie (one row per account that carries the title), then discarded all but the highest-priority one in Python. With 48k movies across multiple active accounts this fetched a multiple of 48k rows. The query now uses a single `DISTINCT ON (movie_id)` pass over `M3UMovieRelation` ordered by account priority, returning exactly one row per movie. Logo URL construction (`reverse()` + `build_absolute_uri_with_port()`) is also computed once and reused via prefix/suffix string concatenation, matching the existing pattern in `get_live_streams`. Observed improvement: 18s → 12s for a 48k-movie library. +- **`get_vod_streams` and `get_series` XC API response times reduced significantly for large libraries.** Both endpoints previously loaded all active relations per title (one row per account that carries the same movie/series), then discarded duplicates in Python. With large libraries across multiple active accounts this fetched a multiple of N rows. Both queries now use a single `DISTINCT ON (movie_id/series_id)` pass over the relation table ordered by account priority, returning exactly one row per title. Logo URL construction (`reverse()` + `build_absolute_uri_with_port()`) is also computed once and reused via prefix/suffix string concatenation, matching the existing pattern in `get_live_streams`. The `m3u_account` JOIN in `get_series` was also removed (it was fetched but never read in the loop). Additionally, `get_series` previously had no `order_by` on `M3USeriesRelation`, so output order was non-deterministic (physical storage order). Both endpoints now sort alphabetically by title. Observed improvements: 18s → 12s for 48k movies; 9.5s → 3.5s for 11k series. - **Database connections are now managed by a persistent per-worker pool.** Previously each request opened a fresh TCP connection to PostgreSQL, paid a full authentication handshake, and closed the connection at request end. `django-db-geventpool` now maintains a pool of warm connections per uWSGI worker; requests borrow a connection and return it when done, eliminating connection-setup overhead on every request. Pool size is bounded (`MAX_CONNS=8` per worker, `REUSE_CONNS=3` warm connections kept idle) to stay comfortably within PostgreSQL's default `max_connections=100` across all uWSGI workers, Celery workers, and Daphne. — Thanks [@JCBird1012](https://github.com/JCBird1012) - **Reduced Redis round-trips on the Stats page channel status endpoint.** `get_basic_channel_info` was making up to 6 individual `HGET` calls per connected client plus a redundant `HGET` for `TOTAL_BYTES` (already present in the preceding `HGETALL` result). Client metadata is now fetched with a single `HMGET` per client, and `TOTAL_BYTES` is read from the already-fetched hash. Under load with many active streams this significantly reduces the time each uWSGI worker holds the GIL servicing the stats endpoint, reducing the chance of concurrent requests from other pages timing out with a 503. The same `HGET`-to-`HMGET` consolidation was applied to `stream_ts` and `get_user_active_connections`. The Stats page frontend was also fixed to fire the initial fetch only once on mount (previously two `useEffect` hooks both triggered an immediate fetch on load). — Thanks [@JCBird1012](https://github.com/JCBird1012) diff --git a/apps/output/views.py b/apps/output/views.py index 44c44a86..bc6046a2 100644 --- a/apps/output/views.py +++ b/apps/output/views.py @@ -2655,47 +2655,70 @@ def xc_get_series_categories(user): def xc_get_series(request, user, category_id=None): """Get series list for XtreamCodes API""" from apps.vod.models import M3USeriesRelation + from django.db import connection - series_list = [] - - # All authenticated users get access to series from all active M3U accounts - filters = {"m3u_account__is_active": True} - + rel_filters = {"m3u_account__is_active": True} if category_id: - filters["category_id"] = category_id + rel_filters["category_id"] = category_id - # Get series relations instead of series directly - series_relations = M3USeriesRelation.objects.filter(**filters).select_related( - 'series', 'series__logo', 'category', 'm3u_account' + base_qs = ( + M3USeriesRelation.objects + .filter(**rel_filters) + .select_related('series', 'series__logo', 'category') ) - for relation in series_relations: + if connection.vendor == 'postgresql': + relations = list( + base_qs.order_by('series_id', '-m3u_account__priority', 'id') + .distinct('series_id') + ) + else: + seen: dict = {} + for rel in base_qs.order_by('-m3u_account__priority', 'id'): + if rel.series_id not in seen: + seen[rel.series_id] = rel + relations = list(seen.values()) + + _base_url = build_absolute_uri_with_port(request, "") + _sample_logo_path = reverse("api:vod:vodlogo-cache", args=[0]) + _logo_prefix_raw, _, _logo_suffix_raw = _sample_logo_path.partition("/0/") + _logo_url_prefix = _base_url + _logo_prefix_raw + "/" + _logo_url_suffix = "/" + _logo_suffix_raw + + relations.sort(key=lambda r: (r.series.name or "").lower()) + + series_list = [] + append = series_list.append + for num, relation in enumerate(relations, 1): series = relation.series - series_list.append({ - "num": relation.id, # Use relation ID + custom_props = series.custom_properties or {} + category = relation.category + rating = series.rating + logo = series.logo + year_str = str(series.year) if series.year else "" + release_date = custom_props.get('release_date', year_str) + + append({ + "num": num, "name": series.name, - "series_id": relation.id, # Use relation ID + "series_id": relation.id, "cover": ( - None if not series.logo - else build_absolute_uri_with_port( - request, - reverse("api:vod:vodlogo-cache", args=[series.logo.id]) - ) + f"{_logo_url_prefix}{logo.id}{_logo_url_suffix}" if logo else None ), "plot": series.description or "", - "cast": series.custom_properties.get('cast', '') if series.custom_properties else "", - "director": series.custom_properties.get('director', '') if series.custom_properties else "", + "cast": custom_props.get('cast', ''), + "director": custom_props.get('director', ''), "genre": series.genre or "", - "release_date": series.custom_properties.get('release_date', str(series.year) if series.year else "") if series.custom_properties else (str(series.year) if series.year else ""), - "releaseDate": series.custom_properties.get('release_date', str(series.year) if series.year else "") if series.custom_properties else (str(series.year) if series.year else ""), + "release_date": release_date, + "releaseDate": release_date, "last_modified": str(int(relation.updated_at.timestamp())), - "rating": str(series.rating or "0"), - "rating_5based": str(round(float(series.rating or 0) / 2, 2)) if series.rating else "0", - "backdrop_path": series.custom_properties.get('backdrop_path', []) if series.custom_properties else [], - "youtube_trailer": series.custom_properties.get('youtube_trailer', '') if series.custom_properties else "", - "episode_run_time": series.custom_properties.get('episode_run_time', '') if series.custom_properties else "", - "category_id": str(relation.category.id) if relation.category else "0", - "category_ids": [int(relation.category.id)] if relation.category else [], + "rating": str(rating or "0"), + "rating_5based": str(round(float(rating or 0) / 2, 2)) if rating else "0", + "backdrop_path": custom_props.get('backdrop_path', []), + "youtube_trailer": custom_props.get('youtube_trailer', ''), + "episode_run_time": custom_props.get('episode_run_time', ''), + "category_id": str(category.id) if category else "0", + "category_ids": [category.id] if category else [], "tmdb_id": series.tmdb_id or "", "imdb_id": series.imdb_id or "", })