From 21913306c4d90a53e599a283318dbdb1b128353f Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 14 May 2026 09:53:10 -0500 Subject: [PATCH] fix: Preserve base path in URL normalization for XC server and transform credentials. (Fixes #1218) --- CHANGELOG.md | 4 +--- apps/m3u/tasks.py | 9 +++++---- core/xtream_codes.py | 22 +++++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3fc6d31..dd638bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,9 +69,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Selection summary in the bulk edit modal.** The bulk edit form shows `Selection: X auto-synced, Y manual` at the top so users can see how a single set of changes will route between override rows and direct writes. - **Delete-Playlist preview.** A new `GET /api/m3u/accounts/{id}/auto-created-channels-count/` endpoint returns the count and up to 5 sample names of auto-created channels owned by the account. The Delete Playlist confirmation dialog uses this to show the user exactly how many channels will cascade away before they confirm. - **Custom `Channel.objects` manager.** A thin manager wraps the existing `with_effective_values()` helper so new code can call `Channel.objects.with_effective_values(...)` directly. The module-level helper remains the canonical implementation; existing call sites are unchanged. - -### Changed - - **Frontend unit tests added for form components.** `GroupManager`, `LiveGroupFilter`, `LoginForm`, and `Logo` now have Vitest + Testing Library test suites covering rendering, user interactions, API integration, and edge cases (144 tests total). Business logic that was extracted into utility modules (`ChannelGroupUtils`, `LiveGroupFilterUtils`, `LogoUtils`, `M3uUtils`, `M3uFilterUtils`, `M3uGroupFilterUtils`, `M3uProfileUtils`, `AutoSyncAdvancedUtils`, `AutoSyncBasicUtils`) is also exercised through these tests. — Thanks [@nick4810](https://github.com/nick4810) - **Global Network Access settings use tag-style inputs**: the IP/CIDR range fields in the Network Access settings panel now use tag-style chip inputs instead of a plain text field, making it easier to add, review, and remove individual addresses or ranges. — Thanks [@sethwv](https://github.com/sethwv) - **Auto-sync overhaul** sync and channel behavior changes for the new override system. — Thanks [@CodeBormen](https://github.com/CodeBormen) @@ -83,6 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **XC server sub-path URLs now work correctly.** When a provider serves its XC API from a sub-path (e.g. `http://server/Pluto/gb/player_api.php`), Dispatcharr was stripping the path entirely and hitting the root (`/player_api.php`) instead. `_normalize_url` now preserves sub-path components and only strips any trailing `.php` segment (covering `player_api.php`, `get.php`, `xmltv.php`, and any future endpoint without a maintained list). The same fix is applied to `get_transformed_credentials` in the M3U profile transformation path. (Fixes #1218) - **M3U filter delete confirmation showed wrong field name and had a typo.** The confirmation dialog for deleting an M3U filter read `filter.type` (always `undefined`) instead of `filter.filter_type`, leaving the "Type:" line blank, and displayed "Patter:" instead of "Pattern:". Both are corrected. — Thanks [@nick4810](https://github.com/nick4810) - **M3U form FileInput expanded the modal width on long filenames.** Uploading a local M3U file with a long name caused the `FileInput` to expand beyond the modal's layout bounds. The input now clips overflow with `textOverflow: ellipsis`. — Thanks [@nick4810](https://github.com/nick4810) - **Login loading spinner not cleared on successful login.** `setIsLoading(false)` was inside the `catch` block only, so a successful login that immediately navigated away left the loading state as `true` if the component re-mounted. Moved to a `finally` block so it always resets. — Thanks [@nick4810](https://github.com/nick4810) diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index 4599d688..65fde32f 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -2830,10 +2830,11 @@ def get_transformed_credentials(account, profile=None): transformed_username = path_parts[-3] transformed_password = path_parts[-2] - # Rebuild server URL without the username/password path - transformed_url = f"{parsed_url.scheme}://{parsed_url.netloc}" - if parsed_url.port: - transformed_url = f"{parsed_url.scheme}://{parsed_url.hostname}:{parsed_url.port}" + # Rebuild server URL: preserve any sub-path that precedes + # /live/username/password/1234.ts (path_parts[:-4]). + base_path_parts = path_parts[:-4] + base_path = ('/' + '/'.join(base_path_parts)) if base_path_parts else '' + transformed_url = f"{parsed_url.scheme}://{parsed_url.netloc}{base_path}" logger.debug(f"Extracted transformed credentials:") logger.debug(f" Server URL: {transformed_url}") diff --git a/core/xtream_codes.py b/core/xtream_codes.py index 822b37fd..195d4428 100644 --- a/core/xtream_codes.py +++ b/core/xtream_codes.py @@ -43,18 +43,22 @@ class Client: self.server_info = None def _normalize_url(self, url): - """Normalize server URL by removing trailing slashes and paths""" + """Normalize server URL: strip XC API endpoints and query params, preserve base path.""" if not url: raise ValueError("Server URL cannot be empty") - url = url.rstrip('/') - # Remove any path after domain - we'll construct proper API URLs - # Split by protocol first to preserve it - if '://' in url: - protocol, rest = url.split('://', 1) - domain = rest.split('/', 1)[0] - return f"{protocol}://{domain}" - return url + from urllib.parse import urlparse, urlunparse + + parsed = urlparse(url.strip()) + path = parsed.path.rstrip('/') + + # XC API endpoints are always .php files; legitimate base paths never are. + # Stripping the trailing segment when it ends in .php handles any pasted API URL. + last_segment = path.rsplit('/', 1)[-1] + if last_segment.endswith('.php'): + path = path[:-(len(last_segment) + 1)] if '/' in path else '' + + return urlunparse((parsed.scheme, parsed.netloc, path, '', '', '')) def _make_request(self, endpoint, params=None): """Make request with detailed error handling"""