From 7b3e2173285fb609720abec56225eb6bb937130f Mon Sep 17 00:00:00 2001 From: mltobi Date: Mon, 5 Oct 2026 20:56:01 +0200 Subject: [PATCH 1/4] Fix QR code generation: omit format kwarg for pure PNG backend, add qrcode/pypng deps --- lib/streaming_providers/base/ui/qr_generator.py | 5 +++-- requirements.txt | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/streaming_providers/base/ui/qr_generator.py b/lib/streaming_providers/base/ui/qr_generator.py index e1d9506..1194f8f 100644 --- a/lib/streaming_providers/base/ui/qr_generator.py +++ b/lib/streaming_providers/base/ui/qr_generator.py @@ -43,9 +43,10 @@ def generate_qr_code_png(data: str, size: int = 512) -> Optional[bytes]: # Generate image using pure Python PNG backend img = qr.make_image(image_factory=PyPNGImage, fill_color="black", back_color="white") - # Convert to PNG bytes + # Convert to PNG bytes. PyPNGImage.save() always writes PNG and does + # not accept a `format` kwarg (unlike PIL), so it must be omitted here. buffer = io.BytesIO() - img.save(buffer, format="PNG") + img.save(buffer) png_data = buffer.getvalue() return png_data diff --git a/requirements.txt b/requirements.txt index a4641c5..dbb4ffb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,12 @@ requests~=2.32.5 Unidecode~=1.3.7 PyYAML~=6.0.3 +# QR-code login (magenta2 SAM3 remote login). pypng provides qrcode's pure +# Python PNG backend so no Pillow/system image libs are required in the +# standalone/Docker runtime. +qrcode>=7.4 +pypng>=0.0.21 + # Optional but recommended aiohttp>=3.8.5 cryptography>=41.0.4 From 8c321d49719401c3009d16667c07a1742d101085 Mon Sep 17 00:00:00 2001 From: mltobi Date: Mon, 5 Oct 2026 20:56:38 +0200 Subject: [PATCH 2/4] Fix magenta2 remote login: render user_agent instead of missing dict key --- .../providers/magenta2/remote_login_handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/streaming_providers/providers/magenta2/remote_login_handler.py b/lib/streaming_providers/providers/magenta2/remote_login_handler.py index ecdfc63..5a073d9 100644 --- a/lib/streaming_providers/providers/magenta2/remote_login_handler.py +++ b/lib/streaming_providers/providers/magenta2/remote_login_handler.py @@ -14,7 +14,7 @@ from urllib.parse import parse_qs, unquote, urlparse from ...base.network import HTTPManager from ...base.ui import NotificationFactory, NotificationInterface, NotificationResult from ...base.utils.logger import logger -from .constants import DEFAULT_PLATFORM, DEFAULT_REQUEST_TIMEOUT, GRANT_TYPES, MAGENTA2_PLATFORMS +from .constants import DEFAULT_PLATFORM, DEFAULT_REQUEST_TIMEOUT, GRANT_TYPES, MAGENTA2_PLATFORMS, render_user_agent @dataclass @@ -73,7 +73,9 @@ class RemoteLoginHandler: # FIX: Get platform config correctly self.platform_config = MAGENTA2_PLATFORMS[DEFAULT_PLATFORM] - self.user_agent = self.platform_config["user_agent"] + # The platform config has no static "user_agent" key - the UA is + # rendered from the platform's templates via render_user_agent(). + self.user_agent = render_user_agent(DEFAULT_PLATFORM) # Get or create notifier with http_manager if notifier: From 41ed4ab2af61f5c7f9118dfd01bbf44f8d26cbe8 Mon Sep 17 00:00:00 2001 From: mltobi Date: Mon, 5 Oct 2026 21:05:44 +0200 Subject: [PATCH 3/4] Fix magenta2 recording delete: use delete-recording-for-listing/{guid} endpoint --- .../providers/magenta2/constants.py | 4 ++ .../providers/magenta2/recordings_manager.py | 60 ++++++++++++++++--- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/lib/streaming_providers/providers/magenta2/constants.py b/lib/streaming_providers/providers/magenta2/constants.py index 1ec96cd..6561f90 100644 --- a/lib/streaming_providers/providers/magenta2/constants.py +++ b/lib/streaming_providers/providers/magenta2/constants.py @@ -373,6 +373,10 @@ VOD_PREFIX_MOVIE_SH = "GN_SH" PVR_GET_RECORDINGS_PATH = "/get-recordings" PVR_RECORDINGS_PATH = "/recordings" +# recordings are deleted with HTTP DELETE on this listing-GUID-based path (returns 202 Accepted), +# NOT via DELETE /recordings/{id} (which 404s for every identifier). +PVR_DELETE_RECORDING_FOR_LISTING_PATH = "/delete-recording-for-listing" + # Default and maximum page sizes for the nPVR get-recordings endpoint PVR_DEFAULT_PAGE_LIMIT = 500 PVR_MAX_PAGE_LIMIT = 500 diff --git a/lib/streaming_providers/providers/magenta2/recordings_manager.py b/lib/streaming_providers/providers/magenta2/recordings_manager.py index 3f5b0f9..54e5621 100644 --- a/lib/streaming_providers/providers/magenta2/recordings_manager.py +++ b/lib/streaming_providers/providers/magenta2/recordings_manager.py @@ -44,6 +44,7 @@ from ...base.utils.logger import logger from .constants import ( PVR_DEFAULT_PAGE_LIMIT, + PVR_DELETE_RECORDING_FOR_LISTING_PATH, PVR_GET_RECORDINGS_PATH, PVR_MAX_PAGE_LIMIT, PVR_RECORDINGS_PATH, @@ -144,23 +145,69 @@ class RecordingsManager(PvrHttpMixin): """ Permanently delete a recording on the nPVR backend. + The recording_id received from the PVR client is the MPX playback GUID + (content_id). Deletion is performed with HTTP DELETE on + ``/delete-recording-for-listing/{listingGuid}`` (confirmed to return + 202 Accepted) - the documented ``/recordings/{id}`` path 404s for every + known identifier. + Args: - recording_id: The recording's ``id`` field (not externalRecordingId). + recording_id: The recording's content_id (MPX GUID). Raises: - RuntimeError: When the API returns a non-200/204 status or the - ``auth_headers_callback`` is not configured. - KeyError: When the recording does not exist (404). + RuntimeError: When the recording cannot be resolved or the API + returns a non-success status. """ pvr_base_url = self._get_pvr_base_url() - url = f"{pvr_base_url}{PVR_RECORDINGS_PATH}/{recording_id}" + listing_guid = self._resolve_listing_guid(recording_id) + if not listing_guid: + raise RuntimeError( + f"{self._provider}: cannot delete recording '{recording_id}' - " + "no matching listing GUID found" + ) + + url = f"{pvr_base_url}{PVR_DELETE_RECORDING_FOR_LISTING_PATH}/{listing_guid}" status_code = self._delete(url) logger.info( f"{self._provider}: Deleted recording '{recording_id}' " - f"[HTTP {status_code}]" + f"(listing '{listing_guid}') [HTTP {status_code}]" ) + def _resolve_listing_guid(self, recording_id: str) -> str: + """ + Resolve the listing GUID for the recording the PVR client refers to. + + The client sends content_id (MPX playback GUID); the delete endpoint + needs the recording's ``listing.guid``. Fetch the raw recordings and + match on the playback GUID (or the internal/external recording ids). + """ + try: + pvr_base_url = self._get_pvr_base_url() + url = f"{pvr_base_url}{PVR_GET_RECORDINGS_PATH}" + params = { + "limit": PVR_MAX_PAGE_LIMIT, + "offset": 1, + "byRecordingStatus": "|".join(PVR_RECORDING_STATUSES_ALL), + } + data = self._get(url, params) or {} + for raw in data.get("recordings", []): + if not raw: + continue + internal_id = raw.get("id", "") + external_id = raw.get("externalRecordingId", "") + mpx_guid = PvrHelpers.extract_mpx_guid(raw.get("playbackUrl")) + if recording_id in (internal_id, external_id, mpx_guid): + listing = raw.get("listing") or {} + if isinstance(listing, dict): + return listing.get("guid") or "" + except Exception as exc: + logger.warning( + f"{self._provider}: could not resolve listing guid for " + f"'{recording_id}': {exc}" + ) + return "" + def get_recording_manifest(self, recording_id: str) -> Optional[str]: """ Return the playback URL for a recording. @@ -320,7 +367,6 @@ class RecordingsManager(PvrHttpMixin): content_id=effective_content_id, name=title, provider=self._provider, - # descriptions description=description, plot=plot, plot_outline=plot_outline, From 4a58f30bf32521eaf28fa5ae2ff1ec83eeed0b80 Mon Sep 17 00:00:00 2001 From: mltobi Date: Mon, 5 Oct 2026 21:57:42 +0200 Subject: [PATCH 4/4] udpate .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b0ac3ed..53b7a8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .aider* +*.pyc +__pycache__/ \ No newline at end of file