add prettier formatting

This commit is contained in:
voldemort 2025-07-22 17:45:48 +07:00
parent a82a3fd106
commit 2828edd6b7
22 changed files with 5069 additions and 4906 deletions

View File

@ -10,6 +10,7 @@ lerna-debug.log*
node_modules node_modules
dist-ssr dist-ssr
*.local *.local
dist
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

View File

@ -0,0 +1,5 @@
dist/
node_modules/
src/assets/icons/
src/components/Functions/protobuf.min.js
src/components/Functions/license_protocol.min.js

View File

@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"useTabs": false,
"printWidth": 100
}

View File

@ -1,33 +1,30 @@
import js from '@eslint/js' import js from "@eslint/js";
import globals from 'globals' import globals from "globals";
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from "eslint-plugin-react-refresh";
export default [ export default [
{ ignores: ['dist'] }, { ignores: ["dist"] },
{ {
files: ['**/*.{js,jsx}'], files: ["**/*.{js,jsx}"],
languageOptions: { languageOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
parserOptions: { parserOptions: {
ecmaVersion: 'latest', ecmaVersion: "latest",
ecmaFeatures: { jsx: true }, ecmaFeatures: { jsx: true },
sourceType: 'module', sourceType: "module",
}, },
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
}, },
plugins: { ];
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

View File

@ -1,20 +1,20 @@
<!doctype html> <!doctype html>
<html lang="en" class="w-full h-full"> <html lang="en" class="w-full h-full">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favico.png" /> <link rel="icon" type="image/svg+xml" href="/favico.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ data.description }}"/> <meta name="description" content="{{ data.description }}" />
<meta name="keywords" content="{{ data.keywords }}"/> <meta name="keywords" content="{{ data.keywords }}" />
<meta property='og:title' content="{{ data.opengraph_title }}" /> <meta property="og:title" content="{{ data.opengraph_title }}" />
<meta property='og:description' content="{{ data.opengraph_description }}" /> <meta property="og:description" content="{{ data.opengraph_description }}" />
<meta property='og:image' content="{{ data.opengraph_image }}" /> <meta property="og:image" content="{{ data.opengraph_image }}" />
<meta property='og:url' content="{{ data.opengraph_url }}" /> <meta property="og:url" content="{{ data.opengraph_url }}" />
<meta property='og:locale' content='en_US' /> <meta property="og:locale" content="en_US" />
<title>{{ data.tab_title }}</title> <title>{{ data.tab_title }}</title>
</head> </head>
<body class="w-full h-full"> <body class="w-full h-full">
<div id="root" class="w-full h-full"></div> <div id="root" class="w-full h-full"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.jsx"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,33 @@
{ {
"name": "cdrm-frontend", "name": "cdrm-frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.11",
"axios": "^1.9.0", "axios": "^1.10.0",
"react": "^19.0.0", "react": "^19.1.0",
"react-dom": "^19.0.0", "react-dom": "^19.1.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-router-dom": "^7.5.2", "react-router-dom": "^7.7.0",
"shaka-player": "^4.14.9", "shaka-player": "^4.15.8",
"tailwindcss": "^4.1.4" "tailwindcss": "^4.1.11"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.22.0", "@eslint/js": "^9.31.0",
"@types/react": "^19.0.10", "@types/react": "^19.1.8",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.7.0",
"eslint": "^9.22.0", "eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0", "globals": "^16.3.0",
"vite": "^6.3.1" "vite": "^7.0.5"
} }
} }

View File

@ -10,34 +10,40 @@ import Account from "./components/Pages/Account";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
function App() { function App() {
const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
return ( return (
<div id="appcontainer" className="flex flex-row w-full h-full bg-black"> <div id="appcontainer" className="flex flex-row w-full h-full bg-black">
{/* The SideMenu should be visible when isMenuOpen is true */} {/* The SideMenu should be visible when isMenuOpen is true */}
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} /> <SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
<div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0"> <div
<NavBar /> id="navbarcontainer"
</div> className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0"
>
<NavBar />
</div>
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow"> <div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
<div id="navbarmaincontainer" className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10"> <div
<NavBarMain setIsMenuOpen={setIsMenuOpen} /> id="navbarmaincontainer"
className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10"
>
<NavBarMain setIsMenuOpen={setIsMenuOpen} />
</div>
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} />
<Route path="/account" element={<Account />} />
</Routes>
</div>
</div>
</div> </div>
);
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} />
<Route path="/account" element={<Account />} />
</Routes>
</div>
</div>
</div>
);
} }
export default App; export default App;

View File

@ -4,11 +4,11 @@ import "./license_protocol.min.js";
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol; const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
function uint8ArrayToBase64(uint8Array) { function uint8ArrayToBase64(uint8Array) {
const binaryString = Array.from(uint8Array) const binaryString = Array.from(uint8Array)
.map(b => String.fromCharCode(b)) .map((b) => String.fromCharCode(b))
.join(''); .join("");
return btoa(binaryString); return btoa(binaryString);
} }
function parseFetch(fetchString) { function parseFetch(fetchString) {
@ -17,10 +17,13 @@ function parseFetch(fetchString) {
// Use a more lenient regex to capture the fetch pattern (including complex bodies) // Use a more lenient regex to capture the fetch pattern (including complex bodies)
const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
const lines = fetchString.split('\n').map(line => line.trim()).filter(Boolean); const lines = fetchString
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const result = { const result = {
method: 'UNDEFINED', method: "UNDEFINED",
url: '', url: "",
headers: {}, headers: {},
body: null, body: null,
}; };
@ -47,9 +50,12 @@ function parseFetch(fetchString) {
return result; return result;
} }
const WIDEVINE_SYSTEM_ID = new Uint8Array([
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]); 0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed,
const PLAYREADY_SYSTEM_ID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]); ]);
const PLAYREADY_SYSTEM_ID = new Uint8Array([
0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
]);
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]); const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
function intToUint8Array(num, endian) { function intToUint8Array(num, endian) {
@ -75,44 +81,44 @@ function psshDataToPsshBoxB64(pssh_data, system_id) {
...new Uint8Array(4), ...new Uint8Array(4),
...system_id, ...system_id,
...intToUint8Array(dataLength, false), ...intToUint8Array(dataLength, false),
...pssh_data ...pssh_data,
]); ]);
return uint8ArrayToBase64(pssh); return uint8ArrayToBase64(pssh);
} }
function wrmHeaderToPlayReadyHeader(wrm_header){ function wrmHeaderToPlayReadyHeader(wrm_header) {
const playready_object = new Uint8Array([ const playready_object = new Uint8Array([
...shortToUint8Array(1, true), ...shortToUint8Array(1, true),
...shortToUint8Array(wrm_header.length, true), ...shortToUint8Array(wrm_header.length, true),
...wrm_header ...wrm_header,
]); ]);
return new Uint8Array([ return new Uint8Array([
...intToUint8Array(playready_object.length + 2 + 4, true), ...intToUint8Array(playready_object.length + 2 + 4, true),
...shortToUint8Array(1, true), ...shortToUint8Array(1, true),
...playready_object ...playready_object,
]); ]);
} }
function encodeUtf16LE(str) { function encodeUtf16LE(str) {
const buffer = new Uint8Array(str.length * 2); const buffer = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i); const code = str.charCodeAt(i);
buffer[i * 2] = code & 0xff; buffer[i * 2] = code & 0xff;
buffer[i * 2 + 1] = code >> 8; buffer[i * 2 + 1] = code >> 8;
} }
return buffer; return buffer;
} }
function stringToUint8Array(string) { function stringToUint8Array(string) {
return Uint8Array.from(string.split("").map(x => x.charCodeAt())); return Uint8Array.from(string.split("").map((x) => x.charCodeAt()));
} }
export async function readTextFromClipboard() { export async function readTextFromClipboard() {
try { try {
// Request text from the clipboard // Request text from the clipboard
const clipboardText = await navigator.clipboard.readText(); const clipboardText = await navigator.clipboard.readText();
const result = parseFetch(clipboardText); const result = parseFetch(clipboardText);
let pssh_data_string; let pssh_data_string;
@ -136,11 +142,15 @@ export async function readTextFromClipboard() {
license_request = LicenseRequest.decode(signed_message.msg); license_request = LicenseRequest.decode(signed_message.msg);
} catch (decodeError) { } catch (decodeError) {
// If error occurs during decoding, return an empty pssh // If error occurs during decoding, return an empty pssh
console.error('Decoding failed, returning empty pssh', decodeError); console.error("Decoding failed, returning empty pssh", decodeError);
pssh_data_string = ''; // Empty pssh if decoding fails pssh_data_string = ""; // Empty pssh if decoding fails
} }
if (license_request && license_request.contentId && license_request.contentId.widevinePsshData) { if (
license_request &&
license_request.contentId &&
license_request.contentId.widevinePsshData
) {
const pssh_data = license_request.contentId.widevinePsshData.psshData[0]; const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID); pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
} }
@ -160,14 +170,12 @@ export async function readTextFromClipboard() {
document.getElementById("pssh").value = pssh_data_string; document.getElementById("pssh").value = pssh_data_string;
document.getElementById("data").value = payload_string; document.getElementById("data").value = payload_string;
} catch (error) { } catch (error) {
console.error('Failed to read clipboard contents:', error); console.error("Failed to read clipboard contents:", error);
} }
} }
// Helper function to check if the data is binary // Helper function to check if the data is binary
function isBinary(uint8Array) { function isBinary(uint8Array) {
// Check for non-text (non-ASCII) bytes (basic heuristic) // Check for non-text (non-ASCII) bytes (basic heuristic)
return uint8Array.some(byte => byte > 127); // Non-ASCII byte indicates binary return uint8Array.some((byte) => byte > 127); // Non-ASCII byte indicates binary
} }

