diff --git a/background.js b/background.js index 9f78064..d9c8862 100644 --- a/background.js +++ b/background.js @@ -45,7 +45,7 @@ chrome.webRequest.onBeforeSendHeaders.addListener( ['requestHeaders', chrome.webRequest.OnSendHeadersOptions.EXTRA_HEADERS].filter(Boolean) ); -async function parseClearKey(body, sendResponse, tab_url) { +async function parseClearKey(body, sendResponse, tab_title, tab_url) { const clearkey = JSON.parse(atob(body)); const formatted_keys = clearkey["keys"].map(key => ({ @@ -66,6 +66,7 @@ async function parseClearKey(body, sendResponse, tab_url) { type: "CLEARKEY", pssh_data: pssh_data, keys: formatted_keys, + title: tab_title, url: tab_url, timestamp: Math.floor(Date.now() / 1000), manifests: manifests.has(tab_url) ? manifests.get(tab_url) : [] @@ -117,7 +118,7 @@ async function generateChallenge(body, sendResponse) { sendResponse(uint8ArrayToBase64(challenge)); } -async function parseLicense(body, sendResponse, tab_url) { +async function parseLicense(body, sendResponse, tab_title, tab_url) { const license = base64toUint8Array(body); const signed_license_message = SignedMessage.decode(license); @@ -144,6 +145,7 @@ async function parseLicense(body, sendResponse, tab_url) { type: "WIDEVINE", pssh_data: pssh, keys: keys, + title: tab_title, url: tab_url, timestamp: Math.floor(Date.now() / 1000), manifests: manifests.has(tab_url) ? manifests.get(tab_url) : [] @@ -196,7 +198,7 @@ async function generateChallengeRemote(body, sendResponse) { sendResponse(challenge_b64); } -async function parseLicenseRemote(body, sendResponse, tab_url) { +async function parseLicenseRemote(body, sendResponse, tab_title, tab_url) { const license = base64toUint8Array(body); const signed_license_message = SignedMessage.decode(license); @@ -241,6 +243,7 @@ async function parseLicenseRemote(body, sendResponse, tab_url) { type: "WIDEVINE", pssh_data: session_id.pssh, keys: keys, + title: tab_title, url: tab_url, timestamp: Math.floor(Date.now() / 1000), manifests: manifests.has(tab_url) ? manifests.get(tab_url) : [] @@ -254,6 +257,7 @@ async function parseLicenseRemote(body, sendResponse, tab_url) { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { (async () => { + const tab_title = sender.tab ? sender.tab.title : ''; const tab_url = sender.tab ? sender.tab.url : null; switch (message.type) { @@ -291,16 +295,16 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } try { - await parseClearKey(message.body, sendResponse, tab_url); + await parseClearKey(message.body, sendResponse, tab_title, tab_url); return; } catch (e) { const device_type = await SettingsManager.getSelectedDeviceType(); switch (device_type) { case "WVD": - await parseLicense(message.body, sendResponse, tab_url); + await parseLicense(message.body, sendResponse, tab_title, tab_url); break; case "REMOTE": - await parseLicenseRemote(message.body, sendResponse, tab_url); + await parseLicenseRemote(message.body, sendResponse, tab_title, tab_url); break; } return; diff --git a/panel/panel.html b/panel/panel.html index a20599a..69ee848 100644 --- a/panel/panel.html +++ b/panel/panel.html @@ -61,6 +61,7 @@ +
Keys diff --git a/panel/panel.js b/panel/panel.js index a744f31..27131d9 100644 --- a/panel/panel.js +++ b/panel/panel.js @@ -1,6 +1,6 @@ import "../protobuf.min.js"; import "../license_protocol.js"; -import {AsyncLocalStorage, base64toUint8Array, stringToUint8Array, DeviceManager, RemoteCDMManager, SettingsManager} from "../util.js"; +import {AsyncLocalStorage, base64toUint8Array, DeviceManager, RemoteCDMManager, SettingsManager} from "../util.js"; const key_container = document.getElementById('key-container'); @@ -30,10 +30,60 @@ remote_select.addEventListener('change', async function (){ } }); +async function getLogs() { + return await AsyncLocalStorage.getStorage(null); +} + const export_button = document.getElementById('export'); export_button.addEventListener('click', async function() { - const logs = await AsyncLocalStorage.getStorage(null); - SettingsManager.downloadFile(stringToUint8Array(JSON.stringify(logs)), "logs.json"); + const logs = await getLogs(); + SettingsManager.downloadFile(new Blob([JSON.stringify(logs)]), "logs.json"); +}); + +const export_kodi_m3u8_button = document.getElementById('export_kodi_m3u8'); +export_kodi_m3u8_button.addEventListener('click', async function () { + const logs = await getLogs(); + + const getKodiDrmLegacyProp = log => "#KODIPROP:inputstream.adaptive.drm_legacy=org.w3.clearkey|" + + log.keys.map(({kid, k}) => `${kid}:${k}`).join(','); + + const getKodiDrmProp = log => { + const drm = {}; + drm["org.w3.clearkey"] = {}; + drm["org.w3.clearkey"]["license"] = {}; + drm["org.w3.clearkey"]["license"]["keyids"] = {}; + log.keys.forEach(({kid, k}) => { + drm["org.w3.clearkey"]["license"]["keyids"][`${kid}`] = `${k}` + }); + return `#KODIPROP:inputstream.adaptive.drm=${JSON.stringify(drm)}`; + }; + + const m3u8 = "#EXTM3U\n" + Object.values(logs) + .sort((a, b) => a.title.localeCompare(b.title)) + .map(log => { + try { + const manifest = log.manifests.find(manifest => manifest.url); + if (manifest) { + const legacy_drm = true; + return [ + `#EXTINF:-1,${log.title}`, + '#KODIPROP:inputstream=inputstream.adaptive', + '#KODIPROP:inputstream.adaptive.common_headers=' + + Object.entries(manifest.headers).map(([key, value]) => `${key}=${value}`).join('&'), + legacy_drm ? getKodiDrmLegacyProp(log) : getKodiDrmProp(log), + manifest.url + ].join('\n'); + } else { + console.warn('Skipping ' + JSON.stringify(log) + ' as it has no url'); + } + } catch (e) { + console.error('Skipping ' + JSON.stringify(log) + ' due to ' + e); + } + return undefined; + }) + .filter(entry => entry) + .join('\n'); + SettingsManager.downloadFile(new Blob([m3u8]), 'kodi.m3u8'); }); // ====================================== @@ -145,6 +195,9 @@ async function appendLog(result) { URL: