From 2b3d7fa1a93bb5ea979234c1393fe6940950f937 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 23 Jul 2026 23:40:24 +0200 Subject: [PATCH] fix(pwa): extract KODIPROP DRM on the web-backend /parse import path The web-backend keeps its own playlist builder for the PWA URL-import path, so the shared createPlaylistObject() DRM hook never ran there and encrypted DASH channels imported by URL reached Shaka without keys. Apply extractDrmFromRaw() in that builder too and cover the path with a regression test. Addresses Codex review on PR #1225. Co-Authored-By: Claude Fable 5 --- .../src/app/web-backend-app.spec.ts | 44 +++++++++++++++++++ apps/web-backend/src/app/web-backend-app.ts | 19 ++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/apps/web-backend/src/app/web-backend-app.spec.ts b/apps/web-backend/src/app/web-backend-app.spec.ts index e9ab7f2a8..83f66ed29 100644 --- a/apps/web-backend/src/app/web-backend-app.spec.ts +++ b/apps/web-backend/src/app/web-backend-app.spec.ts @@ -196,6 +196,50 @@ https://stream.example/news.m3u8`); ); }); + it('extracts KODIPROP ClearKey DRM on the /parse URL-import path', async () => { + const httpClient = new StubHttpClient(); + const kid = '00112233445566778899aabbccddeeff'; + const key = 'ffeeddccbbaa99887766554433221100'; + httpClient.queueResponse(`#EXTM3U +#EXTINF:-1 tvg-id="ck" group-title="DASH",Encrypted DASH +#KODIPROP:inputstream.adaptive.license_type=clearkey +#KODIPROP:inputstream.adaptive.license_key=${kid}:${key} +https://stream.example/enc.mpd +#EXTINF:-1 tvg-id="plain",Plain Channel +https://stream.example/live.m3u8`); + + await withServer( + createWebBackendApp({ + clientOrigins: ['http://localhost:4200'], + guid: () => 'fixed-id', + httpClient, + resolveHostname: resolvePublicHost, + }), + async (baseUrl) => { + const targetId = await registerProviderTarget( + baseUrl, + 'https://provider.example/drm.m3u' + ); + const response = await fetch( + `${baseUrl}/parse?targetId=${targetId}`, + { headers: { Origin: 'http://localhost:4200' } } + ); + const body = (await response.json()) as { + playlist: { items: Array> }; + }; + + expect(response.status).toBe(200); + expect(body.playlist.items).toHaveLength(2); + expect(body.playlist.items[0]['drm']).toEqual({ + licenseType: 'clearkey', + supported: true, + clearKeys: { [kid]: key }, + }); + expect(body.playlist.items[1]['drm']).toBeUndefined(); + } + ); + }); + it('allows browser preflight checks for provider target registration', async () => { await withServer( createWebBackendApp({ diff --git a/apps/web-backend/src/app/web-backend-app.ts b/apps/web-backend/src/app/web-backend-app.ts index 32bd75abe..1ccbba9c9 100644 --- a/apps/web-backend/src/app/web-backend-app.ts +++ b/apps/web-backend/src/app/web-backend-app.ts @@ -8,6 +8,7 @@ import axios from 'axios'; import epgParser from 'epg-parser'; import parser from 'iptv-playlist-parser'; import { normalizeXtreamServerUrl } from '@iptvnator/shared/interfaces'; +import { extractDrmFromRaw } from '@iptvnator/shared/m3u-utils'; export interface WebBackendHttpGetOptions { readonly headers?: Record; @@ -514,10 +515,20 @@ function createPlaylistObject(options: { count: options.playlist.items.length, playlist: { ...options.playlist, - items: options.playlist.items.map((item) => ({ - id: options.guid(), - ...item, - })), + items: options.playlist.items.map((item) => { + // Keep this builder aligned with the shared + // createPlaylistObject() in @iptvnator/shared/m3u-utils: + // KODIPROP ClearKey DRM must survive the /parse URL-import + // path too. + const drm = extractDrmFromRaw( + typeof item['raw'] === 'string' ? item['raw'] : undefined + ); + return { + id: options.guid(), + ...item, + ...(drm ? { drm } : {}), + }; + }), }, importDate: timestamp, lastUsage: timestamp,