View File

@ -1,26 +1,26 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { NavLink } from 'react-router-dom'; import { NavLink } from "react-router-dom";
import homeIcon from '../assets/icons/home.svg'; import homeIcon from "../assets/icons/home.svg";
import cacheIcon from '../assets/icons/cache.svg'; import cacheIcon from "../assets/icons/cache.svg";
import apiIcon from '../assets/icons/api.svg'; import apiIcon from "../assets/icons/api.svg";
import testPlayerIcon from '../assets/icons/testplayer.svg'; import testPlayerIcon from "../assets/icons/testplayer.svg";
import accountIcon from '../assets/icons/account.svg'; import accountIcon from "../assets/icons/account.svg";
import discordIcon from '../assets/icons/discord.svg'; import discordIcon from "../assets/icons/discord.svg";
import telegramIcon from '../assets/icons/telegram.svg'; import telegramIcon from "../assets/icons/telegram.svg";
import giteaIcon from '../assets/icons/gitea.svg'; import giteaIcon from "../assets/icons/gitea.svg";
function NavBar() { function NavBar() {
const [externalLinks, setExternalLinks] = useState({ const [externalLinks, setExternalLinks] = useState({
discord: '#', discord: "#",
telegram: '#', telegram: "#",
gitea: '#', gitea: "#",
}); });
useEffect(() => { useEffect(() => {
fetch('/api/links') fetch("/api/links")
.then(response => response.json()) .then((response) => response.json())
.then(data => setExternalLinks(data)) .then((data) => setExternalLinks(data))
.catch(error => console.error('Error fetching links:', error)); .catch((error) => console.error("Error fetching links:", error));
}, []); }, []);
return ( return (
@ -40,8 +40,8 @@ function NavBar() {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `flex flex-row p-3 border-l-3 ${
isActive isActive
? 'border-l-sky-500/50 bg-black/50' ? "border-l-sky-500/50 bg-black/50"
: 'hover:border-l-sky-500/50 hover:bg-white/5' : "hover:border-l-sky-500/50 hover:bg-white/5"
}` }`
} }
> >
@ -58,8 +58,8 @@ function NavBar() {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `flex flex-row p-3 border-l-3 ${
isActive isActive
? 'border-l-emerald-500/50 bg-black/50' ? "border-l-emerald-500/50 bg-black/50"
: 'hover:border-l-emerald-500/50 hover:bg-white/5' : "hover:border-l-emerald-500/50 hover:bg-white/5"
}` }`
} }
> >
@ -76,8 +76,8 @@ function NavBar() {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `flex flex-row p-3 border-l-3 ${
isActive isActive
? 'border-l-indigo-500/50 bg-black/50' ? "border-l-indigo-500/50 bg-black/50"
: 'hover:border-l-indigo-500/50 hover:bg-white/5' : "hover:border-l-indigo-500/50 hover:bg-white/5"
}` }`
} }
> >
@ -94,13 +94,17 @@ function NavBar() {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `flex flex-row p-3 border-l-3 ${
isActive isActive
? 'border-l-rose-500/50 bg-black/50' ? "border-l-rose-500/50 bg-black/50"
: 'hover:border-l-rose-500/50 hover:bg-white/5' : "hover:border-l-rose-500/50 hover:bg-white/5"
}` }`
} }
> >
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer"> <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
<img src={testPlayerIcon} alt="Test Player" className="w-1/2 cursor-pointer" /> <img
src={testPlayerIcon}
alt="Test Player"
className="w-1/2 cursor-pointer"
/>
</button> </button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start"> <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
Test Player Test Player
@ -114,8 +118,8 @@ function NavBar() {
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `flex flex-row p-3 border-l-3 ${
isActive isActive
? 'border-l-yellow-500/50 bg-black/50' ? "border-l-yellow-500/50 bg-black/50"
: 'hover:border-l-yellow-500/50 hover:bg-white/5' : "hover:border-l-yellow-500/50 hover:bg-white/5"
}` }`
} }
> >
@ -137,7 +141,11 @@ function NavBar() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group" className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group"
> >
<img src={discordIcon} alt="Discord" className="w-1/2 group-hover:animate-bounce" /> <img
src={discordIcon}
alt="Discord"
className="w-1/2 group-hover:animate-bounce"
/>
</a> </a>
<a <a
href={externalLinks.telegram} href={externalLinks.telegram}
@ -145,7 +153,11 @@ function NavBar() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group" className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group"
> >
<img src={telegramIcon} alt="Telegram" className="w-1/2 group-hover:animate-bounce" /> <img
src={telegramIcon}
alt="Telegram"
className="w-1/2 group-hover:animate-bounce"
/>
</a> </a>
<a <a
href={externalLinks.gitea} href={externalLinks.gitea}

View File

@ -2,21 +2,21 @@ import { useState } from "react";
import hamburgerIcon from "../assets/icons/hamburger.svg"; import hamburgerIcon from "../assets/icons/hamburger.svg";
function NavBarMain({ setIsMenuOpen }) { function NavBarMain({ setIsMenuOpen }) {
const handleMenuToggle = () => { const handleMenuToggle = () => {
setIsMenuOpen((prevState) => !prevState); // Toggle the menu state setIsMenuOpen((prevState) => !prevState); // Toggle the menu state
}; };
return ( return (
<div className="flex flex-row w-full h-full bg-white/1"> <div className="flex flex-row w-full h-full bg-white/1">
<button className="w-24 p-4" onClick={handleMenuToggle}> <button className="w-24 p-4" onClick={handleMenuToggle}>
<img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" /> <img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" />
</button> </button>
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4"> <p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
CDRM-Project CDRM-Project
</p> </p>
<div className="w-24 p-4"></div> <div className="w-24 p-4"></div>
</div> </div>
); );
} }
export default NavBarMain; export default NavBarMain;

View File

@ -1,73 +1,73 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { Helmet } from 'react-helmet'; // Import Helmet import { Helmet } from "react-helmet"; // Import Helmet
const { protocol, hostname, port } = window.location; const { protocol, hostname, port } = window.location;
let fullHost = `${protocol}//${hostname}`; let fullHost = `${protocol}//${hostname}`;
if ( if (
(protocol === 'http:' && port !== '80') || (protocol === "http:" && port !== "80") ||
(protocol === 'https:' && port !== '443' && port !== '') (protocol === "https:" && port !== "443" && port !== "")
) { ) {
fullHost += `:${port}`; fullHost += `:${port}`;
} }
function API() { function API() {
const [deviceInfo, setDeviceInfo] = useState({ const [deviceInfo, setDeviceInfo] = useState({
device_type: '', device_type: "",
system_id: '', system_id: "",
security_level: '', security_level: "",
host: '', host: "",
secret: '', secret: "",
device_name: '' device_name: "",
}); });
const [prDeviceInfo, setPrDeviceInfo] = useState({ const [prDeviceInfo, setPrDeviceInfo] = useState({
security_level: '', security_level: "",
host: '', host: "",
secret: '', secret: "",
device_name: '' device_name: "",
}); });
useEffect(() => { useEffect(() => {
// Fetch Widevine info // Fetch Widevine info
fetch('/remotecdm/widevine/deviceinfo') fetch("/remotecdm/widevine/deviceinfo")
.then(response => response.json()) .then((response) => response.json())
.then(data => { .then((data) => {
setDeviceInfo({ setDeviceInfo({
device_type: data.device_type, device_type: data.device_type,
system_id: data.system_id, system_id: data.system_id,
security_level: data.security_level, security_level: data.security_level,
host: data.host, host: data.host,
secret: data.secret, secret: data.secret,
device_name: data.device_name device_name: data.device_name,
}); });
}) })
.catch(error => console.error('Error fetching Widevine info:', error)); .catch((error) => console.error("Error fetching Widevine info:", error));
// Fetch PlayReady info // Fetch PlayReady info
fetch('/remotecdm/playready/deviceinfo') fetch("/remotecdm/playready/deviceinfo")
.then(response => response.json()) .then((response) => response.json())
.then(data => { .then((data) => {
setPrDeviceInfo({ setPrDeviceInfo({
security_level: data.security_level, security_level: data.security_level,
host: data.host, host: data.host,
secret: data.secret, secret: data.secret,
device_name: data.device_name device_name: data.device_name,
}); });
}) })
.catch(error => console.error('Error fetching PlayReady info:', error)); .catch((error) => console.error("Error fetching PlayReady info:", error));
}, []); }, []);
return ( return (
<div className="flex flex-col w-full overflow-y-auto p-4 text-white"> <div className="flex flex-col w-full overflow-y-auto p-4 text-white">
<Helmet> <Helmet>
<title>API</title> <title>API</title>
</Helmet> </Helmet>
<details open className='w-full list-none'> <details open className="w-full list-none">
<summary className='text-2xl'>Sending a decryption request</summary> <summary className="text-2xl">Sending a decryption request</summary>
<div className='mt-5 p-5 rounded-lg border-2 border-indigo-500/50'> <div className="mt-5 p-5 rounded-lg border-2 border-indigo-500/50">
<pre className='rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto'> <pre className="rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto">
{`import requests {`import requests
print(requests.post( print(requests.post(
url='${fullHost}/api/decrypt', url='${fullHost}/api/decrypt',
@ -84,14 +84,14 @@ print(requests.post(
}) })
} }
).json()['message'])`} ).json()['message'])`}
</pre> </pre>
</div> </div>
</details> </details>
<details open className='w-full list-none mt-5'> <details open className="w-full list-none mt-5">
<summary className='text-2xl'>Sending a search request</summary> <summary className="text-2xl">Sending a search request</summary>
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg'> <div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg">
<pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5"> <pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
{`import requests {`import requests
print(requests.post( print(requests.post(
url='${fullHost}/api/cache/search', url='${fullHost}/api/cache/search',
@ -99,36 +99,40 @@ print(requests.post(
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==' 'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
} }
).json())`} ).json())`}
</pre> </pre>
</div> </div>
</details> </details>
<details open className='w-full list-none mt-5'> <details open className="w-full list-none mt-5">
<summary className='text-2xl'>PyWidevine RemoteCDM info</summary> <summary className="text-2xl">PyWidevine RemoteCDM info</summary>
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'> <div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
<p> <p>
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br /> <strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
<strong>System ID:</strong> {deviceInfo.system_id}<br /> <strong>System ID:</strong> {deviceInfo.system_id}
<strong>Security Level:</strong> {deviceInfo.security_level}<br /> <br />
<strong>Host:</strong> {fullHost}/remotecdm/widevine<br /> <strong>Security Level:</strong> {deviceInfo.security_level}
<strong>Secret:</strong> '{deviceInfo.secret}'<br /> <br />
<strong>Device Name:</strong> {deviceInfo.device_name} <strong>Host:</strong> {fullHost}/remotecdm/widevine
</p> <br />
</div> <strong>Secret:</strong> '{deviceInfo.secret}'<br />
</details> <strong>Device Name:</strong> {deviceInfo.device_name}
<details open className='w-full list-none mt-5'> </p>
<summary className='text-2xl'>PyPlayready RemoteCDM info</summary> </div>
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'> </details>
<p> <details open className="w-full list-none mt-5">
<strong>Security Level:</strong> {prDeviceInfo.security_level}<br /> <summary className="text-2xl">PyPlayready RemoteCDM info</summary>
<strong>Host:</strong> {fullHost}/remotecdm/playready<br /> <div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
<strong>Secret:</strong> '{prDeviceInfo.secret}'<br /> <p>
<strong>Device Name:</strong> {prDeviceInfo.device_name} <strong>Security Level:</strong> {prDeviceInfo.security_level}
</p> <br />
</div> <strong>Host:</strong> {fullHost}/remotecdm/playready
</details> <br />
<strong>Secret:</strong> '{prDeviceInfo.secret}'<br />
</div> <strong>Device Name:</strong> {prDeviceInfo.device_name}
); </p>
</div>
</details>
</div>
);
} }
export default API; export default API;

View File

@ -3,36 +3,36 @@ import Register from "./Register";
import MyAccount from "./MyAccount"; // <-- Import the MyAccount component import MyAccount from "./MyAccount"; // <-- Import the MyAccount component
function Account() { function Account() {
const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state
useEffect(() => { useEffect(() => {
fetch('/login/status', { fetch("/login/status", {
method: 'POST', method: "POST",
credentials: 'include', // Sends cookies with request credentials: "include", // Sends cookies with request
}) })
.then(res => res.json()) .then((res) => res.json())
.then(data => { .then((data) => {
if (data.message === 'True') { if (data.message === "True") {
setIsLoggedIn(true); setIsLoggedIn(true);
} else { } else {
setIsLoggedIn(false); setIsLoggedIn(false);
} }
}) })
.catch(err => { .catch((err) => {
console.error("Error checking login status:", err); console.error("Error checking login status:", err);
setIsLoggedIn(false); // Assume not logged in on error setIsLoggedIn(false); // Assume not logged in on error
}); });
}, []); }, []);
if (isLoggedIn === null) { if (isLoggedIn === null) {
return <div>Loading...</div>; // Optional loading UI return <div>Loading...</div>; // Optional loading UI
} }
return ( return (
<div id="accountpage" className="w-full h-full flex"> <div id="accountpage" className="w-full h-full flex">
{isLoggedIn ? <MyAccount /> : <Register />} {isLoggedIn ? <MyAccount /> : <Register />}
</div> </div>
); );
} }
export default Account; export default Account;

View File

@ -1,8 +1,8 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from "react";
import { Helmet } from 'react-helmet'; // Import Helmet import { Helmet } from "react-helmet"; // Import Helmet
function Cache() { function Cache() {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState("");
const [cacheData, setCacheData] = useState([]); const [cacheData, setCacheData] = useState([]);
const [keyCount, setKeyCount] = useState(0); // New state to store the key count const [keyCount, setKeyCount] = useState(0); // New state to store the key count
const debounceTimeout = useRef(null); const debounceTimeout = useRef(null);
@ -11,11 +11,11 @@ function Cache() {
useEffect(() => { useEffect(() => {
const fetchKeyCount = async () => { const fetchKeyCount = async () => {
try { try {
const response = await fetch('/api/cache/keycount'); const response = await fetch("/api/cache/keycount");
const data = await response.json(); const data = await response.json();
setKeyCount(data.count); // Update key count setKeyCount(data.count); // Update key count
} catch (error) { } catch (error) {
console.error('Error fetching key count:', error); console.error("Error fetching key count:", error);
} }
}; };
@ -25,7 +25,7 @@ function Cache() {
const handleInputChange = (event) => { const handleInputChange = (event) => {
const query = event.target.value; const query = event.target.value;
setSearchQuery(query); // Update the search query setSearchQuery(query); // Update the search query
// Clear the previous timeout // Clear the previous timeout
if (debounceTimeout.current) { if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current); clearTimeout(debounceTimeout.current);
@ -33,7 +33,7 @@ function Cache() {
// Set a new timeout to send the API call after 1 second of no typing // Set a new timeout to send the API call after 1 second of no typing
debounceTimeout.current = setTimeout(() => { debounceTimeout.current = setTimeout(() => {
if (query.trim() !== '') { if (query.trim() !== "") {
sendApiCall(query); // Only call the API if the query is not empty sendApiCall(query); // Only call the API if the query is not empty
} else { } else {
setCacheData([]); // Clear results if query is empty setCacheData([]); // Clear results if query is empty
@ -42,14 +42,14 @@ function Cache() {
}; };
const sendApiCall = (text) => { const sendApiCall = (text) => {
fetch('/api/cache/search', { fetch("/api/cache/search", {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input: text }), body: JSON.stringify({ input: text }),
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => setCacheData(data)) // Update cache data with the results .then((data) => setCacheData(data)) // Update cache data with the results
.catch((error) => console.error('Error:', error)); .catch((error) => console.error("Error:", error));
}; };
return ( return (

View File

@ -1,248 +1,272 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from "react";
import { readTextFromClipboard } from '../Functions/ParseChallenge'; import { readTextFromClipboard } from "../Functions/ParseChallenge";
import { Helmet } from 'react-helmet'; // Import Helmet import { Helmet } from "react-helmet"; // Import Helmet
function HomePage() { function HomePage() {
const [pssh, setPssh] = useState(''); const [pssh, setPssh] = useState("");
const [licurl, setLicurl] = useState(''); const [licurl, setLicurl] = useState("");
const [proxy, setProxy] = useState(''); const [proxy, setProxy] = useState("");
const [headers, setHeaders] = useState(''); const [headers, setHeaders] = useState("");
const [cookies, setCookies] = useState(''); const [cookies, setCookies] = useState("");
const [data, setData] = useState(''); const [data, setData] = useState("");
const [message, setMessage] = useState(''); const [message, setMessage] = useState("");
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [devices, setDevices] = useState([]); const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState('default'); const [selectedDevice, setSelectedDevice] = useState("default");
const bottomRef = useRef(null); const bottomRef = useRef(null);
const messageRef = useRef(null); // Reference to result container const messageRef = useRef(null); // Reference to result container
const handleReset = () => { const handleReset = () => {
if (isVisible) { if (isVisible) {
setIsVisible(false); setIsVisible(false);
}
setPssh('');
setLicurl('');
setProxy('');
setHeaders('');
setCookies('');
setData('');
};
const handleSubmitButton = (event) => {
event.preventDefault();
fetch('/api/decrypt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pssh: pssh,
licurl: licurl,
proxy: proxy,
headers: headers,
cookies: cookies,
data: data,
device: selectedDevice, // Include selected device in the request
}),
})
.then(response => response.json())
.then(data => {
const resultMessage = data['message'].replace(/\n/g, '<br />');
setMessage(resultMessage);
setIsVisible(true);
})
.catch((error) => {
console.error('Error during decryption request:', error);
setMessage('Error: Unable to process request.');
setIsVisible(true);
});
};
const handleCopy = (event) => {
event.preventDefault();
if (messageRef.current) {
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
navigator.clipboard.writeText(textToCopy).catch(err => {
alert('Failed to copy!');
console.error(err);
});
}
};
const handleFetchPaste = () => {
event.preventDefault();
readTextFromClipboard().then(() => {
setPssh(document.getElementById("pssh").value);
setLicurl(document.getElementById("licurl").value);
setHeaders(document.getElementById("headers").value);
setData(document.getElementById("data").value);
}).catch(err => {
alert('Failed to paste from fetch!');
});
};
useEffect(() => {
if (isVisible && bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [message, isVisible]);
useEffect(() => {
fetch('/login/status', {
method: 'POST',
})
.then(res => res.json())
.then(statusData => {
if (statusData.message === 'True') {
return fetch('/userinfo', { method: 'POST' });
} else {
throw new Error('Not logged in');
} }
}) setPssh("");
.then(res => res.json()) setLicurl("");
.then(deviceData => { setProxy("");
const combinedDevices = [ setHeaders("");
...deviceData.Widevine_Devices, setCookies("");
...deviceData.Playready_Devices, setData("");
]; };
// Add default devices if logged in const handleSubmitButton = (event) => {
const allDevices = [ event.preventDefault();
"CDRM-Project Public Widevine CDM",
"CDRM-Project Public PlayReady CDM",
...combinedDevices,
];
// Set devices and select a device if logged in fetch("/api/decrypt", {
setDevices(allDevices.length > 0 ? allDevices : []); method: "POST",
setSelectedDevice(allDevices.length > 0 ? allDevices[0] : 'default'); headers: {
}) "Content-Type": "application/json",
.catch(() => { },
// User isn't logged in, set default device to 'default' body: JSON.stringify({
setDevices([]); // Don't display devices list pssh: pssh,
setSelectedDevice('default'); licurl: licurl,
}); proxy: proxy,
}, []); headers: headers,
cookies: cookies,
data: data,
device: selectedDevice, // Include selected device in the request
}),
})
.then((response) => response.json())
.then((data) => {
const resultMessage = data["message"].replace(/\n/g, "<br />");
setMessage(resultMessage);
setIsVisible(true);
})
.catch((error) => {
console.error("Error during decryption request:", error);
setMessage("Error: Unable to process request.");
setIsVisible(true);
});
};
return ( const handleCopy = (event) => {
<> event.preventDefault();
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full"> if (messageRef.current) {
<Helmet> const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
<title>CDRM-Project</title> navigator.clipboard.writeText(textToCopy).catch((err) => {
</Helmet> alert("Failed to copy!");
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto"> console.error(err);
<label htmlFor="pssh" className="text-white w-8/10 self-center">PSSH: </label> });
<input }
type="text" };
id="pssh"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={pssh}
onChange={(e) => setPssh(e.target.value)}
/>
<label htmlFor="licurl" className="text-white w-8/10 self-center">License URL: </label>
<input
type="text"
id="licurl"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={licurl}
onChange={(e) => setLicurl(e.target.value)}
/>
<label htmlFor="proxy" className="text-white w-8/10 self-center">Proxy: </label>
<input
type="text"
id="proxy"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={proxy}
onChange={(e) => setProxy(e.target.value)}
/>
<label htmlFor="headers" className="text-white w-8/10 self-center">Headers: </label>
<textarea
id="headers"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={headers}
onChange={(e) => setHeaders(e.target.value)}
/>
<label htmlFor="cookies" className="text-white w-8/10 self-center">Cookies: </label>
<textarea
id="cookies"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={cookies}
onChange={(e) => setCookies(e.target.value)}
/>
<label htmlFor="data" className="text-white w-8/10 self-center">Data: </label>
<textarea
id="data"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={data}
onChange={(e) => setData(e.target.value)}
/>
{/* Device Selection Dropdown, only show if logged in */} const handleFetchPaste = () => {
{devices.length > 0 && ( event.preventDefault();
<> readTextFromClipboard()
<label htmlFor="device" className="text-white w-8/10 self-center">Select Device:</label> .then(() => {
<select setPssh(document.getElementById("pssh").value);
id="device" setLicurl(document.getElementById("licurl").value);
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1" setHeaders(document.getElementById("headers").value);
value={selectedDevice} setData(document.getElementById("data").value);
onChange={(e) => setSelectedDevice(e.target.value)} })
> .catch((err) => {
{devices.map((device, index) => ( alert("Failed to paste from fetch!");
<option key={index} value={device}>{device}</option> });
))} };
</select>
</>
)}
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch"> useEffect(() => {
<button if (isVisible && bottomRef.current) {
type="button" bottomRef.current.scrollIntoView({ behavior: "smooth" });
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2" }
onClick={handleSubmitButton} }, [message, isVisible]);
>
Submit
</button>
<button onClick={handleFetchPaste} className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0">
Paste from fetch
</button>
<button
type="button"
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
onClick={handleReset}
>
Reset
</button>
</div>
</form>
</div>
{isVisible && ( useEffect(() => {
<div id="main_content" className="flex-col w-full h-full p-10 items-center justify-center self-center"> fetch("/login/status", {
<div className="flex flex-col w-full h-full overflow-y-auto items-center"> method: "POST",
<div className='w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5'> })
<p className="w-full border-b-2 border-white/75 pb-2">Results:</p> .then((res) => res.json())
<p .then((statusData) => {
className="w-full grow pt-10 break-words overflow-y-auto" if (statusData.message === "True") {
ref={messageRef} return fetch("/userinfo", { method: "POST" });
dangerouslySetInnerHTML={{ __html: message }} } else {
/> throw new Error("Not logged in");
<div ref={bottomRef} /> }
})
.then((res) => res.json())
.then((deviceData) => {
const combinedDevices = [
...deviceData.Widevine_Devices,
...deviceData.Playready_Devices,
];
// Add default devices if logged in
const allDevices = [
"CDRM-Project Public Widevine CDM",
"CDRM-Project Public PlayReady CDM",
...combinedDevices,
];
// Set devices and select a device if logged in
setDevices(allDevices.length > 0 ? allDevices : []);
setSelectedDevice(allDevices.length > 0 ? allDevices[0] : "default");
})
.catch(() => {
// User isn't logged in, set default device to 'default'
setDevices([]); // Don't display devices list
setSelectedDevice("default");
});
}, []);
return (
<>
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
<Helmet>
<title>CDRM-Project</title>
</Helmet>
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
<label htmlFor="pssh" className="text-white w-8/10 self-center">
PSSH:{" "}
</label>
<input
type="text"
id="pssh"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={pssh}
onChange={(e) => setPssh(e.target.value)}
/>
<label htmlFor="licurl" className="text-white w-8/10 self-center">
License URL:{" "}
</label>
<input
type="text"
id="licurl"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={licurl}
onChange={(e) => setLicurl(e.target.value)}
/>
<label htmlFor="proxy" className="text-white w-8/10 self-center">
Proxy:{" "}
</label>
<input
type="text"
id="proxy"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={proxy}
onChange={(e) => setProxy(e.target.value)}
/>
<label htmlFor="headers" className="text-white w-8/10 self-center">
Headers:{" "}
</label>
<textarea
id="headers"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={headers}
onChange={(e) => setHeaders(e.target.value)}
/>
<label htmlFor="cookies" className="text-white w-8/10 self-center">
Cookies:{" "}
</label>
<textarea
id="cookies"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={cookies}
onChange={(e) => setCookies(e.target.value)}
/>
<label htmlFor="data" className="text-white w-8/10 self-center">
Data:{" "}
</label>
<textarea
id="data"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={data}
onChange={(e) => setData(e.target.value)}
/>
{/* Device Selection Dropdown, only show if logged in */}
{devices.length > 0 && (
<>
<label htmlFor="device" className="text-white w-8/10 self-center">
Select Device:
</label>
<select
id="device"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
value={selectedDevice}
onChange={(e) => setSelectedDevice(e.target.value)}
>
{devices.map((device, index) => (
<option key={index} value={device}>
{device}
</option>
))}
</select>
</>
)}
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
type="button"
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleSubmitButton}
>
Submit
</button>
<button
onClick={handleFetchPaste}
className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
>
Paste from fetch
</button>
<button
type="button"
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
onClick={handleReset}
>
Reset
</button>
</div>
</form>
</div> </div>
</div>
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch"> {isVisible && (
<button <div
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2" id="main_content"
onClick={handleCopy} className="flex-col w-full h-full p-10 items-center justify-center self-center"
> >
Copy Results <div className="flex flex-col w-full h-full overflow-y-auto items-center">
</button> <div className="w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5">
</div> <p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
</div> <p
)} className="w-full grow pt-10 break-words overflow-y-auto"
</> ref={messageRef}
); dangerouslySetInnerHTML={{ __html: message }}
/>
<div ref={bottomRef} />
</div>
</div>
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleCopy}
>
Copy Results
</button>
</div>
</div>
)}
</>
);
} }
export default HomePage; export default HomePage;

View File

@ -1,262 +1,285 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import axios from 'axios'; import axios from "axios";
function MyAccount() { function MyAccount() {
const [wvList, setWvList] = useState([]); const [wvList, setWvList] = useState([]);
const [prList, setPrList] = useState([]); const [prList, setPrList] = useState([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [username, setUsername] = useState(''); const [username, setUsername] = useState("");
const [apiKey, setApiKey] = useState(''); const [apiKey, setApiKey] = useState("");
const [password, setPassword] = useState(''); const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState(''); const [passwordError, setPasswordError] = useState("");
const [newApiKey, setNewApiKey] = useState(''); const [newApiKey, setNewApiKey] = useState("");
const [apiKeyError, setApiKeyError] = useState(''); const [apiKeyError, setApiKeyError] = useState("");
// Fetch user info // Fetch user info
const fetchUserInfo = async () => { const fetchUserInfo = async () => {
try { try {
const response = await axios.post('/userinfo'); const response = await axios.post("/userinfo");
setWvList(response.data.Widevine_Devices || []); setWvList(response.data.Widevine_Devices || []);
setPrList(response.data.Playready_Devices || []); setPrList(response.data.Playready_Devices || []);
setUsername(response.data.Styled_Username || ''); setUsername(response.data.Styled_Username || "");
setApiKey(response.data.API_Key || ''); setApiKey(response.data.API_Key || "");
} catch (err) { } catch (err) {
console.error('Failed to fetch user info', err); console.error("Failed to fetch user info", err);
} }
}; };
useEffect(() => { useEffect(() => {
fetchUserInfo(); fetchUserInfo();
}, []); }, []);
// Handle file upload // Handle file upload
const handleUpload = async (event, cdmType) => { const handleUpload = async (event, cdmType) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
const extension = file.name.split('.').pop(); const extension = file.name.split(".").pop();
if ((cdmType === 'PR' && extension !== 'prd') || (cdmType === 'WV' && extension !== 'wvd')) { if (
alert(`Please upload a .${cdmType === 'PR' ? 'prd' : 'wvd'} file.`); (cdmType === "PR" && extension !== "prd") ||
return; (cdmType === "WV" && extension !== "wvd")
} ) {
alert(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
return;
}
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append("file", file);
setUploading(true); setUploading(true);
try { try {
await axios.post(`/upload/${cdmType}`, formData); await axios.post(`/upload/${cdmType}`, formData);
await fetchUserInfo(); // Refresh list after upload await fetchUserInfo(); // Refresh list after upload
} catch (err) { } catch (err) {
console.error('Upload failed', err); console.error("Upload failed", err);
alert('Upload failed'); alert("Upload failed");
} finally { } finally {
setUploading(false); setUploading(false);
} }
}; };
// Handle logout // Handle logout
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await axios.post('/logout'); await axios.post("/logout");
window.location.reload(); window.location.reload();
} catch (error) { } catch (error) {
console.error('Logout failed:', error); console.error("Logout failed:", error);
alert('Logout failed!'); alert("Logout failed!");
} }
}; };
// Handle change password // Handle change password
const handleChangePassword = async () => { const handleChangePassword = async () => {
if (passwordError || password === '') { if (passwordError || password === "") {
alert('Please enter a valid password.'); alert("Please enter a valid password.");
return; return;
} }
try { try {
const response = await axios.post('/user/change_password', { const response = await axios.post("/user/change_password", {
new_password: password new_password: password,
}); });
if (response.data.message === 'True') { if (response.data.message === "True") {
alert('Password changed successfully.'); alert("Password changed successfully.");
setPassword(''); setPassword("");
} else { } else {
alert('Failed to change password.'); alert("Failed to change password.");
} }
} catch (error) { } catch (error) {
if (error.response && error.response.data?.message === 'Invalid password format') { if (error.response && error.response.data?.message === "Invalid password format") {
alert('Password format is invalid. Please try again.'); alert("Password format is invalid. Please try again.");
} else { } else {
alert('Error occurred while changing password.'); alert("Error occurred while changing password.");
} }
} }
}; };
// Handle change API key // Handle change API key
const handleChangeApiKey = async () => { const handleChangeApiKey = async () => {
if (apiKeyError || newApiKey === '') { if (apiKeyError || newApiKey === "") {
alert('Please enter a valid API key.'); alert("Please enter a valid API key.");
return; return;
} }
try { try {
const response = await axios.post('/user/change_api_key', { const response = await axios.post("/user/change_api_key", {
new_api_key: newApiKey, new_api_key: newApiKey,
}); });
if (response.data.message === 'True') { if (response.data.message === "True") {
alert('API key changed successfully.'); alert("API key changed successfully.");
setApiKey(newApiKey); setApiKey(newApiKey);
setNewApiKey(''); setNewApiKey("");
} else { } else {
alert('Failed to change API key.'); alert("Failed to change API key.");
} }
} catch (error) { } catch (error) {
alert('Error occurred while changing API key.'); alert("Error occurred while changing API key.");
console.error(error); console.error(error);
} }
}; };
return ( return (
<div id="myaccount" className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4"> <div
<div className="flex-col w-full min-h-164 lg:h-full lg:w-96 border-2 border-yellow-500/50 rounded-2xl p-4 flex items-center overflow-y-auto"> id="myaccount"
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2"> className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4"
{username ? `${username}` : 'My Account'}
</h1>
{/* API Key Section */}
<div className="w-full flex flex-col items-center">
<label htmlFor="apiKey" className="text-white font-semibold mb-1">API Key</label>
<input
id="apiKey"
type="text"
value={apiKey}
readOnly
className="w-full p-2 mb-4 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{/* New API Key Section */}
<label htmlFor="newApiKey" className="text-white font-semibold mt-4 mb-1">New API Key</label>
<input
id="newApiKey"
type="text"
value={newApiKey}
onChange={(e) => {
const value = e.target.value;
const isValid = /^[^\s]+$/.test(value); // No spaces
if (!isValid) {
setApiKeyError('API key must not contain spaces.');
} else {
setApiKeyError('');
}
setNewApiKey(value);
}}
placeholder="Enter new API key"
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{apiKeyError && <p className="text-red-500 text-sm mb-3">{apiKeyError}</p>}
<button
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
onClick={handleChangeApiKey}
>
Change API Key
</button>
{/* Change Password Section */}
<label htmlFor="password" className="text-white font-semibold mt-4 mb-1">Change Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => {
const value = e.target.value;
const isValid = /^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(value);
if (!isValid) {
setPasswordError('Password must not contain spaces or invalid characters.');
} else {
setPasswordError('');
}
setPassword(value);
}}
placeholder="New Password"
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{passwordError && <p className="text-red-500 text-sm mb-3">{passwordError}</p>}
<button
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
onClick={handleChangePassword}
>
Change Password
</button>
</div>
<button
onClick={handleLogout}
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
> >
Log out <div className="flex-col w-full min-h-164 lg:h-full lg:w-96 border-2 border-yellow-500/50 rounded-2xl p-4 flex items-center overflow-y-auto">
</button> <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
</div> {username ? `${username}` : "My Account"}
</h1>
<div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0"> {/* API Key Section */}
{/* Widevine Section */} <div className="w-full flex flex-col items-center">
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto"> <label htmlFor="apiKey" className="text-white font-semibold mb-1">
<h1 className="bg-black text-2xl font-bold text-white border-b-2 border-white p-2">Widevine CDMs</h1> API Key
<div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left"> </label>
{wvList.length === 0 ? ( <input
<div className="text-white text-center font-bold">No Widevine CDMs uploaded.</div> id="apiKey"
) : ( type="text"
wvList.map((filename, i) => ( value={apiKey}
<div readOnly
key={i} className="w-full p-2 mb-4 rounded bg-gray-800 text-white border border-gray-600 text-center"
className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`} />
>
{filename}
</div>
))
)}
</div>
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
{uploading ? 'Uploading...' : 'Upload CDM'}
<input
type="file"
accept=".wvd"
hidden
onChange={(e) => handleUpload(e, 'WV')}
/>
</label>
</div>
{/* Playready Section */} {/* New API Key Section */}
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl p-2 mt-2 lg:mt-2 overflow-y-auto"> <label htmlFor="newApiKey" className="text-white font-semibold mt-4 mb-1">
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 bg-black">Playready CDMs</h1> New API Key
<div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2"> </label>
{prList.length === 0 ? ( <input
<div className="text-white text-center font-bold">No Playready CDMs uploaded.</div> id="newApiKey"
) : ( type="text"
prList.map((filename, i) => ( value={newApiKey}
<div onChange={(e) => {
key={i} const value = e.target.value;
className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`} const isValid = /^[^\s]+$/.test(value); // No spaces
> if (!isValid) {
{filename} setApiKeyError("API key must not contain spaces.");
} else {
setApiKeyError("");
}
setNewApiKey(value);
}}
placeholder="Enter new API key"
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{apiKeyError && <p className="text-red-500 text-sm mb-3">{apiKeyError}</p>}
<button
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
onClick={handleChangeApiKey}
>
Change API Key
</button>
{/* Change Password Section */}
<label htmlFor="password" className="text-white font-semibold mt-4 mb-1">
Change Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => {
const value = e.target.value;
const isValid =
/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(value);
if (!isValid) {
setPasswordError(
"Password must not contain spaces or invalid characters."
);
} else {
setPasswordError("");
}
setPassword(value);
}}
placeholder="New Password"
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
/>
{passwordError && <p className="text-red-500 text-sm mb-3">{passwordError}</p>}
<button
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
onClick={handleChangePassword}
>
Change Password
</button>
</div> </div>
))
)} <button
</div> onClick={handleLogout}
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer"> className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
{uploading ? 'Uploading...' : 'Upload CDM'} >
<input Log out
type="file" </button>
accept=".prd" </div>
hidden
onChange={(e) => handleUpload(e, 'PR')} <div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0">
/> {/* Widevine Section */}
</label> <div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto">
<h1 className="bg-black text-2xl font-bold text-white border-b-2 border-white p-2">
Widevine CDMs
</h1>
<div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
{wvList.length === 0 ? (
<div className="text-white text-center font-bold">
No Widevine CDMs uploaded.
</div>
) : (
wvList.map((filename, i) => (
<div
key={i}
className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? "bg-black/30" : "bg-black/60"}`}
>
{filename}
</div>
))
)}
</div>
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".wvd"
hidden
onChange={(e) => handleUpload(e, "WV")}
/>
</label>
</div>
{/* Playready Section */}
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl p-2 mt-2 lg:mt-2 overflow-y-auto">
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 bg-black">
Playready CDMs
</h1>
<div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2">
{prList.length === 0 ? (
<div className="text-white text-center font-bold">
No Playready CDMs uploaded.
</div>
) : (
prList.map((filename, i) => (
<div
key={i}
className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? "bg-black/30" : "bg-black/60"}`}
>
{filename}
</div>
))
)}
</div>
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
{uploading ? "Uploading..." : "Upload CDM"}
<input
type="file"
accept=".prd"
hidden
onChange={(e) => handleUpload(e, "PR")}
/>
</label>
</div>
</div>
</div> </div>
</div> );
</div>
);
} }
export default MyAccount; export default MyAccount;

View File

@ -1,117 +1,117 @@
import React, { useState } from 'react'; import React, { useState } from "react";
function Register() { function Register() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState("");
const [password, setPassword] = useState(''); const [password, setPassword] = useState("");
const [status, setStatus] = useState(''); const [status, setStatus] = useState("");
// Validation functions // Validation functions
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name); const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
const handleRegister = async () => { const handleRegister = async () => {
if (!validateUsername(username)) { if (!validateUsername(username)) {
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores."); setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
return; return;
} }
if (!validatePassword(password)) { if (!validatePassword(password)) {
setStatus("Invalid password. Spaces are not allowed."); setStatus("Invalid password. Spaces are not allowed.");
return; return;
} }
try { try {
const response = await fetch('/register', { const response = await fetch("/register", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ username, password }) body: JSON.stringify({ username, password }),
}); });
const data = await response.json(); const data = await response.json();
if (data.message) { if (data.message) {
setStatus(data.message); setStatus(data.message);
} else if (data.error) { } else if (data.error) {
setStatus(data.error); setStatus(data.error);
} }
} catch (err) { } catch (err) {
setStatus('An error occurred while registering.'); setStatus("An error occurred while registering.");
} }
}; };
const handleLogin = async () => { const handleLogin = async () => {
if (!validateUsername(username)) { if (!validateUsername(username)) {
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores."); setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
return; return;
} }
if (!validatePassword(password)) { if (!validatePassword(password)) {
setStatus("Invalid password. Spaces are not allowed."); setStatus("Invalid password. Spaces are not allowed.");
return; return;
} }
try { try {
const response = await fetch('/login', { const response = await fetch("/login", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
credentials: 'include', // Important to send cookies credentials: "include", // Important to send cookies
body: JSON.stringify({ username, password }) body: JSON.stringify({ username, password }),
}); });
const data = await response.json(); const data = await response.json();
if (data.message) { if (data.message) {
// Successful login - reload the page to trigger Account check // Successful login - reload the page to trigger Account check
window.location.reload(); window.location.reload();
} else if (data.error) { } else if (data.error) {
setStatus(data.error); setStatus(data.error);
} }
} catch (err) { } catch (err) {
setStatus('An error occurred while logging in.'); setStatus("An error occurred while logging in.");
} }
}; };
return ( return (
<div className="flex flex-col w-full h-full items-center justify-center p-4"> <div className="flex flex-col w-full h-full items-center justify-center p-4">
<div className="flex flex-col w-full h-full lg:w-1/2 lg:h-96 border-2 border-yellow-500/50 rounded-2xl p-4 overflow-x-auto justify-center items-center"> <div className="flex flex-col w-full h-full lg:w-1/2 lg:h-96 border-2 border-yellow-500/50 rounded-2xl p-4 overflow-x-auto justify-center items-center">
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<label htmlFor="username" className="text-lg font-bold mb-2 text-white">Username:</label> <label htmlFor="username" className="text-lg font-bold mb-2 text-white">
<input Username:
type="text" </label>
value={username} <input
onChange={e => setUsername(e.target.value)} type="text"
placeholder="Username" value={username}
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent" onChange={(e) => setUsername(e.target.value)}
/> placeholder="Username"
<label htmlFor="password" className="text-lg font-bold mb-2 text-white">Password:</label> className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
<input />
type="password" <label htmlFor="password" className="text-lg font-bold mb-2 text-white">
value={password} Password:
onChange={e => setPassword(e.target.value)} </label>
placeholder="Password" <input
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent" type="password"
/> value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
</div>
<div className="flex flex-col lg:flex-row w-8/10 items-center lg:justify-between mt-4">
<button
onClick={handleLogin}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Login
</button>
<button
onClick={handleRegister}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Register
</button>
</div>
{status && <p className="text-sm text-white mt-4 p-4">{status}</p>}
</div>
</div> </div>
<div className="flex flex-col lg:flex-row w-8/10 items-center lg:justify-between mt-4"> );
<button
onClick={handleLogin}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Login
</button>
<button
onClick={handleRegister}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
>
Register
</button>
</div>
{status && (
<p className="text-sm text-white mt-4 p-4">
{status}
</p>
)}
</div>
</div>
);
} }
export default Register; export default Register;

View File

@ -1,158 +1,152 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from "react";
import shaka from 'shaka-player'; import shaka from "shaka-player";
import { Helmet } from 'react-helmet'; // Import Helmet import { Helmet } from "react-helmet"; // Import Helmet
function TestPlayer() { function TestPlayer() {
const [mpdUrl, setMpdUrl] = useState(''); // State to hold the MPD URL const [mpdUrl, setMpdUrl] = useState(""); // State to hold the MPD URL
const [kids, setKids] = useState(''); // State to hold KIDs (separated by line breaks) const [kids, setKids] = useState(""); // State to hold KIDs (separated by line breaks)
const [keys, setKeys] = useState(''); // State to hold Keys (separated by line breaks) const [keys, setKeys] = useState(""); // State to hold Keys (separated by line breaks)
const [headers, setHeaders] = useState(''); // State to hold request headers const [headers, setHeaders] = useState(""); // State to hold request headers
const videoRef = useRef(null); // Ref for the video element const videoRef = useRef(null); // Ref for the video element
const playerRef = useRef(null); // Ref for Shaka Player instance const playerRef = useRef(null); // Ref for Shaka Player instance
// Function to update the MPD URL state // Function to update the MPD URL state
const handleInputChange = (event) => { const handleInputChange = (event) => {
setMpdUrl(event.target.value); setMpdUrl(event.target.value);
}; };
// Function to update KIDs and Keys // Function to update KIDs and Keys
const handleKidsChange = (event) => { const handleKidsChange = (event) => {
setKids(event.target.value); setKids(event.target.value);
}; };
const handleKeysChange = (event) => { const handleKeysChange = (event) => {
setKeys(event.target.value); setKeys(event.target.value);
}; };
const handleHeadersChange = (event) => { const handleHeadersChange = (event) => {
setHeaders(event.target.value); setHeaders(event.target.value);
}; };
// Function to initialize Shaka Player // Function to initialize Shaka Player
const initializePlayer = () => { const initializePlayer = () => {
if (videoRef.current) { if (videoRef.current) {
// Initialize Shaka Player only if it's not already initialized // Initialize Shaka Player only if it's not already initialized
if (!playerRef.current) { if (!playerRef.current) {
const player = new shaka.Player(videoRef.current); const player = new shaka.Player(videoRef.current);
playerRef.current = player; playerRef.current = player;
// Add error listener // Add error listener
player.addEventListener('error', (event) => { player.addEventListener("error", (event) => {
console.error('Error code', event.detail.code, 'object', event.detail); console.error("Error code", event.detail.code, "object", event.detail);
});
}
}
};
// Function to handle submit and configure player with DRM keys and headers
const handleSubmit = () => {
if (mpdUrl && kids && keys) {
// Split the KIDs and Keys by new lines
const kidsArray = kids.split("\n").map((k) => k.trim());
const keysArray = keys.split("\n").map((k) => k.trim());
if (kidsArray.length !== keysArray.length) {
console.error("The number of KIDs and Keys must be the same.");
return;
}
// Initialize Shaka Player only when the submit button is pressed
const player = new shaka.Player(videoRef.current);
// Widevine DRM configuration with the provided KIDs and Keys
const config = {
drm: {
clearKeys: {},
},
};
// Map KIDs to Keys
kidsArray.forEach((kid, index) => {
config.drm.clearKeys[kid] = keysArray[index];
});
console.log("Configuring player with the following DRM config and headers:", config);
// Configure the player with ClearKey DRM and custom headers
player.configure(config);
// Load the video stream with MPD URL
player
.load(mpdUrl)
.then(() => {
console.log("Video loaded");
})
.catch((error) => {
console.error("Error loading the video", error);
});
} else {
console.error("MPD URL, KIDs, and Keys are required.");
}
};
// Load the video stream whenever the MPD URL changes
useEffect(() => {
initializePlayer(); // Initialize the player if it's not initialized already
}, []); // This effect runs only once on mount
// Helper function to parse headers from the textarea input
const parseHeaders = (headersText) => {
const headersArr = headersText.split("\n");
const headersObj = {};
headersArr.forEach((line) => {
const [key, value] = line.split(":");
if (key && value) {
headersObj[key.trim()] = value.trim();
}
}); });
} return headersObj;
} };
};
// Function to handle submit and configure player with DRM keys and headers return (
const handleSubmit = () => { <div className="flex flex-col items-center w-full p-4">
if (mpdUrl && kids && keys) { <Helmet>
// Split the KIDs and Keys by new lines <title>Test Player</title>
const kidsArray = kids.split("\n").map((k) => k.trim()); </Helmet>
const keysArray = keys.split("\n").map((k) => k.trim()); <div className="w-full flex flex-col">
<video ref={videoRef} width="100%" height="auto" controls className="h-96" />
if (kidsArray.length !== keysArray.length) { <input
console.error("The number of KIDs and Keys must be the same."); type="text"
return; value={mpdUrl}
} onChange={handleInputChange}
placeholder="MPD URL"
// Initialize Shaka Player only when the submit button is pressed className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
const player = new shaka.Player(videoRef.current); />
<textarea
// Widevine DRM configuration with the provided KIDs and Keys placeholder="KIDs (one per line)"
const config = { value={kids}
drm: { onChange={handleKidsChange}
clearKeys: {}, className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
}, />
}; <textarea
placeholder="Keys (one per line)"
// Map KIDs to Keys value={keys}
kidsArray.forEach((kid, index) => { onChange={handleKeysChange}
config.drm.clearKeys[kid] = keysArray[index]; className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
}); />
<textarea
console.log("Configuring player with the following DRM config and headers:", config); placeholder="Headers (one per line)"
value={headers}
// Configure the player with ClearKey DRM and custom headers onChange={handleHeadersChange}
player.configure(config); className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
// Load the video stream with MPD URL <button onClick={handleSubmit} className="mt-4 p-2 bg-blue-500 text-white rounded">
player.load(mpdUrl).then(() => { Submit
console.log('Video loaded'); </button>
}).catch((error) => { </div>
console.error('Error loading the video', error); </div>
}); );
} else {
console.error('MPD URL, KIDs, and Keys are required.');
}
};
// Load the video stream whenever the MPD URL changes
useEffect(() => {
initializePlayer(); // Initialize the player if it's not initialized already
}, []); // This effect runs only once on mount
// Helper function to parse headers from the textarea input
const parseHeaders = (headersText) => {
const headersArr = headersText.split('\n');
const headersObj = {};
headersArr.forEach((line) => {
const [key, value] = line.split(':');
if (key && value) {
headersObj[key.trim()] = value.trim();
}
});
return headersObj;
};
return (
<div className="flex flex-col items-center w-full p-4">
<Helmet>
<title>Test Player</title>
</Helmet>
<div className="w-full flex flex-col">
<video
ref={videoRef}
width="100%"
height="auto"
controls
className="h-96"
/>
<input
type="text"
value={mpdUrl}
onChange={handleInputChange}
placeholder="MPD URL"
className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="KIDs (one per line)"
value={kids}
onChange={handleKidsChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="Keys (one per line)"
value={keys}
onChange={handleKeysChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="Headers (one per line)"
value={headers}
onChange={handleHeadersChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<button
onClick={handleSubmit}
className="mt-4 p-2 bg-blue-500 text-white rounded"
>
Submit
</button>
</div>
</div>
);
} }
export default TestPlayer; export default TestPlayer;

View File

@ -1,178 +1,182 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { NavLink } from 'react-router-dom'; import { NavLink } from "react-router-dom";
import closeIcon from '../assets/icons/close.svg'; import closeIcon from "../assets/icons/close.svg";
import homeIcon from '../assets/icons/home.svg'; import homeIcon from "../assets/icons/home.svg";
import cacheIcon from '../assets/icons/cache.svg'; import cacheIcon from "../assets/icons/cache.svg";
import apiIcon from '../assets/icons/api.svg'; import apiIcon from "../assets/icons/api.svg";
import testPlayerIcon from '../assets/icons/testplayer.svg'; import testPlayerIcon from "../assets/icons/testplayer.svg";
import accountIcon from '../assets/icons/account.svg'; import accountIcon from "../assets/icons/account.svg";
import discordIcon from '../assets/icons/discord.svg'; import discordIcon from "../assets/icons/discord.svg";
import telegramIcon from '../assets/icons/telegram.svg'; import telegramIcon from "../assets/icons/telegram.svg";
import giteaIcon from '../assets/icons/gitea.svg'; import giteaIcon from "../assets/icons/gitea.svg";
function SideMenu({ isMenuOpen, setIsMenuOpen }) { function SideMenu({ isMenuOpen, setIsMenuOpen }) {
const [externalLinks, setExternalLinks] = useState({ const [externalLinks, setExternalLinks] = useState({
discord: '#', discord: "#",
telegram: '#', telegram: "#",
gitea: '#', gitea: "#",
}); });
useEffect(() => { useEffect(() => {
fetch('/api/links') fetch("/api/links")
.then((res) => res.json()) .then((res) => res.json())
.then((data) => setExternalLinks(data)) .then((data) => setExternalLinks(data))
.catch((err) => console.error('Failed to fetch links:', err)); .catch((err) => console.error("Failed to fetch links:", err));
}, []); }, []);
return ( return (
<div <div
className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${ className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${
isMenuOpen ? 'translate-x-0' : '-translate-x-full' isMenuOpen ? "translate-x-0" : "-translate-x-full"
} z-50`} } z-50`}
style={{ transitionDuration: '0.3s' }} style={{ transitionDuration: "0.3s" }}
> >
<div className="flex flex-col bg-gray-950/55 h-full"> <div className="flex flex-col bg-gray-950/55 h-full">
{/* Header */} {/* Header */}
<div className="h-16 w-full border-b-2 border-white/5 flex flex-row"> <div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
<div className="w-1/4 h-full"></div> <div className="w-1/4 h-full"></div>
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4"> <p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
CDRM-Project CDRM-Project
</p> </p>
<div className="w-1/4 h-full"> <div className="w-1/4 h-full">
<button <button
className="w-full h-full flex items-center justify-center" className="w-full h-full flex items-center justify-center"
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
> >
<img src={closeIcon} alt="Close" className="w-1/2 h-1/2 cursor-pointer" /> <img
</button> src={closeIcon}
</div> alt="Close"
className="w-1/2 h-1/2 cursor-pointer"
/>
</button>
</div>
</div>
{/* Scrollable Navigation Links */}
<div className="overflow-y-auto flex flex-col p-5 w-full flex-grow">
<div className="flex flex-col space-y-2">
<NavLink
to="/"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-sky-500/50 bg-black/50 text-white"
: "border-transparent hover:border-l-sky-500/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={homeIcon} alt="Home" className="w-5 h-5" />
<span className="text-lg">Home</span>
</NavLink>
<NavLink
to="/cache"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-emerald-500/50 bg-black/50 text-white"
: "border-transparent hover:border-l-emerald-500/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={cacheIcon} alt="Cache" className="w-5 h-5" />
<span className="text-lg">Cache</span>
</NavLink>
<NavLink
to="/api"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-indigo-500/50 bg-black/50 text-white"
: "border-transparent hover:border-l-indigo-500/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={apiIcon} alt="API" className="w-5 h-5" />
<span className="text-lg">API</span>
</NavLink>
<NavLink
to="/testplayer"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-rose-700/50 bg-black/50 text-white"
: "border-transparent hover:border-l-rose-700/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={testPlayerIcon} alt="Test Player" className="w-5 h-5" />
<span className="text-lg">Test Player</span>
</NavLink>
</div>
{/* My Account Link at the Bottom of Scrollable Area */}
<div className="mt-auto pt-4">
<NavLink
to="/account"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? "border-l-yellow-500/50 bg-black/50 text-white"
: "border-transparent hover:border-l-yellow-500/50 hover:bg-white/5 text-white/80"
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={accountIcon} alt="My Account" className="w-5 h-5" />
<span className="text-lg">My Account</span>
</NavLink>
</div>
</div>
{/* External Links */}
<div className="h-16 w-full flex flex-row bg-black/5">
<a
href={externalLinks.discord}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-950 group"
>
<img
src={discordIcon}
alt="Discord"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.telegram}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-400 group"
>
<img
src={telegramIcon}
alt="Telegram"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.gitea}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-green-700 group"
>
<img
src={giteaIcon}
alt="Gitea"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
</div>
</div>
</div> </div>
);
{/* Scrollable Navigation Links */}
<div className="overflow-y-auto flex flex-col p-5 w-full flex-grow">
<div className="flex flex-col space-y-2">
<NavLink
to="/"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-sky-500/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-sky-500/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={homeIcon} alt="Home" className="w-5 h-5" />
<span className="text-lg">Home</span>
</NavLink>
<NavLink
to="/cache"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-emerald-500/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-emerald-500/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={cacheIcon} alt="Cache" className="w-5 h-5" />
<span className="text-lg">Cache</span>
</NavLink>
<NavLink
to="/api"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-indigo-500/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-indigo-500/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={apiIcon} alt="API" className="w-5 h-5" />
<span className="text-lg">API</span>
</NavLink>
<NavLink
to="/testplayer"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-rose-700/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-rose-700/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={testPlayerIcon} alt="Test Player" className="w-5 h-5" />
<span className="text-lg">Test Player</span>
</NavLink>
</div>
{/* My Account Link at the Bottom of Scrollable Area */}
<div className="mt-auto pt-4">
<NavLink
to="/account"
className={({ isActive }) =>
`flex flex-row items-center gap-3 p-3 border-l-4 ${
isActive
? 'border-l-yellow-500/50 bg-black/50 text-white'
: 'border-transparent hover:border-l-yellow-500/50 hover:bg-white/5 text-white/80'
}`
}
onClick={() => setIsMenuOpen(false)}
>
<img src={accountIcon} alt="My Account" className="w-5 h-5" />
<span className="text-lg">My Account</span>
</NavLink>
</div>
</div>
{/* External Links */}
<div className="h-16 w-full flex flex-row bg-black/5">
<a
href={externalLinks.discord}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-950 group"
>
<img
src={discordIcon}
alt="Discord"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.telegram}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-400 group"
>
<img
src={telegramIcon}
alt="Telegram"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
<a
href={externalLinks.gitea}
target="_blank"
rel="noopener noreferrer"
className="w-1/3 h-full flex items-center justify-center hover:bg-green-700 group"
>
<img
src={giteaIcon}
alt="Gitea"
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
/>
</a>
</div>
</div>
</div>
);
} }
export default SideMenu; export default SideMenu;

View File

@ -2,9 +2,9 @@
details summary::-webkit-details-marker { details summary::-webkit-details-marker {
display: none; display: none;
} }
details summary { details summary {
list-style: none; list-style: none;
cursor: pointer; cursor: pointer;
} }

View File

@ -1,13 +1,13 @@
import { StrictMode } from 'react' import { StrictMode } from "react";
import { createRoot } from 'react-dom/client' import { createRoot } from "react-dom/client";
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from "react-router-dom";
import './index.css' import "./index.css";
import App from './App.jsx' import App from "./App.jsx";
createRoot(document.getElementById('root')).render( createRoot(document.getElementById("root")).render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</StrictMode> </StrictMode>
) );

View File

@ -1,8 +1,8 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react";
import tailwindcss from '@tailwindcss/vite' import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
}) });