Compare commits

..

6 Commits
main ... github

Author SHA1 Message Date
57625445d8 dist 2025-04-30 05:02:34 -04:00
3bccf4d165 Update README.md 2025-04-30 04:57:27 -04:00
082e7eba53 Update cdm_checks.py 2025-04-30 04:56:25 -04:00
778455cb3e GE 2025-04-30 04:55:14 -04:00
4415372992 Merge branch 'github' of https://cdm-project.com/tpd94/CDRM-Project into github 2025-04-30 04:44:48 -04:00
8af432add4 Merge pull request 'Added dist' (#8) from main into github
Reviewed-on: tpd94/CDRM-Project#8
2025-04-28 22:26:46 +00:00
58 changed files with 6411 additions and 7824 deletions

3
.gitignore vendored
View File

@ -8,6 +8,3 @@ build
main.spec
pyinstallericon.ico
icon.ico
venv
frontend-dist
cdrm-frontend/dist/

View File

@ -1,51 +1,39 @@
# CDRM-Project
## CDRM-Project
![forthebadge](https://forthebadge.com/images/badges/uses-html.svg) ![forthebadge](https://forthebadge.com/images/badges/uses-css.svg) ![forthebadge](https://forthebadge.com/images/badges/uses-javascript.svg) ![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)
## Prerequisites (from source only)
## GITHUB EDITION
> This version **DOES NOT** come with CDM's (Content Decryption Modules) or the link to automatically download them - A simple web search should help you find what you're looking for.
>
## Prerequisites (from source only)
- [Python](https://www.python.org/downloads/) version 3.12+ with PIP installed
- [Python](https://www.python.org/downloads/) version [3.12](https://www.python.org/downloads/release/python-3120/)+ with PIP and VENV installed
> Python 3.13 was used at the time of writing
Python 3.13 was used at the time of writing
- [Node.js](https://nodejs.org/en/download/) v20+
## Installation (Automatic) - Recommended
- Extract contents of CDRM-Project into a new folder
## Installation (Automatic) - Recommended
- Extract contents of CDRM-Project 2.0 git contents into a new folder
- Open a terminal and change directory into the new folder
- Run `python build.py && python main.py`
- Run `python main.py`
- Follow the on-screen prompts
## Installation (From binary)
- Download the latest release from the [releases](https://github.com/TPD94/CDRM-Project-2.0/releases) page and run the `.exe`
- Download the latest release from the [releases](https://cdm-project.com/tpd94/CDRM-Project/releases) page and run the `.exe`
## Installation (Manual)
- Open your terminal and navigate to where you'd like to store the application
- Clone the project with `git clone https://cdm-project.com/tpd94/CDRM-Project.git`
- Navigate to the `CDRM-Project` folder
- Create a new python virtual environment using `python -m venv venv`
- Activate the virtual environment
- Windows:
## Installation (Manual)
- Open your terminal and navigate to where you'd like to store the application
- Create a new python virtual environment using `python -m venv CDRM-Project`
- Change directory into the new `CDRM-Project` folder
- Activate the virtual environment
```bash
.\venv\Scripts\activate
```
- Linux:
> Windows - change directory into the `Scripts` directory then `activate.bat`
>
> Linux - `source bin/activate`
```bash
source venv/bin/activate
```
Verify that the virtual environment is activated by seeing the `(venv)` prefix in your terminal
- Install python dependencies `pip install -r requirements.txt`
- (Optional) Create the folder structure `/configs/CDMs/WV` and place your .WVD file into `/configs/CDMs/WV`
- (Optional) Create the folder structure `/config/CDMs/PR` and place your .PRD file into `/configs/CDMs/PR`
- Build the frontend with `python build.py`
- And finally, run the application with `python main.py`
- Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder
- Install python dependencies `pip install -r requirements.txt`
- (Optional) Create the folder structure `/configs/CDMs/WV` and place your .WVD file into `/configs/CDMs/WV`
- (Optional) Create the folder structure `/config/CDMs/PR` and place your .PRD file into `/configs/CDMs/PR`
- Run the application with `python main.py`

View File

@ -1,39 +0,0 @@
"""Main file to build the frontend."""
import os
import subprocess
import shutil
import sys
def get_npm_command():
"""Get the appropriate npm command for the current OS."""
if sys.platform == "win32":
return "npm.cmd"
return "npm"
def build_frontend():
"""Build the frontend."""
frontend_dir = "cdrm-frontend"
npm_cmd = get_npm_command()
# Check and install dependencies if node_modules doesn't exist
if not os.path.exists(f"{frontend_dir}/node_modules"):
print("📦 Installing dependencies...")
subprocess.run([npm_cmd, "install"], cwd=frontend_dir, check=True)
# Always build the frontend to ensure it's up to date
print("🔨 Building frontend...")
subprocess.run([npm_cmd, "run", "build"], cwd=frontend_dir, check=True)
# Move dist to frontend-dist
if os.path.exists("frontend-dist"):
shutil.rmtree("frontend-dist")
shutil.copytree(f"{frontend_dir}/dist", "frontend-dist")
print("✅ Build complete. Run the application with 'python main.py'")
if __name__ == "__main__":
build_frontend()

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
cdrm-frontend/dist/favico.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

21
cdrm-frontend/dist/index.html vendored Normal file
View File

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

BIN
cdrm-frontend/dist/og-api.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
cdrm-frontend/dist/og-cache.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
cdrm-frontend/dist/og-home.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

BIN
cdrm-frontend/dist/og-testplayer.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -1,30 +1,33 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ["dist"] },
{
files: ["**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
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 }],
},
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
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 },
],
},
},
]

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -10,40 +10,34 @@ import Account from "./components/Pages/Account";
import { Routes, Route } from "react-router-dom";
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 (
<div id="appcontainer" className="flex flex-row w-full h-full bg-black">
{/* The SideMenu should be visible when isMenuOpen is true */}
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
return (
<div id="appcontainer" className="flex flex-row w-full h-full bg-black">
{/* The SideMenu should be visible when isMenuOpen is true */}
<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"
>
<NavBar />
</div>
<div id="navbarcontainer" 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="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 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">
<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>
);
}
export default App;

View File

@ -4,11 +4,11 @@ import "./license_protocol.min.js";
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
function uint8ArrayToBase64(uint8Array) {
const binaryString = Array.from(uint8Array)
.map((b) => String.fromCharCode(b))
.join("");
const binaryString = Array.from(uint8Array)
.map(b => String.fromCharCode(b))
.join('');
return btoa(binaryString);
return btoa(binaryString);
}
function parseFetch(fetchString) {
@ -17,13 +17,10 @@ function parseFetch(fetchString) {
// Use a more lenient regex to capture the fetch pattern (including complex bodies)
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 = {
method: "UNDEFINED",
url: "",
method: 'UNDEFINED',
url: '',
headers: {},
body: null,
};
@ -50,12 +47,9 @@ function parseFetch(fetchString) {
return result;
}
const WIDEVINE_SYSTEM_ID = new Uint8Array([
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 WIDEVINE_SYSTEM_ID = new Uint8Array([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 PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
function intToUint8Array(num, endian) {
@ -81,44 +75,44 @@ function psshDataToPsshBoxB64(pssh_data, system_id) {
...new Uint8Array(4),
...system_id,
...intToUint8Array(dataLength, false),
...pssh_data,
...pssh_data
]);
return uint8ArrayToBase64(pssh);
}
function wrmHeaderToPlayReadyHeader(wrm_header) {
function wrmHeaderToPlayReadyHeader(wrm_header){
const playready_object = new Uint8Array([
...shortToUint8Array(1, true),
...shortToUint8Array(wrm_header.length, true),
...wrm_header,
...wrm_header
]);
return new Uint8Array([
...intToUint8Array(playready_object.length + 2 + 4, true),
...shortToUint8Array(1, true),
...playready_object,
...playready_object
]);
}
function encodeUtf16LE(str) {
const buffer = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
buffer[i * 2] = code & 0xff;
buffer[i * 2 + 1] = code >> 8;
}
return buffer;
const buffer = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
buffer[i * 2] = code & 0xff;
buffer[i * 2 + 1] = code >> 8;
}
return buffer;
}
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() {
try {
// Request text from the clipboard
const clipboardText = await navigator.clipboard.readText();
const result = parseFetch(clipboardText);
let pssh_data_string;
@ -142,15 +136,11 @@ export async function readTextFromClipboard() {
license_request = LicenseRequest.decode(signed_message.msg);
} catch (decodeError) {
// If error occurs during decoding, return an empty pssh
console.error("Decoding failed, returning empty pssh", decodeError);
pssh_data_string = ""; // Empty pssh if decoding fails
console.error('Decoding failed, returning empty pssh', decodeError);
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];
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
}
@ -170,12 +160,14 @@ export async function readTextFromClipboard() {
document.getElementById("pssh").value = pssh_data_string;
document.getElementById("data").value = payload_string;
} 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
function isBinary(uint8Array) {
// 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 { NavLink } from "react-router-dom";
import homeIcon from "../assets/icons/home.svg";
import cacheIcon from "../assets/icons/cache.svg";
import apiIcon from "../assets/icons/api.svg";
import testPlayerIcon from "../assets/icons/testplayer.svg";
import accountIcon from "../assets/icons/account.svg";
import discordIcon from "../assets/icons/discord.svg";
import telegramIcon from "../assets/icons/telegram.svg";
import giteaIcon from "../assets/icons/gitea.svg";
import { useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import homeIcon from '../assets/icons/home.svg';
import cacheIcon from '../assets/icons/cache.svg';
import apiIcon from '../assets/icons/api.svg';
import testPlayerIcon from '../assets/icons/testplayer.svg';
import accountIcon from '../assets/icons/account.svg';
import discordIcon from '../assets/icons/discord.svg';
import telegramIcon from '../assets/icons/telegram.svg';
import giteaIcon from '../assets/icons/gitea.svg';
function NavBar() {
const [externalLinks, setExternalLinks] = useState({
discord: "#",
telegram: "#",
gitea: "#",
discord: '#',
telegram: '#',
gitea: '#',
});
useEffect(() => {
fetch("/api/links")
.then((response) => response.json())
.then((data) => setExternalLinks(data))
.catch((error) => console.error("Error fetching links:", error));
fetch('/api/links')
.then(response => response.json())
.then(data => setExternalLinks(data))
.catch(error => console.error('Error fetching links:', error));
}, []);
return (
@ -28,7 +28,7 @@ function NavBar() {
{/* Header */}
<div>
<p className="text-white text-2xl font-bold p-3 text-center mb-5">
<a href="/">CDRM-Project</a>
<a href="/">CDRM-Project</a><br /><span className="text-sm">Github Edition</span>
</p>
</div>
@ -40,8 +40,8 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? "border-l-sky-500/50 bg-black/50"
: "hover:border-l-sky-500/50 hover:bg-white/5"
? 'border-l-sky-500/50 bg-black/50'
: 'hover:border-l-sky-500/50 hover:bg-white/5'
}`
}
>
@ -58,8 +58,8 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? "border-l-emerald-500/50 bg-black/50"
: "hover:border-l-emerald-500/50 hover:bg-white/5"
? 'border-l-emerald-500/50 bg-black/50'
: 'hover:border-l-emerald-500/50 hover:bg-white/5'
}`
}
>
@ -76,8 +76,8 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? "border-l-indigo-500/50 bg-black/50"
: "hover:border-l-indigo-500/50 hover:bg-white/5"
? 'border-l-indigo-500/50 bg-black/50'
: 'hover:border-l-indigo-500/50 hover:bg-white/5'
}`
}
>
@ -94,17 +94,13 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? "border-l-rose-500/50 bg-black/50"
: "hover:border-l-rose-500/50 hover:bg-white/5"
? 'border-l-rose-500/50 bg-black/50'
: '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">
<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>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
Test Player
@ -118,8 +114,8 @@ function NavBar() {
className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${
isActive
? "border-l-yellow-500/50 bg-black/50"
: "hover:border-l-yellow-500/50 hover:bg-white/5"
? 'border-l-yellow-500/50 bg-black/50'
: 'hover:border-l-yellow-500/50 hover:bg-white/5'
}`
}
>
@ -141,11 +137,7 @@ function NavBar() {
rel="noopener noreferrer"
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
href={externalLinks.telegram}
@ -153,11 +145,7 @@ function NavBar() {
rel="noopener noreferrer"
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
href={externalLinks.gitea}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { useState, useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet'; // Import Helmet
function Cache() {
const [searchQuery, setSearchQuery] = useState("");
const [searchQuery, setSearchQuery] = useState('');
const [cacheData, setCacheData] = useState([]);
const [keyCount, setKeyCount] = useState(0); // New state to store the key count
const debounceTimeout = useRef(null);
@ -10,11 +11,11 @@ function Cache() {
useEffect(() => {
const fetchKeyCount = async () => {
try {
const response = await fetch("/api/cache/keycount");
const response = await fetch('/api/cache/keycount');
const data = await response.json();
setKeyCount(data.count); // Update key count
} catch (error) {
console.error("Error fetching key count:", error);
console.error('Error fetching key count:', error);
}
};
@ -24,7 +25,7 @@ function Cache() {
const handleInputChange = (event) => {
const query = event.target.value;
setSearchQuery(query); // Update the search query
// Clear the previous timeout
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
@ -32,7 +33,7 @@ function Cache() {
// Set a new timeout to send the API call after 1 second of no typing
debounceTimeout.current = setTimeout(() => {
if (query.trim() !== "") {
if (query.trim() !== '') {
sendApiCall(query); // Only call the API if the query is not empty
} else {
setCacheData([]); // Clear results if query is empty
@ -41,22 +42,21 @@ function Cache() {
};
const sendApiCall = (text) => {
fetch("/api/cache/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
fetch('/api/cache/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: text }),
})
.then((response) => response.json())
.then((data) => setCacheData(data)) // Update cache data with the results
.catch((error) => console.error("Error:", error));
.catch((error) => console.error('Error:', error));
};
useEffect(() => {
document.title = "Cache | CDRM-Project";
}, []);
return (
<div className="flex flex-col w-full h-full overflow-y-auto p-4">
<Helmet>
<title>Cache</title>
</Helmet>
<div className="flex flex-col lg:flex-row w-full lg:h-12 items-center">
<input
type="text"

View File

@ -1,272 +1,248 @@
import React, { useEffect, useRef, useState } from "react";
import { readTextFromClipboard } from "../Functions/ParseChallenge";
import React, { useState, useEffect, useRef } from 'react';
import { readTextFromClipboard } from '../Functions/ParseChallenge';
import { Helmet } from 'react-helmet'; // Import Helmet
function HomePage() {
const [pssh, setPssh] = useState("");
const [licurl, setLicurl] = useState("");
const [proxy, setProxy] = useState("");
const [headers, setHeaders] = useState("");
const [cookies, setCookies] = useState("");
const [data, setData] = useState("");
const [message, setMessage] = useState("");
const [isVisible, setIsVisible] = useState(false);
const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState("default");
const [pssh, setPssh] = useState('');
const [licurl, setLicurl] = useState('');
const [proxy, setProxy] = useState('');
const [headers, setHeaders] = useState('');
const [cookies, setCookies] = useState('');
const [data, setData] = useState('');
const [message, setMessage] = useState('');
const [isVisible, setIsVisible] = useState(false);
const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState('default');
useEffect(() => {
document.title = "Home | CDRM-Project";
}, []);
const bottomRef = useRef(null);
const messageRef = useRef(null); // Reference to result container
const bottomRef = useRef(null);
const messageRef = useRef(null); // Reference to result container
const handleReset = () => {
if (isVisible) {
setIsVisible(false);
}
setPssh('');
setLicurl('');
setProxy('');
setHeaders('');
setCookies('');
setData('');
};
const handleReset = () => {
if (isVisible) {
setIsVisible(false);
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("");
setLicurl("");
setProxy("");
setHeaders("");
setCookies("");
setData("");
};
})
.then(res => res.json())
.then(deviceData => {
const combinedDevices = [
...deviceData.Widevine_Devices,
...deviceData.Playready_Devices,
];
const handleSubmitButton = (event) => {
event.preventDefault();
// Add default devices if logged in
const allDevices = [
"CDRM-Project Public Widevine CDM",
"CDRM-Project Public PlayReady CDM",
...combinedDevices,
];
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);
});
};
// 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');
});
}, []);
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);
});
}
};
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)}
/>
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!");
});
};
{/* 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>
</>
)}
useEffect(() => {
if (isVisible && bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [message, isVisible]);
<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>
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");
}
})
.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">
<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>
{isVisible && (
<div id="main_content" className="flex-col w-full h-full p-10 items-center justify-center self-center">
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
<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>
<p
className="w-full grow pt-10 break-words overflow-y-auto"
ref={messageRef}
dangerouslySetInnerHTML={{ __html: message }}
/>
<div ref={bottomRef} />
</div>
{isVisible && (
<div
id="main_content"
className="flex-col w-full h-full p-10 items-center justify-center self-center"
>
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
<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>
<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>
)}
</>
);
</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;

View File

@ -1,293 +1,146 @@
import axios from "axios";
import React, { useEffect, useState } from "react";
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function MyAccount() {
const [wvList, setWvList] = useState([]);
const [prList, setPrList] = useState([]);
const [uploading, setUploading] = useState(false);
const [username, setUsername] = useState("");
const [apiKey, setApiKey] = useState("");
const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState("");
const [newApiKey, setNewApiKey] = useState("");
const [apiKeyError, setApiKeyError] = useState("");
const [wvList, setWvList] = useState([]);
const [prList, setPrList] = useState([]);
const [uploading, setUploading] = useState(false);
const [username, setUsername] = useState('');
// Fetch user info
const fetchUserInfo = async () => {
try {
const response = await axios.post("/userinfo");
setWvList(response.data.Widevine_Devices || []);
setPrList(response.data.Playready_Devices || []);
setUsername(response.data.Styled_Username || "");
setApiKey(response.data.API_Key || "");
} catch (err) {
console.error("Failed to fetch user info", err);
}
};
// Fetch user info
const fetchUserInfo = async () => {
try {
const response = await axios.post('/userinfo');
setWvList(response.data.Widevine_Devices || []);
setPrList(response.data.Playready_Devices || []);
setUsername(response.data.Username || '');
} catch (err) {
console.error('Failed to fetch user info', err);
}
};
useEffect(() => {
fetchUserInfo();
}, []);
useEffect(() => {
fetchUserInfo();
}, []);
useEffect(() => {
document.title = "My account | CDRM-Project";
}, []);
// Handle file upload
const handleUpload = async (event, cdmType) => {
const file = event.target.files[0];
if (!file) return;
// Handle file upload
const handleUpload = async (event, cdmType) => {
const file = event.target.files[0];
if (!file) return;
const extension = file.name.split('.').pop();
if ((cdmType === 'PR' && extension !== 'prd') || (cdmType === 'WV' && extension !== 'wvd')) {
alert(`Please upload a .${cdmType === 'PR' ? 'prd' : 'wvd'} file.`);
return;
}
const extension = file.name.split(".").pop();
if (
(cdmType === "PR" && extension !== "prd") ||
(cdmType === "WV" && extension !== "wvd")
) {
alert(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
return;
}
const formData = new FormData();
formData.append('file', file);
const formData = new FormData();
formData.append("file", file);
setUploading(true);
try {
await axios.post(`/upload/${cdmType}`, formData);
await fetchUserInfo(); // Refresh list after upload
} catch (err) {
console.error('Upload failed', err);
alert('Upload failed');
} finally {
setUploading(false);
}
};
setUploading(true);
try {
await axios.post(`/upload/${cdmType}`, formData);
await fetchUserInfo(); // Refresh list after upload
} catch (err) {
console.error("Upload failed", err);
alert("Upload failed");
} finally {
setUploading(false);
}
};
// Handle logout
const handleLogout = async () => {
try {
await axios.post('/logout');
window.location.reload();
} catch (error) {
console.error('Logout failed:', error);
alert('Logout failed!');
}
};
// Handle logout
const handleLogout = async () => {
try {
await axios.post("/logout");
window.location.reload();
} catch (error) {
console.error("Logout failed:", error);
alert("Logout failed!");
}
};
// Handle change password
const handleChangePassword = async () => {
if (passwordError || password === "") {
alert("Please enter a valid password.");
return;
}
try {
const response = await axios.post("/user/change_password", {
new_password: password,
});
if (response.data.message === "True") {
alert("Password changed successfully.");
setPassword("");
} else {
alert("Failed to change password.");
}
} catch (error) {
if (error.response && error.response.data?.message === "Invalid password format") {
alert("Password format is invalid. Please try again.");
} else {
alert("Error occurred while changing password.");
}
}
};
// Handle change API key
const handleChangeApiKey = async () => {
if (apiKeyError || newApiKey === "") {
alert("Please enter a valid API key.");
return;
}
try {
const response = await axios.post("/user/change_api_key", {
new_api_key: newApiKey,
});
if (response.data.message === "True") {
alert("API key changed successfully.");
setApiKey(newApiKey);
setNewApiKey("");
} else {
alert("Failed to change API key.");
}
} catch (error) {
alert("Error occurred while changing API key.");
console.error(error);
}
};
return (
<div
id="myaccount"
className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4"
>
<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">
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
{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
</button>
</div>
<div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0">
{/* Widevine Section */}
<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>
return (
<div id="myaccount" className="flex flex-row w-full min-h-full overflow-y-auto p-4">
<div className="flex flex-col w-full min-h-full lg:flex-row">
{/* Left Panel */}
<div className="border-2 border-yellow-500/50 lg:h-full lg:w-96 w-full rounded-2xl p-4 flex flex-col items-center overflow-y-auto">
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
{username ? `${username}` : 'My Account'}
</h1>
<button
onClick={handleLogout}
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
>
Log out
</button>
</div>
);
{/* Right Panel */}
<div className="flex flex-col grow lg:ml-2 mt-2 lg:mt-0">
{/* Widevine Section */}
<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="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 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">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 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>
);
}
export default MyAccount;

View File

@ -1,121 +1,95 @@
import React, { useEffect, useState } from "react";
import React, { useState } from 'react';
function Register() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [status, setStatus] = useState("");
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [status, setStatus] = useState('');
// Validation functions
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
const handleRegister = async () => {
try {
const response = await fetch('/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.message) {
setStatus(data.message);
} else if (data.error) {
setStatus(data.error);
}
} catch (err) {
setStatus('An error occurred while registering.');
}
};
useEffect(() => {
document.title = "Register | CDRM-Project";
}, []);
const handleLogin = async () => {
try {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include', // Important to send cookies
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.message) {
// Successful login - reload the page to trigger Account check
window.location.reload();
} else if (data.error) {
setStatus(data.error);
}
} catch (err) {
setStatus('An error occurred while logging in.');
}
};
const handleRegister = async () => {
if (!validateUsername(username)) {
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
return;
}
if (!validatePassword(password)) {
setStatus("Invalid password. Spaces are not allowed.");
return;
}
try {
const response = await fetch("/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (data.message) {
setStatus(data.message);
} else if (data.error) {
setStatus(data.error);
}
} catch (err) {
setStatus("An error occurred while registering.");
}
};
const handleLogin = async () => {
if (!validateUsername(username)) {
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
return;
}
if (!validatePassword(password)) {
setStatus("Invalid password. Spaces are not allowed.");
return;
}
try {
const response = await fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include", // Important to send cookies
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (data.message) {
// Successful login - reload the page to trigger Account check
window.location.reload();
} else if (data.error) {
setStatus(data.error);
}
} catch (err) {
setStatus("An error occurred while logging in.");
}
};
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 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">
<label htmlFor="username" className="text-lg font-bold mb-2 text-white">
Username:
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
<label htmlFor="password" className="text-lg font-bold mb-2 text-white">
Password:
</label>
<input
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>
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 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">
<label htmlFor="username" className="text-lg font-bold mb-2 text-white">Username:</label>
<input
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="Username"
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
/>
<label htmlFor="password" className="text-lg font-bold mb-2 text-white">Password:</label>
<input
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>
);
}
export default Register;

View File

@ -1,152 +1,158 @@
import React, { useState, useEffect, useRef } from "react";
import shaka from "shaka-player";
import React, { useState, useEffect, useRef } from 'react';
import shaka from 'shaka-player';
import { Helmet } from 'react-helmet'; // Import Helmet
function TestPlayer() {
const [mpdUrl, setMpdUrl] = useState(""); // State to hold the MPD URL
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 [headers, setHeaders] = useState(""); // State to hold request headers
const [mpdUrl, setMpdUrl] = useState(''); // State to hold the MPD URL
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 [headers, setHeaders] = useState(''); // State to hold request headers
const videoRef = useRef(null); // Ref for the video element
const playerRef = useRef(null); // Ref for Shaka Player instance
const videoRef = useRef(null); // Ref for the video element
const playerRef = useRef(null); // Ref for Shaka Player instance
// Function to update the MPD URL state
const handleInputChange = (event) => {
setMpdUrl(event.target.value);
};
// Function to update the MPD URL state
const handleInputChange = (event) => {
setMpdUrl(event.target.value);
};
// Function to update KIDs and Keys
const handleKidsChange = (event) => {
setKids(event.target.value);
};
// Function to update KIDs and Keys
const handleKidsChange = (event) => {
setKids(event.target.value);
};
const handleKeysChange = (event) => {
setKeys(event.target.value);
};
const handleKeysChange = (event) => {
setKeys(event.target.value);
};
const handleHeadersChange = (event) => {
setHeaders(event.target.value);
};
const handleHeadersChange = (event) => {
setHeaders(event.target.value);
};
// Function to initialize Shaka Player
const initializePlayer = () => {
if (videoRef.current) {
// Initialize Shaka Player only if it's not already initialized
if (!playerRef.current) {
const player = new shaka.Player(videoRef.current);
playerRef.current = player;
// Function to initialize Shaka Player
const initializePlayer = () => {
if (videoRef.current) {
// Initialize Shaka Player only if it's not already initialized
if (!playerRef.current) {
const player = new shaka.Player(videoRef.current);
playerRef.current = player;
// Add error listener
player.addEventListener("error", (event) => {
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();
}
// Add error listener
player.addEventListener('error', (event) => {
console.error('Error code', event.detail.code, 'object', event.detail);
});
return headersObj;
};
}
}
};
useEffect(() => {
document.title = "Test player | CDRM-Project";
}, []);
// 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());
return (
<div className="flex flex-col items-center w-full p-4">
<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>
);
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;
};
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;

View File

@ -1,182 +1,179 @@
import { useEffect, useState } from "react";
import { NavLink } from "react-router-dom";
import closeIcon from "../assets/icons/close.svg";
import homeIcon from "../assets/icons/home.svg";
import cacheIcon from "../assets/icons/cache.svg";
import apiIcon from "../assets/icons/api.svg";
import testPlayerIcon from "../assets/icons/testplayer.svg";
import accountIcon from "../assets/icons/account.svg";
import discordIcon from "../assets/icons/discord.svg";
import telegramIcon from "../assets/icons/telegram.svg";
import giteaIcon from "../assets/icons/gitea.svg";
import { useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import closeIcon from '../assets/icons/close.svg';
import homeIcon from '../assets/icons/home.svg';
import cacheIcon from '../assets/icons/cache.svg';
import apiIcon from '../assets/icons/api.svg';
import testPlayerIcon from '../assets/icons/testplayer.svg';
import accountIcon from '../assets/icons/account.svg';
import discordIcon from '../assets/icons/discord.svg';
import telegramIcon from '../assets/icons/telegram.svg';
import giteaIcon from '../assets/icons/gitea.svg';
function SideMenu({ isMenuOpen, setIsMenuOpen }) {
const [externalLinks, setExternalLinks] = useState({
discord: "#",
telegram: "#",
gitea: "#",
});
const [externalLinks, setExternalLinks] = useState({
discord: '#',
telegram: '#',
gitea: '#',
});
useEffect(() => {
fetch("/api/links")
.then((res) => res.json())
.then((data) => setExternalLinks(data))
.catch((err) => console.error("Failed to fetch links:", err));
}, []);
useEffect(() => {
fetch('/api/links')
.then((res) => res.json())
.then((data) => setExternalLinks(data))
.catch((err) => console.error('Failed to fetch links:', err));
}, []);
return (
<div
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"
} z-50`}
style={{ transitionDuration: "0.3s" }}
>
<div className="flex flex-col bg-gray-950/55 h-full">
{/* Header */}
<div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
<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">
CDRM-Project
</p>
<div className="w-1/4 h-full">
<button
className="w-full h-full flex items-center justify-center"
onClick={() => setIsMenuOpen(false)}
>
<img
src={closeIcon}
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>
return (
<div
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'
} z-50`}
style={{ transitionDuration: '0.3s' }}
>
<div className="flex flex-col bg-gray-950/55 h-full">
{/* Header */}
<div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
<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 flex-col">
CDRM-Project<br />
<span className="text-sm">Github Edition</span>
</p>
<div className="w-1/4 h-full">
<button
className="w-full h-full flex items-center justify-center"
onClick={() => setIsMenuOpen(false)}
>
<img src={closeIcon} 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>
);
}
export default SideMenu;

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
"""Icon links module for social media links."""
data = {
"discord": "https://discord.cdrm-project.com/",
"telegram": "https://telegram.cdrm-project.com/",
"gitea": "https://cdm-project.com/tpd94/cdrm-project",
}
'discord': 'https://discord.cdrm-project.com/',
'telegram': 'https://telegram.cdrm-project.com/',
'gitea': 'https://cdm-project.com/tpd94/cdm-project'
}

View File

@ -1,49 +1,47 @@
"""Index tags module for the index page."""
tags = {
"index": {
"description": "Decrypt Widevine and PlayReady protected content",
"keywords": "CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption",
"opengraph_title": "CDRM-Project",
"opengraph_description": "Self Hosted web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library to decrypt Widevine & Playready content",
"opengraph_image": "https://cdrm-project.com/og-home.jpg",
"opengraph_url": "https://cdm-project.com/tpd94/cdrm-project",
"tab_title": "CDRM-Project",
'index': {
'description': 'Decrypt Widevine and PlayReady protected content',
'keywords': 'CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption',
'opengraph_title': 'CDRM-Project',
'opengraph_description': 'Self Hosted web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library to decrypt Widevine & Playready content',
'opengraph_image': 'https://cdrm-project.com/og-home.jpg',
'opengraph_url': 'https://cdm-project.com/tpd94/cdrm-project',
'tab_title': 'CDRM-Project',
},
"cache": {
"description": "Search the cache by KID or PSSH for decryption keys",
"keywords": "Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption",
"opengraph_title": "Search the Cache",
"opengraph_description": "Search the cache by KID or PSSH for decryption keys",
"opengraph_image": "https://cdrm-project.com/og-cache.jpg",
"opengraph_url": "https://cdrm-project.com/cache",
"tab_title": "Cache",
'cache': {
'description': 'Search the cache by KID or PSSH for decryption keys',
'keywords': 'Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption',
'opengraph_title': 'Search the Cache',
'opengraph_description': 'Search the cache by KID or PSSH for decryption keys',
'opengraph_image': 'https://cdrm-project.com/og-cache.jpg',
'opengraph_url': 'https://cdrm-project.com/cache',
'tab_title': 'Cache',
},
"testplayer": {
"description": "Shaka Player for testing decryption keys",
"keywords": "Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY",
"opengraph_title": "Test Player",
"opengraph_description": "Shaka Player for testing decryption keys",
"opengraph_image": "https://cdrm-project.com/og-testplayer.jpg",
"opengraph_url": "https://cdrm-project.com/testplayer",
"tab_title": "Test Player",
'testplayer': {
'description': 'Shaka Player for testing decryption keys',
'keywords': 'Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY',
'opengraph_title': 'Test Player',
'opengraph_description': 'Shaka Player for testing decryption keys',
'opengraph_image': 'https://cdrm-project.com/og-testplayer.jpg',
'opengraph_url': 'https://cdrm-project.com/testplayer',
'tab_title': 'Test Player',
},
"api": {
"description": 'API documentation for the program "CDRM-Project"',
"keywords": "API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault",
"opengraph_title": "API",
"opengraph_description": 'Documentation for the program "CDRM-Project"',
"opengraph_image": "https://cdrm-project.com/og-api.jpg",
"opengraph_url": "https://cdrm-project.com/api",
"tab_title": "API",
'api': {
'description': 'API documentation for the program "CDRM-Project"',
'keywords': 'API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault',
'opengraph_title': 'API',
'opengraph_description': 'Documentation for the program "CDRM-Project"',
'opengraph_image': 'https://cdrm-project.com/og-api.jpg',
'opengraph_url': 'https://cdrm-project.com/api',
'tab_title': 'API',
},
"account": {
"description": "Account for CDRM-Project",
"keywords": "Login, CDRM, CDM, CDRM-Project, register, account",
"opengraph_title": "My account",
"opengraph_description": "Account for CDRM-Project",
"opengraph_image": "https://cdrm-project.com/og-home.jpg",
"opengraph_url": "https://cdrm-project.com/account",
"tab_title": "My account",
},
}
'account': {
'description': 'Account for CDRM-Project',
'keywords': 'Login, CDRM, CDM, CDRM-Project, register, account',
'opengraph_title': 'My account',
'opengraph_description': 'Account for CDRM-Project',
'opengraph_image': 'https://cdrm-project.com/og-home.jpg',
'opengraph_url': 'https://cdrm-project.com/account',
'tab_title': 'My account',
}
}

View File

@ -1,33 +1,28 @@
"""Module to cache data to MariaDB."""
import os
import yaml
import mysql.connector
from mysql.connector import Error
def get_db_config():
"""Get the database configuration for MariaDB."""
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
# Configure your MariaDB connection
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
db_config = {
"host": f'{config["mariadb"]["host"]}',
"user": f'{config["mariadb"]["user"]}',
"password": f'{config["mariadb"]["password"]}',
"database": f'{config["mariadb"]["database"]}',
'host': f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}'
}
return db_config
def create_database():
"""Create the database for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute('''
CREATE TABLE IF NOT EXISTS licenses (
SERVICE VARCHAR(255),
PSSH TEXT,
@ -38,33 +33,20 @@ def create_database():
Cookies TEXT,
Data BLOB
)
"""
)
''')
conn.commit()
except Error as e:
print(f"Error: {e}")
def cache_to_db(
service: str = "",
pssh: str = "",
kid: str = "",
key: str = "",
license_url: str = "",
headers: str = "",
cookies: str = "",
data: str = "",
):
"""Cache data to the database for MariaDB."""
def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, headers=None, cookies=None, data=None):
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM licenses WHERE KID = %s", (kid,))
cursor.execute('SELECT 1 FROM licenses WHERE KID = %s', (kid,))
existing_record = cursor.fetchone()
cursor.execute(
"""
cursor.execute('''
INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
@ -75,9 +57,7 @@ def cache_to_db(
Headers = VALUES(Headers),
Cookies = VALUES(Cookies),
Data = VALUES(Data)
""",
(service, pssh, kid, key, license_url, headers, cookies, data),
)
''', (service, pssh, kid, key, license_url, headers, cookies, data))
conn.commit()
return True if existing_record else False
@ -85,84 +65,61 @@ def cache_to_db(
print(f"Error: {e}")
return False
def search_by_pssh_or_kid(search_filter):
"""Search the database by PSSH or KID for MariaDB."""
results = set()
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
like_filter = f"%{search_filter}%"
cursor.execute(
"SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s",
(like_filter,),
)
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s', (like_filter,))
results.update(cursor.fetchall())
cursor.execute(
"SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s",
(like_filter,),
)
cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s', (like_filter,))
results.update(cursor.fetchall())
final_results = [
{"PSSH": row[0], "KID": row[1], "Key": row[2]} for row in results
]
final_results = [{'PSSH': row[0], 'KID': row[1], 'Key': row[2]} for row in results]
return final_results[:20]
except Error as e:
print(f"Error: {e}")
return []
def get_key_by_kid_and_service(kid, service):
"""Get the key by KID and service for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s",
(kid, service),
)
cursor.execute('SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s', (kid, service))
result = cursor.fetchone()
return result[0] if result else None
except Error as e:
print(f"Error: {e}")
return None
def get_kid_key_dict(service_name):
"""Get the KID and key dictionary for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT KID, `Key` FROM licenses WHERE SERVICE = %s", (service_name,)
)
cursor.execute('SELECT KID, `Key` FROM licenses WHERE SERVICE = %s', (service_name,))
return {row[0]: row[1] for row in cursor.fetchall()}
except Error as e:
print(f"Error: {e}")
return {}
def get_unique_services():
"""Get the unique services for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute("SELECT DISTINCT SERVICE FROM licenses")
cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
return [row[0] for row in cursor.fetchall()]
except Error as e:
print(f"Error: {e}")
return []
def key_count():
"""Get the key count for MariaDB."""
try:
with mysql.connector.connect(**get_db_config()) as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(KID) FROM licenses")
cursor.execute('SELECT COUNT(KID) FROM licenses')
return cursor.fetchone()[0]
except Error as e:
print(f"Error: {e}")

View File

@ -1,17 +1,11 @@
"""Module to cache data to SQLite."""
import sqlite3
import os
def create_database():
"""Create the database for SQLite."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
) as conn:
# Using with statement to manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute('''
CREATE TABLE IF NOT EXISTS licenses (
SERVICE TEXT,
PSSH TEXT,
@ -22,138 +16,92 @@ def create_database():
Cookies TEXT,
Data TEXT
)
"""
)
''')
def cache_to_db(
service: str = "",
pssh: str = "",
kid: str = "",
key: str = "",
license_url: str = "",
headers: str = "",
cookies: str = "",
data: str = "",
):
"""Cache data to the database for SQLite."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
) as conn:
def cache_to_db(service: str = None, pssh: str = None, kid: str = None, key: str = None, license_url: str = None, headers: str = None, cookies: str = None, data: str = None):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Check if the record with the given KID already exists
cursor.execute("""SELECT 1 FROM licenses WHERE KID = ?""", (kid,))
cursor.execute('''SELECT 1 FROM licenses WHERE KID = ?''', (kid,))
existing_record = cursor.fetchone()
# Insert or replace the record
cursor.execute(
"""
cursor.execute('''
INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(service, pssh, kid, key, license_url, headers, cookies, data),
)
''', (service, pssh, kid, key, license_url, headers, cookies, data))
# If the record was existing and updated, return True (updated), else return False (added)
return True if existing_record else False
def search_by_pssh_or_kid(search_filter):
"""Search the database by PSSH or KID for SQLite."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
) as conn:
# Using with statement to automatically close the connection
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Initialize a set to store unique matching records
results = set()
# Search for records where PSSH contains the search_filter
cursor.execute(
"""
cursor.execute('''
SELECT * FROM licenses WHERE PSSH LIKE ?
""",
("%" + search_filter + "%",),
)
''', ('%' + search_filter + '%',))
rows = cursor.fetchall()
for row in rows:
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
# Search for records where KID contains the search_filter
cursor.execute(
"""
cursor.execute('''
SELECT * FROM licenses WHERE KID LIKE ?
""",
("%" + search_filter + "%",),
)
''', ('%' + search_filter + '%',))
rows = cursor.fetchall()
for row in rows:
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
# Convert the set of results to a list of dictionaries for output
final_results = [
{"PSSH": result[0], "KID": result[1], "Key": result[2]}
for result in results
]
final_results = [{'PSSH': result[0], 'KID': result[1], 'Key': result[2]} for result in results]
return final_results[:20]
def get_key_by_kid_and_service(kid, service):
"""Get the key by KID and service for SQLite."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
) as conn:
# Using 'with' to automatically close the connection when done
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Query to search by KID and SERVICE
cursor.execute(
"""
cursor.execute('''
SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ?
""",
(kid, service),
)
''', (kid, service))
# Fetch the result
result = cursor.fetchone()
# Check if a result was found
return (
result[0] if result else None
) # The 'Key' is the first (and only) column returned in the result
return result[0] if result else None # The 'Key' is the first (and only) column returned in the result
def get_kid_key_dict(service_name):
"""Get the KID and key dictionary for SQLite."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
) as conn:
# Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Query to fetch KID and Key for the selected service
cursor.execute(
"""
cursor.execute('''
SELECT KID, Key FROM licenses WHERE SERVICE = ?
""",
(service_name,),
)
''', (service_name,))
# Fetch all results and create the dictionary
kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()}
return kid_key_dict
def get_unique_services():
"""Get the unique services for SQLite."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
) as conn:
# Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Query to get distinct services from the 'licenses' table
cursor.execute("SELECT DISTINCT SERVICE FROM licenses")
cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
# Fetch all results and extract the unique services
services = cursor.fetchall()
@ -163,16 +111,13 @@ def get_unique_services():
return unique_services
def key_count():
"""Get the key count for SQLite."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
) as conn:
# Using with statement to automatically manage the connection and cursor
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
cursor = conn.cursor()
# Count the number of KID entries in the licenses table
cursor.execute("SELECT COUNT(KID) FROM licenses")
cursor.execute('SELECT COUNT(KID) FROM licenses')
count = cursor.fetchone()[0] # Fetch the result and get the count
return count

View File

@ -1,159 +0,0 @@
"""Unified database operations module that automatically uses the correct backend."""
import os
from typing import Optional, List, Dict, Any
import yaml
# Import both backend modules
try:
import custom_functions.database.cache_to_db_sqlite as sqlite_db
except ImportError:
sqlite_db = None
try:
import custom_functions.database.cache_to_db_mariadb as mariadb_db
except ImportError:
mariadb_db = None
class DatabaseOperations:
"""Unified database operations class that automatically selects the correct backend."""
def __init__(self):
self.backend = self._get_database_backend()
self.db_module = self._get_db_module()
def _get_database_backend(self) -> str:
"""Get the database backend from config, default to sqlite."""
try:
config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config.get("database_type", "sqlite").lower()
except (FileNotFoundError, KeyError, yaml.YAMLError):
return "sqlite"
def _get_db_module(self):
"""Get the appropriate database module based on backend."""
if self.backend == "mariadb" and mariadb_db:
return mariadb_db
if sqlite_db:
return sqlite_db
raise ImportError(f"Database module for {self.backend} not available")
def get_backend_info(self) -> Dict[str, str]:
"""Get information about the current database backend being used."""
return {
"backend": self.backend,
"module": self.db_module.__name__ if self.db_module else "None",
}
def create_database(self) -> None:
"""Create the database using the configured backend."""
return self.db_module.create_database()
def cache_to_db(
self,
service: str = "",
pssh: str = "",
kid: str = "",
key: str = "",
license_url: str = "",
headers: str = "",
cookies: str = "",
data: str = "",
) -> bool:
"""Cache data to the database using the configured backend."""
return self.db_module.cache_to_db(
service=service,
pssh=pssh,
kid=kid,
key=key,
license_url=license_url,
headers=headers,
cookies=cookies,
data=data,
)
def search_by_pssh_or_kid(self, search_filter: str) -> List[Dict[str, str]]:
"""Search the database by PSSH or KID using the configured backend."""
return self.db_module.search_by_pssh_or_kid(search_filter)
def get_key_by_kid_and_service(self, kid: str, service: str) -> Optional[str]:
"""Get the key by KID and service using the configured backend."""
return self.db_module.get_key_by_kid_and_service(kid, service)
def get_kid_key_dict(self, service_name: str) -> Dict[str, str]:
"""Get the KID and key dictionary using the configured backend."""
return self.db_module.get_kid_key_dict(service_name)
def get_unique_services(self) -> List[str]:
"""Get the unique services using the configured backend."""
return self.db_module.get_unique_services()
def key_count(self) -> int:
"""Get the key count using the configured backend."""
return self.db_module.key_count()
# Create a singleton instance for easy import and use
db_ops = DatabaseOperations()
# Convenience functions that use the singleton instance
def get_backend_info() -> Dict[str, str]:
"""Get information about the current database backend being used."""
return db_ops.get_backend_info()
def create_database() -> None:
"""Create the database using the configured backend."""
return db_ops.create_database()
def cache_to_db(
service: str = "",
pssh: str = "",
kid: str = "",
key: str = "",
license_url: str = "",
headers: str = "",
cookies: str = "",
data: str = "",
) -> bool:
"""Cache data to the database using the configured backend."""
return db_ops.cache_to_db(
service=service,
pssh=pssh,
kid=kid,
key=key,
license_url=license_url,
headers=headers,
cookies=cookies,
data=data,
)
def search_by_pssh_or_kid(search_filter: str) -> List[Dict[str, str]]:
"""Search the database by PSSH or KID using the configured backend."""
return db_ops.search_by_pssh_or_kid(search_filter)
def get_key_by_kid_and_service(kid: str, service: str) -> Optional[str]:
"""Get the key by KID and service using the configured backend."""
return db_ops.get_key_by_kid_and_service(kid, service)
def get_kid_key_dict(service_name: str) -> Dict[str, str]:
"""Get the KID and key dictionary using the configured backend."""
return db_ops.get_kid_key_dict(service_name)
def get_unique_services() -> List[str]:
"""Get the unique services using the configured backend."""
return db_ops.get_unique_services()
def key_count() -> int:
"""Get the key count using the configured backend."""
return db_ops.key_count()

View File

@ -1,43 +1,28 @@
"""Module to manage the user database."""
import sqlite3
import os
import bcrypt
def create_user_database():
"""Create the user database."""
os.makedirs(os.path.join(os.getcwd(), "databases", "sql"), exist_ok=True)
os.makedirs(f'{os.getcwd()}/databases/sql', exist_ok=True)
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_info (
Username TEXT PRIMARY KEY,
Password TEXT,
Styled_Username TEXT,
API_Key TEXT
)
"""
Password TEXT
)
''')
def add_user(username, password, api_key):
"""Add a user to the database."""
hashed_pw = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
def add_user(username, password):
hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)",
(username.lower(), hashed_pw, username, api_key),
)
cursor.execute('INSERT INTO user_info (Username, Password) VALUES (?, ?)', (username, hashed_pw))
conn.commit()
return True
except sqlite3.IntegrityError:
@ -45,100 +30,16 @@ def add_user(username, password, api_key):
def verify_user(username, password):
"""Verify a user."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT Password FROM user_info WHERE Username = ?", (username.lower(),)
)
cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username,))
result = cursor.fetchone()
if result:
stored_hash = result[0]
# Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT)
if isinstance(stored_hash, str):
stored_hash = stored_hash.encode("utf-8")
return bcrypt.checkpw(password.encode("utf-8"), stored_hash)
return False
def fetch_api_key(username):
"""Fetch the API key for a user."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT API_Key FROM user_info WHERE Username = ?", (username.lower(),)
)
result = cursor.fetchone()
if result:
return result[0]
return None
def change_password(username, new_password):
"""Change the password for a user."""
# Hash the new password
new_hashed_pw = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt())
# Update the password in the database
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute(
"UPDATE user_info SET Password = ? WHERE Username = ?",
(new_hashed_pw, username.lower()),
)
conn.commit()
return True
def change_api_key(username, new_api_key):
"""Change the API key for a user."""
# Update the API key in the database
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute(
"UPDATE user_info SET API_Key = ? WHERE Username = ?",
(new_api_key, username.lower()),
)
conn.commit()
return True
def fetch_styled_username(username):
"""Fetch the styled username for a user."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT Styled_Username FROM user_info WHERE Username = ?",
(username.lower(),),
)
result = cursor.fetchone()
if result:
return result[0]
return None
def fetch_username_by_api_key(api_key):
"""Fetch the username for a user by API key."""
with sqlite3.connect(
os.path.join(os.getcwd(), "databases", "sql", "users.db")
) as conn:
cursor = conn.cursor()
cursor.execute("SELECT Username FROM user_info WHERE API_Key = ?", (api_key,))
result = cursor.fetchone()
if result:
return result[0] # Return the username
return None # If no user is found for the API key
stored_hash = stored_hash.encode('utf-8')
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
else:
return False

View File

@ -1,45 +1,31 @@
"""Module to decrypt the license using the API."""
import base64
import ast
import glob
import json
import os
from urllib.parse import urlparse
import binascii
import requests
from requests.exceptions import Timeout, RequestException
import yaml
from pywidevine.cdm import Cdm as widevineCdm
from pywidevine.device import Device as widevineDevice
from pywidevine.pssh import PSSH as widevinePSSH
from pyplayready.cdm import Cdm as playreadyCdm
from pyplayready.device import Device as playreadyDevice
from pyplayready.system.pssh import PSSH as playreadyPSSH
import requests
import base64
import ast
import glob
import os
import yaml
from urllib.parse import urlparse
from custom_functions.database.unified_db_ops import cache_to_db
def find_license_key(data, keywords=None):
"""Find the license key in the data."""
if keywords is None:
keywords = [
"license",
"licenseData",
"widevine2License",
] # Default list of keywords to search for
keywords = ["license", "licenseData", "widevine2License"] # Default list of keywords to search for
# If the data is a dictionary, check each key
if isinstance(data, dict):
for key, value in data.items():
if any(
keyword in key.lower() for keyword in keywords
): # Check if any keyword is in the key (case-insensitive)
return value.replace("-", "+").replace(
"_", "/"
) # Return the value immediately when found
if any(keyword in key.lower() for keyword in
keywords): # Check if any keyword is in the key (case-insensitive)
return value.replace("-", "+").replace("_", "/") # Return the value immediately when found
# Recursively check if the value is a dictionary or list
if isinstance(value, (dict, list)):
result = find_license_key(value, keywords) # Recursively search
@ -57,52 +43,37 @@ def find_license_key(data, keywords=None):
def find_license_challenge(data, keywords=None, new_value=None):
"""Find the license challenge in the data."""
if keywords is None:
keywords = [
"license",
"licenseData",
"widevine2License",
"licenseRequest",
] # Default list of keywords to search for
keywords = ["license", "licenseData", "widevine2License", "licenseRequest"] # Default list of keywords to search for
# If the data is a dictionary, check each key
if isinstance(data, dict):
for key, value in data.items():
if any(
keyword in key.lower() for keyword in keywords
): # Check if any keyword is in the key (case-insensitive)
if any(keyword in key.lower() for keyword in keywords): # Check if any keyword is in the key (case-insensitive)
data[key] = new_value # Modify the value in-place
# Recursively check if the value is a dictionary or list
elif isinstance(value, (dict, list)):
find_license_challenge(
value, keywords, new_value
) # Recursively modify in place
find_license_challenge(value, keywords, new_value) # Recursively modify in place
# If the data is a list, iterate through each item
elif isinstance(data, list):
for i, item in enumerate(data):
result = find_license_challenge(
item, keywords, new_value
) # Recursively modify in place
result = find_license_challenge(item, keywords, new_value) # Recursively modify in place
return data # Return the modified original data (no new structure is created)
def is_base64(string):
"""Check if the string is base64 encoded."""
try:
# Try decoding the string
decoded_data = base64.b64decode(string)
# Check if the decoded data, when re-encoded, matches the original string
return base64.b64encode(decoded_data).decode("utf-8") == string
except (binascii.Error, TypeError):
return base64.b64encode(decoded_data).decode('utf-8') == string
except Exception:
# If decoding or encoding fails, it's not Base64
return False
def is_url_and_split(input_str):
"""Check if the string is a URL and split it into protocol and FQDN."""
parsed = urlparse(input_str)
# Check if it's a valid URL with scheme and netloc
@ -110,286 +81,364 @@ def is_url_and_split(input_str):
protocol = parsed.scheme
fqdn = parsed.netloc
return True, protocol, fqdn
return False, None, None
def load_device(device_type, device, username, config):
"""Load the appropriate device file for PlayReady or Widevine."""
if device_type == "PR":
ext, config_key, class_loader = ".prd", "default_pr_cdm", playreadyDevice.load
base_dir = "PR"
else:
ext, config_key, class_loader = ".wvd", "default_wv_cdm", widevineDevice.load
base_dir = "WV"
return False, None, None
if device == "public":
base_name = config[config_key]
if not base_name.endswith(ext):
base_name += ext
search_path = f"{os.getcwd()}/configs/CDMs/{base_dir}/{base_name}"
else:
base_name = device
if not base_name.endswith(ext):
base_name += ext
search_path = f"{os.getcwd()}/configs/CDMs/{username}/{base_dir}/{base_name}"
files = glob.glob(search_path)
if not files:
return None, f"No {ext} file found for device '{device}'"
try:
return class_loader(files[0]), None
except (IOError, OSError) as e:
return None, f"Failed to read device file: {e}"
except (ValueError, TypeError, AttributeError) as e:
return None, f"Failed to parse device file: {e}"
def prepare_request_data(headers, cookies, json_data, challenge, is_widevine):
"""Prepare headers, cookies, and json_data for the license request."""
try:
format_headers = ast.literal_eval(headers) if headers else None
except (ValueError, SyntaxError) as e:
raise ValueError(f"Invalid headers format: {e}") from e
try:
format_cookies = ast.literal_eval(cookies) if cookies else None
except (ValueError, SyntaxError) as e:
raise ValueError(f"Invalid cookies format: {e}") from e
format_json_data = None
if json_data and not is_base64(json_data):
try:
format_json_data = ast.literal_eval(json_data)
if is_widevine:
format_json_data = find_license_challenge(
data=format_json_data,
new_value=base64.b64encode(challenge).decode(),
)
except (ValueError, SyntaxError) as e:
raise ValueError(f"Invalid json_data format: {e}") from e
except (TypeError, AttributeError) as e:
raise ValueError(f"Error processing json_data: {e}") from e
return format_headers, format_cookies, format_json_data
def send_license_request(license_url, headers, cookies, json_data, challenge, proxies):
"""Send the license request and return the response."""
try:
response = requests.post(
url=license_url,
headers=headers,
proxies=proxies,
cookies=cookies,
json=json_data if json_data is not None else None,
data=challenge if json_data is None else None,
timeout=10,
)
return response, None
except ConnectionError as error:
return None, f"Connection error: {error}"
except Timeout as error:
return None, f"Request timeout: {error}"
except RequestException as error:
return None, f"Request error: {error}"
def extract_and_cache_keys(
cdm,
session_id,
cache_to_db,
pssh,
license_url,
headers,
cookies,
challenge,
json_data,
is_widevine,
):
"""Extract keys from the session and cache them."""
returned_keys = ""
try:
keys = list(cdm.get_keys(session_id))
for index, key in enumerate(keys):
# Widevine: key.type, PlayReady: key.key_type
key_type = getattr(key, "type", getattr(key, "key_type", None))
kid = getattr(key, "kid", getattr(key, "key_id", None))
if key_type != "SIGNING" and kid is not None:
cache_to_db(
pssh=pssh,
license_url=license_url,
headers=headers,
cookies=cookies,
data=challenge if json_data is None else json_data,
kid=kid.hex,
key=key.key.hex(),
)
if index != len(keys) - 1:
returned_keys += f"{kid.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{kid.hex}:{key.key.hex()}"
return returned_keys, None
except AttributeError as error:
return None, f"Error accessing CDM keys: {error}"
except (TypeError, ValueError) as error:
return None, f"Error processing keys: {error}"
def api_decrypt(
pssh: str = "",
license_url: str = "",
proxy: str = "",
headers: str = "",
cookies: str = "",
json_data: str = "",
device: str = "public",
username: str = "",
):
"""Decrypt the license using the API."""
print(f"Using device {device} for user {username}")
with open(f"{os.getcwd()}/configs/config.yaml", "r", encoding="utf-8") as file:
def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, headers: str = None, cookies: str = None, json_data: str = None, device: str = 'public', username: str = None):
print(f'Using device {device} for user {username}')
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if pssh == "":
return {"status": "error", "message": "No PSSH provided"}
# Detect PlayReady or Widevine
try:
is_pr = "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh)
except (binascii.Error, TypeError) as error:
if config['database_type'].lower() == 'sqlite':
from custom_functions.database.cache_to_db_sqlite import cache_to_db
elif config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import cache_to_db
if pssh is None:
return {
"status": "error",
"message": f"An error occurred processing PSSH\n\n{error}",
'status': 'error',
'message': 'No PSSH provided'
}
device_type = "PR" if is_pr else "WV"
cdm_class = playreadyCdm if is_pr else widevineCdm
pssh_class = playreadyPSSH if is_pr else widevinePSSH
# Load device
device_obj, device_err = load_device(device_type, device, username, config)
if device_obj is None:
return {"status": "error", "message": device_err}
# Create CDM
try:
cdm = cdm_class.from_device(device_obj)
except (IOError, ValueError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred loading {device_type} CDM\n\n{error}",
}
# Open session
try:
session_id = cdm.open()
except (IOError, ValueError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred opening a CDM session\n\n{error}",
}
# Parse PSSH and get challenge
try:
pssh_obj = pssh_class(pssh)
if is_pr:
challenge = cdm.get_license_challenge(session_id, pssh_obj.wrm_headers[0])
else:
challenge = cdm.get_license_challenge(session_id, pssh_obj)
except (ValueError, AttributeError, IndexError) as error:
return {
"status": "error",
"message": f"An error occurred getting license challenge\n\n{error}",
}
# Prepare request data
try:
format_headers, format_cookies, format_json_data = prepare_request_data(
headers, cookies, json_data, challenge, is_widevine=(not is_pr)
)
except (ValueError, SyntaxError) as error:
return {
"status": "error",
"message": f"An error occurred preparing request data\n\n{error}",
}
# Prepare proxies
proxies = ""
if proxy != "":
is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url:
proxies = {"http": proxy, "https": proxy}
else:
return {
"status": "error",
"message": "Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port",
}
# Send license request
licence, req_err = send_license_request(
license_url,
format_headers,
format_cookies,
format_json_data,
challenge,
proxies,
)
if licence is None:
return {"status": "error", "message": req_err}
# Parse license
try:
if is_pr:
cdm.parse_license(session_id, licence.text)
else:
if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
try:
cdm.parse_license(session_id, licence.content) # type: ignore[arg-type]
except (ValueError, TypeError):
# Try to extract license from JSON
try:
license_json = licence.json()
license_value = find_license_key(license_json)
if license_value is not None:
cdm.parse_license(session_id, license_value)
pr_pssh = playreadyPSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
try:
if device == 'public':
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
base_name += ".prd"
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
else:
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
if prd_files:
pr_device = playreadyDevice.load(prd_files[0])
else:
return {
"status": "error",
"message": f"Could not extract license from JSON: {license_json}",
'status': 'error',
'message': 'No default .prd file found'
}
except (ValueError, json.JSONDecodeError, AttributeError) as error:
else:
base_name = device
if not base_name.endswith(".prd"):
base_name += ".prd"
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
else:
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
if prd_files:
pr_device = playreadyDevice.load(prd_files[0])
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred location PlayReady CDM file\n\n{error}'
}
try:
pr_cdm = playreadyCdm.from_device(pr_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading PlayReady CDM\n\n{error}'
}
try:
pr_session_id = pr_cdm.open()
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred opening a CDM session\n\n{error}'
}
try:
pr_challenge = pr_cdm.get_license_challenge(pr_session_id, pr_pssh.wrm_headers[0])
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting license challenge\n\n{error}'
}
try:
if headers:
format_headers = ast.literal_eval(headers)
else:
format_headers = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting headers\n\n{error}'
}
try:
if cookies:
format_cookies = ast.literal_eval(cookies)
else:
format_cookies = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting cookies\n\n{error}'
}
try:
if json_data and not is_base64(json_data):
format_json_data = ast.literal_eval(json_data)
else:
format_json_data = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting json_data\n\n{error}'
}
licence = None
proxies = None
if proxy is not None:
is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url:
proxies = {'http': proxy, 'https': proxy}
else:
return {
"status": "error",
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
'status': 'error',
'message': f'Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port'
}
except (ValueError, TypeError, AttributeError) as error:
try:
licence = requests.post(
url=license_url,
headers=format_headers,
proxies=proxies,
cookies=format_cookies,
json=format_json_data if format_json_data is not None else None,
data=pr_challenge if format_json_data is None else None
)
except requests.exceptions.ConnectionError as error:
return {
'status': 'error',
'message': f'An error occurred sending license challenge through your proxy\n\n{error}'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
}
try:
pr_cdm.parse_license(pr_session_id, licence.text)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
}
returned_keys = ""
try:
keys = list(pr_cdm.get_keys(pr_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
}
try:
for index, key in enumerate(keys):
if key.key_type != 'SIGNING':
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies,
data=pr_challenge if json_data is None else json_data, kid=key.key_id.hex,
key=key.key.hex())
if index != len(keys) - 1:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{key.key_id.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
}
try:
pr_cdm.close(pr_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
}
try:
return {
'status': 'success',
'message': returned_keys
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
}
except Exception as error:
return {
"status": "error",
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
# Extract and cache keys
returned_keys, key_err = extract_and_cache_keys(
cdm,
session_id,
cache_to_db,
pssh,
license_url,
headers,
cookies,
challenge,
json_data,
is_widevine=(not is_pr),
)
if returned_keys == "":
return {"status": "error", "message": key_err}
# Close session
try:
cdm.close(session_id)
except (IOError, ValueError, AttributeError) as error:
return {
"status": "error",
"message": f"An error occurred closing session\n\n{error}",
}
return {"status": "success", "message": returned_keys}
else:
try:
wv_pssh = widevinePSSH(pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred processing PSSH\n\n{error}'
}
try:
if device == 'public':
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
if wvd_files:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': 'No default .wvd file found'
}
else:
base_name = device
if not base_name.endswith(".wvd"):
base_name += ".wvd"
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
if wvd_files:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred location Widevine CDM file\n\n{error}'
}
try:
wv_cdm = widevineCdm.from_device(wv_device)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred loading Widevine CDM\n\n{error}'
}
try:
wv_session_id = wv_cdm.open()
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred opening a CDM session\n\n{error}'
}
try:
wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting license challenge\n\n{error}'
}
try:
if headers:
format_headers = ast.literal_eval(headers)
else:
format_headers = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting headers\n\n{error}'
}
try:
if cookies:
format_cookies = ast.literal_eval(cookies)
else:
format_cookies = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting cookies\n\n{error}'
}
try:
if json_data and not is_base64(json_data):
format_json_data = ast.literal_eval(json_data)
format_json_data = find_license_challenge(data=format_json_data, new_value=base64.b64encode(wv_challenge).decode())
else:
format_json_data = None
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting json_data\n\n{error}'
}
licence = None
proxies = None
if proxy is not None:
is_url, protocol, fqdn = is_url_and_split(proxy)
if is_url:
proxies = {'http': proxy, 'https': proxy}
try:
licence = requests.post(
url=license_url,
headers=format_headers,
proxies=proxies,
cookies=format_cookies,
json=format_json_data if format_json_data is not None else None,
data=wv_challenge if format_json_data is None else None
)
except requests.exceptions.ConnectionError as error:
return {
'status': 'error',
'message': f'An error occurred sending license challenge through your proxy\n\n{error}'
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
}
try:
wv_cdm.parse_license(wv_session_id, licence.content)
except:
try:
license_json = licence.json()
license_value = find_license_key(license_json)
wv_cdm.parse_license(wv_session_id, license_value)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
}
returned_keys = ""
try:
keys = list(wv_cdm.get_keys(wv_session_id))
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting keys\n\n{error}'
}
try:
for index, key in enumerate(keys):
if key.type != 'SIGNING':
cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, data=wv_challenge if json_data is None else json_data, kid=key.kid.hex, key=key.key.hex())
if index != len(keys) - 1:
returned_keys += f"{key.kid.hex}:{key.key.hex()}\n"
else:
returned_keys += f"{key.kid.hex}:{key.key.hex()}"
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred formatting keys\n\n{error}'
}
try:
wv_cdm.close(wv_session_id)
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred closing session\n\n{error}'
}
try:
return {
'status': 'success',
'message': returned_keys
}
except Exception as error:
return {
'status': 'error',
'message': f'An error occurred getting returned_keys\n\n{error}'
}

View File

@ -1,106 +1,38 @@
"""Module to check for and download CDM files."""
import os
import sys
import yaml
import requests
CONFIG_PATH = os.path.join(os.getcwd(), "configs", "config.yaml")
def load_config():
"""Load the config file."""
with open(CONFIG_PATH, "r", encoding="utf-8") as file:
return yaml.safe_load(file)
def save_config(config):
"""Save the config file."""
with open(CONFIG_PATH, "w", encoding="utf-8") as file:
yaml.dump(config, file)
def prompt_yes_no(message):
"""Prompt the user for a yes or no answer."""
answer = " "
while answer[0].upper() not in ["Y", "N"]:
answer = input(message)
return answer[0].upper() == "Y"
def check_for_cdm(config_key, file_ext, download_url, cdm_dir, cdm_name):
"""Check for a CDM file."""
config = load_config()
cdm_value = config.get(config_key, "")
cdm_dir_path = os.path.join(os.getcwd(), "configs", "CDMs", cdm_dir)
os.makedirs(cdm_dir_path, exist_ok=True)
if not cdm_value:
if prompt_yes_no(
f"No default {cdm_name} CDM specified, would you like to download one "
"from The CDM Project? (Y)es / (N)o: "
):
response = requests.get(download_url, timeout=10)
if response.status_code == 200:
file_path = os.path.join(cdm_dir_path, f"public.{file_ext}")
with open(file_path, "wb") as file:
file.write(response.content)
config[config_key] = "public"
save_config(config)
print(f"Successfully downloaded {cdm_name} CDM")
else:
sys.exit(
f"Download failed, please try again, or place a .{file_ext} file "
f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
else:
sys.exit(
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
def check_for_wvd_cdm():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['default_wv_cdm'] == '':
exit(f"Please put the name of your Widevine CDM inside of {os.getcwd()}/configs/config.yaml")
else:
base_name = (
cdm_value
if cdm_value.endswith(f".{file_ext}")
else f"{cdm_value}.{file_ext}"
)
file_path = os.path.join(cdm_dir_path, base_name)
if os.path.exists(file_path):
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name += ".wvd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/WV/{base_name}'):
return
# Prompt to download if file is missing, even if config has a value
if prompt_yes_no(
f"{cdm_name} CDM {base_name} does not exist in {cdm_dir_path}. Would you like to download it from The CDM Project? (Y)es/(N)o: "
):
response = requests.get(download_url, timeout=10)
if response.status_code == 200:
with open(file_path, "wb") as file:
file.write(response.content)
config[config_key] = base_name.replace(f".{file_ext}", "")
save_config(config)
print(f"Successfully downloaded {cdm_name} CDM")
else:
sys.exit(
f"Download failed, please try again, or place a .{file_ext} file "
f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
else:
sys.exit(
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
)
exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
def check_for_prd_cdm():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['default_pr_cdm'] == '':
exit(f"Please put the name of your PlayReady CDM inside of {os.getcwd()}/configs/config.yaml")
else:
base_name = config["default_pr_cdm"]
if not base_name.endswith(".prd"):
base_name += ".prd"
if os.path.exists(f'{os.getcwd()}/configs/CDMs/PR/{base_name}'):
return
else:
exit(f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
def check_for_cdms():
"""Check for CDM files."""
check_for_cdm(
config_key="default_wv_cdm",
file_ext="wvd",
download_url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd",
cdm_dir="WV",
cdm_name="Widevine",
)
check_for_cdm(
config_key="default_pr_cdm",
file_ext="prd",
download_url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd",
cdm_dir="PR",
cdm_name="PlayReady",
)
check_for_wvd_cdm()
check_for_prd_cdm()

View File

@ -1,13 +1,10 @@
"""Module to check for the config file."""
import os
def check_for_config_file():
"""Check for the config file."""
if os.path.exists(os.path.join(os.getcwd(), "configs", "config.yaml")):
if os.path.exists(f'{os.getcwd()}/configs/config.yaml'):
return
default_config = """
else:
default_config = """\
default_wv_cdm: ''
default_pr_cdm: ''
secret_key_flask: 'secretkey'
@ -24,8 +21,6 @@ remote_cdm_secret: ''
# port: ''
# database: ''
"""
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "w", encoding="utf-8"
) as f:
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as f:
f.write(default_config)
return
return

View File

@ -1,159 +1,37 @@
"""Module to check for the database with unified backend support."""
import os
from typing import Dict, Any
import yaml
from custom_functions.database.unified_db_ops import (
db_ops,
get_backend_info,
key_count,
)
from custom_functions.database.user_db import create_user_database
def get_database_config() -> Dict[str, Any]:
"""Get the database configuration from config.yaml."""
try:
config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config
except (FileNotFoundError, KeyError, yaml.YAMLError) as e:
print(f"Warning: Could not load config.yaml: {e}")
return {"database_type": "sqlite"} # Default fallback
def check_for_sqlite_database() -> None:
"""Check for the SQLite database file and create if needed."""
config = get_database_config()
database_type = config.get("database_type", "sqlite").lower()
# Only check for SQLite file if we're using SQLite
if database_type == "sqlite":
sqlite_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
if not os.path.exists(sqlite_path):
print("SQLite database not found, creating...")
# Ensure directory exists
os.makedirs(os.path.dirname(sqlite_path), exist_ok=True)
db_ops.create_database()
print(f"SQLite database created at: {sqlite_path}")
else:
print(f"SQLite database found at: {sqlite_path}")
def check_for_mariadb_database() -> None:
"""Check for the MariaDB database and create if needed."""
config = get_database_config()
database_type = config.get("database_type", "sqlite").lower()
# Only check MariaDB if we're using MariaDB
if database_type == "mariadb":
try:
print("Checking MariaDB connection and creating database if needed...")
db_ops.create_database()
print("MariaDB database check completed successfully")
except Exception as e:
print(f"Error checking/creating MariaDB database: {e}")
print("Falling back to SQLite...")
# Fallback to SQLite if MariaDB fails
fallback_config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
try:
with open(fallback_config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
config["database_type"] = "sqlite"
with open(fallback_config_path, "w", encoding="utf-8") as file:
yaml.safe_dump(config, file)
check_for_sqlite_database()
except Exception as fallback_error:
print(f"Error during fallback to SQLite: {fallback_error}")
def check_for_user_database() -> None:
"""Check for the user database and create if needed."""
user_db_path = os.path.join(os.getcwd(), "databases", "users.db")
if not os.path.exists(user_db_path):
print("User database not found, creating...")
# Ensure directory exists
os.makedirs(os.path.dirname(user_db_path), exist_ok=True)
create_user_database()
print(f"User database created at: {user_db_path}")
def check_for_sqlite_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
return
else:
print(f"User database found at: {user_db_path}")
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import create_database
create_database()
return
else:
return
def check_for_user_database():
if os.path.exists(f'{os.getcwd()}/databases/users.db'):
return
else:
from custom_functions.database.user_db import create_user_database
create_user_database()
def check_for_sql_database() -> None:
"""Check for the SQL database based on configuration."""
print("=== Database Check Starting ===")
def check_for_mariadb_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import create_database
create_database()
return
else:
return
# Get backend information
backend_info = get_backend_info()
print(f"Database backend: {backend_info['backend']}")
print(f"Using module: {backend_info['module']}")
config = get_database_config()
database_type = config.get("database_type", "sqlite").lower()
# Ensure databases directory exists
os.makedirs(os.path.join(os.getcwd(), "databases"), exist_ok=True)
os.makedirs(os.path.join(os.getcwd(), "databases", "sql"), exist_ok=True)
# Check main database based on type
if database_type == "mariadb":
check_for_mariadb_database()
else: # Default to SQLite
check_for_sqlite_database()
# Always check user database (always SQLite)
check_for_user_database()
print("=== Database Check Completed ===")
def get_database_status() -> Dict[str, Any]:
"""Get the current database status and configuration."""
config = get_database_config()
backend_info = get_backend_info()
status = {
"configured_backend": config.get("database_type", "sqlite").lower(),
"active_backend": backend_info["backend"],
"module_in_use": backend_info["module"],
"sqlite_file_exists": os.path.exists(
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
),
"user_db_exists": os.path.exists(
os.path.join(os.getcwd(), "databases", "users.db")
),
}
# Try to get key count to verify database is working
try:
status["key_count"] = key_count()
status["database_operational"] = True
except Exception as e:
status["key_count"] = "Error"
status["database_operational"] = False
status["error"] = str(e)
return status
def print_database_status() -> None:
"""Print a formatted database status report."""
status = get_database_status()
print("\n=== Database Status Report ===")
print(f"Configured Backend: {status['configured_backend']}")
print(f"Active Backend: {status['active_backend']}")
print(f"Module in Use: {status['module_in_use']}")
print(f"SQLite File Exists: {status['sqlite_file_exists']}")
print(f"User DB Exists: {status['user_db_exists']}")
print(f"Database Operational: {status['database_operational']}")
print(f"Key Count: {status['key_count']}")
if not status["database_operational"]:
print(f"Error: {status.get('error', 'Unknown error')}")
print("==============================\n")
def check_for_sql_database():
check_for_sqlite_database()
check_for_mariadb_database()
check_for_user_database()

View File

@ -1,53 +1,44 @@
"""Module to check for the folders."""
import os
def check_for_config_folder():
"""Check for the config folder."""
if os.path.isdir(os.path.join(os.getcwd(), "configs")):
if os.path.isdir(f'{os.getcwd()}/configs'):
return
else:
os.mkdir(f'{os.getcwd()}/configs')
return
os.mkdir(os.path.join(os.getcwd(), "configs"))
return
def check_for_database_folder():
"""Check for the database folder."""
if os.path.isdir(os.path.join(os.getcwd(), "databases")):
if os.path.isdir(f'{os.getcwd()}/databases'):
return
else:
os.mkdir(f'{os.getcwd()}/databases')
os.mkdir(f'{os.getcwd()}/databases/sql')
return
os.mkdir(os.path.join(os.getcwd(), "databases"))
os.mkdir(os.path.join(os.getcwd(), "databases", "sql"))
return
def check_for_cdm_folder():
"""Check for the CDM folder."""
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs")):
if os.path.isdir(f'{os.getcwd()}/configs/CDMs'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs')
return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs"))
return
def check_for_wv_cdm_folder():
"""Check for the Widevine CDM folder."""
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV")):
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV"))
return
def check_for_cdm_pr_folder():
"""Check for the PlayReady CDM folder."""
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR")):
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'):
return
else:
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
return
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR"))
return
def folder_checks():
"""Check for the folders."""
check_for_config_folder()
check_for_database_folder()
check_for_cdm_folder()
check_for_wv_cdm_folder()
check_for_cdm_pr_folder()
check_for_cdm_pr_folder()

View File

@ -1,29 +1,11 @@
"""Module to run the prechecks."""
import os
import subprocess
from custom_functions.prechecks.folder_checks import folder_checks
from custom_functions.prechecks.config_file_checks import check_for_config_file
from custom_functions.prechecks.database_checks import check_for_sql_database
from custom_functions.prechecks.cdm_checks import check_for_cdms
def check_frontend_built():
"""Check if the frontend is built; if not, run build.py."""
frontend_dist = os.path.join(os.getcwd(), "frontend-dist")
frontend_dist = os.path.abspath(frontend_dist)
if not os.path.exists(frontend_dist) or not os.listdir(frontend_dist):
print("Frontend has not been built. Running build.py...")
subprocess.run(["python", "build.py"], check=True)
else:
print("Frontend build found.")
def run_precheck():
"""Run the prechecks."""
check_frontend_built()
folder_checks()
check_for_config_file()
check_for_cdms()
check_for_sql_database()
return

View File

@ -1,52 +1,49 @@
"""Module to check for the Python version and environment."""
import sys
import os
import subprocess
import venv
import importlib.util
def version_check():
"""Check for the Python version."""
if sys.version_info < (3, 12):
sys.exit("Python version 3.12 or higher is required")
major_version = sys.version_info.major
minor_version = sys.version_info.minor
if major_version >= 3:
if minor_version >= 12:
return
else:
exit("Python version 3.12 or higher is required")
else:
exit("Python 2 detected, Python version 3.12 or higher is required")
def pip_check():
"""Check for the pip installation."""
if importlib.util.find_spec("pip") is None:
sys.exit("Pip is not installed")
try:
import pip
return
except ImportError:
exit("Pip is not installed")
def venv_check():
"""Check for the virtual environment."""
if hasattr(sys, "real_prefix") or (
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
):
# Check if we're already inside a virtual environment
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
return
venv_path = os.path.join(os.getcwd(), "cdrm-venv")
venv_python = (
os.path.join(venv_path, "bin", "python")
if os.name != "nt"
else os.path.join(venv_path, "Scripts", "python.exe")
)
venv_path = os.path.join(os.getcwd(), 'cdrm-venv')
venv_python = os.path.join(venv_path, 'bin', 'python') if os.name != 'nt' else os.path.join(venv_path, 'Scripts', 'python.exe')
# If venv already exists, restart script using its Python
if os.path.exists(venv_path):
subprocess.call([venv_python] + sys.argv)
sys.exit()
answer = (
input(
"Program is not running from a virtual environment. To maintain "
"compatibility, this program must be run from one.\n"
"Would you like to create one? (Y/N): "
# Ask user for permission to create a virtual environment
answer = ''
while not answer or answer[0].upper() not in {'Y', 'N'}:
answer = input(
'Program is not running from a venv. To maintain compatibility and dependencies, this program must be run from one.\n'
'Would you like me to create one for you? (Y/N): '
)
.strip()
.upper()
)
if answer.startswith("Y"):
if answer[0].upper() == 'Y':
print("Creating virtual environment...")
venv.create(venv_path, with_pip=True)
subprocess.call([venv_python] + sys.argv)
@ -57,57 +54,32 @@ def venv_check():
def requirements_check():
"""Check for the requirements."""
required_packages = [
"pywidevine",
"pyplayready",
"flask",
"flask_cors",
"yaml",
"mysql.connector",
]
missing = []
for pkg in required_packages:
if "." in pkg:
parent, _ = pkg.split(".", 1)
if (
importlib.util.find_spec(parent) is None
or importlib.util.find_spec(pkg) is None
):
missing.append(pkg)
else:
if importlib.util.find_spec(pkg) is None:
missing.append(pkg)
if not missing:
try:
import pywidevine
import pyplayready
import flask
import flask_cors
import yaml
import mysql.connector
return
while True:
user_input = (
input(
f"Missing packages: {', '.join(missing)}. Do you want to install them? (Y/N): "
)
.strip()
.upper()
)
if user_input == "Y":
print("Installing packages from requirements.txt...")
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]
)
print("Installation complete.")
break
if user_input == "N":
print("Dependencies required, please install them and run again.")
sys.exit()
else:
print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
except ImportError:
while True:
user_input = input("Missing packages. Do you want to install them? (Y/N): ").strip().upper()
if user_input == 'Y':
print("Installing packages from requirements.txt...")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
print("Installation complete.")
break
elif user_input == 'N':
print("Dependencies required, please install them and run again.")
sys.exit()
else:
print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
def run_python_checks():
"""Run the Python checks."""
if getattr(sys, "frozen", False): # Check if running from PyInstaller
if getattr(sys, 'frozen', False): # Check if running from PyInstaller
return
version_check()
pip_check()
venv_check()
requirements_check()
requirements_check()

View File

@ -1,20 +1,12 @@
"""Module to check if the user is allowed to use the device."""
import os
import glob
def user_allowed_to_use_device(device, username):
"""Check if the user is allowed to use the device."""
base_path = os.path.join(os.getcwd(), "configs", "CDMs", username)
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
# Get filenames with extensions
pr_files = [
os.path.basename(f) for f in glob.glob(os.path.join(base_path, "PR", "*.prd"))
]
wv_files = [
os.path.basename(f) for f in glob.glob(os.path.join(base_path, "WV", "*.wvd"))
]
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
# Combine all filenames
all_files = pr_files + wv_files
@ -22,4 +14,4 @@ def user_allowed_to_use_device(device, username):
# Check if filename matches directly or by adding extensions
possible_names = {device, f"{device}.prd", f"{device}.wvd"}
return any(name in all_files for name in possible_names)
return any(name in all_files for name in possible_names)

30
main.py
View File

@ -1,10 +1,9 @@
"""Main module to run the application."""
import os
import yaml
from custom_functions.prechecks.python_checks import run_python_checks
run_python_checks()
from custom_functions.prechecks.precheck import run_precheck
run_precheck()
from flask import Flask
from flask_cors import CORS
from routes.react import react_bp
from routes.api import api_bp
from routes.remote_device_wv import remotecdm_wv_bp
@ -13,20 +12,12 @@ from routes.upload import upload_bp
from routes.user_info import user_info_bp
from routes.register import register_bp
from routes.login import login_bp
from routes.user_changes import user_change_bp
from custom_functions.prechecks.python_checks import run_python_checks
from custom_functions.prechecks.precheck import run_precheck
run_python_checks()
run_precheck()
import os
import yaml
app = Flask(__name__)
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
app.secret_key = config["secret_key_flask"]
app.secret_key = config['secret_key_flask']
CORS(app)
@ -39,7 +30,6 @@ app.register_blueprint(user_info_bp)
app.register_blueprint(upload_bp)
app.register_blueprint(remotecdm_wv_bp)
app.register_blueprint(remotecdm_pr_bp)
app.register_blueprint(user_change_bp)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')

View File

@ -1,19 +0,0 @@
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| cdrm-frontend
)/
'''

View File

@ -1,10 +1,9 @@
Flask
Flask~=3.1.0
Flask-Cors
pywidevine~=1.8.0
pyplayready~=0.6.0
requests
requests~=2.32.3
protobuf~=4.25.6
PyYAML
PyYAML~=6.0.2
mysql-connector-python
bcrypt
black
bcrypt

View File

@ -1,140 +1,113 @@
"""Module to handle the API routes."""
import os
import sqlite3
from flask import Blueprint, jsonify, request, send_file, session
import json
from custom_functions.decrypt.api_decrypt import api_decrypt
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
import shutil
import math
import yaml
import mysql.connector
from io import StringIO
import tempfile
import time
from flask import Blueprint, jsonify, request, send_file, session, after_this_request
import yaml
import mysql.connector
from custom_functions.decrypt.api_decrypt import api_decrypt
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
from custom_functions.database.unified_db_ops import (
search_by_pssh_or_kid,
cache_to_db,
get_key_by_kid_and_service,
get_unique_services,
get_kid_key_dict,
key_count,
)
from configs.icon_links import data as icon_data
api_bp = Blueprint("api", __name__)
with open(os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8") as file:
api_bp = Blueprint('api', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import search_by_pssh_or_kid, cache_to_db, \
get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count
elif config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import search_by_pssh_or_kid, cache_to_db, \
get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count
def get_db_config():
"""Get the MariaDB database configuration."""
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file_mariadb:
config_mariadb = yaml.safe_load(file_mariadb)
# Configure your MariaDB connection
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
db_config = {
"host": f'{config_mariadb["mariadb"]["host"]}',
"user": f'{config_mariadb["mariadb"]["user"]}',
"password": f'{config_mariadb["mariadb"]["password"]}',
"database": f'{config_mariadb["mariadb"]["database"]}',
'host': f'{config["mariadb"]["host"]}',
'user': f'{config["mariadb"]["user"]}',
'password': f'{config["mariadb"]["password"]}',
'database': f'{config["mariadb"]["database"]}'
}
return db_config
@api_bp.route("/api/cache/search", methods=["POST"])
@api_bp.route('/api/cache/search', methods=['POST'])
def get_data():
"""Get the data from the database."""
search_argument = json.loads(request.data)["input"]
search_argument = json.loads(request.data)['input']
results = search_by_pssh_or_kid(search_filter=search_argument)
return jsonify(results)
@api_bp.route("/api/cache/<service>/<kid>", methods=["GET"])
@api_bp.route('/api/cache/<service>/<kid>', methods=['GET'])
def get_single_key_service(service, kid):
"""Get the single key from the database."""
result = get_key_by_kid_and_service(kid=kid, service=service)
return jsonify(
{
"code": 0,
"content_key": result,
}
)
return jsonify({
'code': 0,
'content_key': result,
})
@api_bp.route("/api/cache/<service>", methods=["GET"])
@api_bp.route('/api/cache/<service>', methods=['GET'])
def get_multiple_key_service(service):
"""Get the multiple keys from the database."""
result = get_kid_key_dict(service_name=service)
pages = math.ceil(len(result) / 10)
return jsonify({"code": 0, "content_keys": result, "pages": pages})
return jsonify({
'code': 0,
'content_keys': result,
'pages': pages
})
@api_bp.route("/api/cache/<service>/<kid>", methods=["POST"])
@api_bp.route('/api/cache/<service>/<kid>', methods=['POST'])
def add_single_key_service(service, kid):
"""Add the single key to the database."""
body = request.get_json()
content_key = body["content_key"]
content_key = body['content_key']
result = cache_to_db(service=service, kid=kid, key=content_key)
if result:
return jsonify(
{
"code": 0,
"updated": True,
}
)
return jsonify(
{
"code": 0,
"updated": True,
}
)
return jsonify({
'code': 0,
'updated': True,
})
elif result is False:
return jsonify({
'code': 0,
'updated': True,
})
@api_bp.route("/api/cache/<service>", methods=["POST"])
@api_bp.route('/api/cache/<service>', methods=['POST'])
def add_multiple_key_service(service):
"""Add the multiple keys to the database."""
body = request.get_json()
keys_added = 0
keys_updated = 0
for kid, key in body["content_keys"].items():
for kid, key in body['content_keys'].items():
result = cache_to_db(service=service, kid=kid, key=key)
if result is True:
keys_updated += 1
else:
elif result is False:
keys_added += 1
return jsonify(
{
"code": 0,
"added": str(keys_added),
"updated": str(keys_updated),
}
)
return jsonify({
'code': 0,
'added': str(keys_added),
'updated': str(keys_updated),
})
@api_bp.route("/api/cache", methods=["POST"])
@api_bp.route('/api/cache', methods=['POST'])
def unique_service():
"""Get the unique services from the database."""
services = get_unique_services()
return jsonify(
{
"code": 0,
"service_list": services,
}
)
return jsonify({
'code': 0,
'service_list': services,
})
@api_bp.route("/api/cache/download", methods=["GET"])
@api_bp.route('/api/cache/download', methods=['GET'])
def download_database():
"""Download the database."""
if config["database_type"].lower() != "mariadb":
original_database_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
if config['database_type'].lower() != 'mariadb':
original_database_path = f'{os.getcwd()}/databases/sql/key_cache.db'
# Make a copy of the original database (without locking the original)
modified_database_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache_modified.db")
modified_database_path = f'{os.getcwd()}/databases/sql/key_cache_modified.db'
# Using shutil.copy2 to preserve metadata (timestamps, etc.)
shutil.copy2(original_database_path, modified_database_path)
@ -144,156 +117,145 @@ def download_database():
cursor = conn.cursor()
# Update all rows to remove Headers and Cookies (set them to NULL or empty strings)
cursor.execute(
"""
cursor.execute('''
UPDATE licenses
SET Headers = NULL,
Cookies = NULL
"""
)
''')
# No need for explicit commit, it's done automatically with the 'with' block
# The connection will automatically be committed and closed when the block ends
# Send the modified database as an attachment
return send_file(
modified_database_path, as_attachment=True, download_name="key_cache.db"
)
try:
conn = mysql.connector.connect(**get_db_config())
cursor = conn.cursor()
return send_file(modified_database_path, as_attachment=True, download_name='key_cache.db')
if config['database_type'].lower() == 'mariadb':
try:
# Connect to MariaDB
conn = mysql.connector.connect(**get_db_config())
cursor = conn.cursor()
# Get column names
cursor.execute("SHOW COLUMNS FROM licenses")
columns = [row[0] for row in cursor.fetchall()]
# Update sensitive data (this updates the live DB, you may want to duplicate rows instead)
cursor.execute('''
UPDATE licenses
SET Headers = NULL,
Cookies = NULL
''')
# Build SELECT with Headers and Cookies as NULL
select_columns = []
for col in columns:
if col.lower() in ("headers", "cookies"):
select_columns.append("NULL AS " + col)
else:
select_columns.append(col)
select_query = f"SELECT {', '.join(select_columns)} FROM licenses"
cursor.execute(select_query)
rows = cursor.fetchall()
conn.commit()
# Dump to SQL-like format
output = StringIO()
output.write("-- Dump of `licenses` table (Headers and Cookies are NULL)\n")
for row in rows:
values = ", ".join(
f"'{str(v).replace('\'', '\\\'')}'" if v is not None else "NULL"
for v in row
)
output.write(
f"INSERT INTO licenses ({', '.join(columns)}) VALUES ({values});\n"
)
# Now export the table
cursor.execute('SELECT * FROM licenses')
rows = cursor.fetchall()
column_names = [desc[0] for desc in cursor.description]
# Write to a temp file for download
temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, "key_cache.sql")
with open(temp_path, "w", encoding="utf-8") as f:
f.write(output.getvalue())
# Dump to SQL-like format
output = StringIO()
output.write(f"-- Dump of `licenses` table\n")
for row in rows:
values = ', '.join(f"'{str(v).replace('\'', '\\\'')}'" if v is not None else 'NULL' for v in row)
output.write(f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n")
@after_this_request
def remove_file(response):
try:
os.remove(temp_path)
except Exception:
pass
return response
# Write to a temp file for download
temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, 'key_cache.sql')
with open(temp_path, 'w', encoding='utf-8') as f:
f.write(output.getvalue())
return send_file(
temp_path, as_attachment=True, download_name="licenses_dump.sql"
)
except mysql.connector.Error as err:
return {"error": str(err)}, 500
return send_file(temp_path, as_attachment=True, download_name='licenses_dump.sql')
except mysql.connector.Error as err:
return {"error": str(err)}, 500
_keycount_cache = {
'count': None,
'timestamp': 0
}
_keycount_cache = {"count": None, "timestamp": 0}
@api_bp.route("/api/cache/keycount", methods=["GET"])
@api_bp.route('/api/cache/keycount', methods=['GET'])
def get_count():
"""Get the count of the keys in the database."""
now = time.time()
if now - _keycount_cache["timestamp"] > 10 or _keycount_cache["count"] is None:
_keycount_cache["count"] = key_count()
_keycount_cache["timestamp"] = now
return jsonify({"count": _keycount_cache["count"]})
if now - _keycount_cache['timestamp'] > 10 or _keycount_cache['count'] is None:
_keycount_cache['count'] = key_count()
_keycount_cache['timestamp'] = now
return jsonify({
'count': _keycount_cache['count']
})
@api_bp.route("/api/decrypt", methods=["POST"])
@api_bp.route('/api/decrypt', methods=['POST'])
def decrypt_data():
"""Decrypt the data."""
api_request_data = request.get_json(force=True)
# Helper to get fields or None if missing/empty
def get_field(key, default=""):
value = api_request_data.get(key, default)
return value if value != "" else default
api_request_pssh = get_field("pssh")
api_request_licurl = get_field("licurl")
api_request_proxy = get_field("proxy")
api_request_headers = get_field("headers")
api_request_cookies = get_field("cookies")
api_request_data_func = get_field("data")
# Device logic
device = get_field("device", "public")
if device in [
"default",
"CDRM-Project Public Widevine CDM",
"CDRM-Project Public PlayReady CDM",
"",
None,
]:
api_request_device = "public"
api_request_data = json.loads(request.data)
if 'pssh' in api_request_data:
if api_request_data['pssh'] == '':
api_request_pssh = None
else:
api_request_pssh = api_request_data['pssh']
else:
api_request_device = device
username = ""
if api_request_device != "public":
username = session.get("username")
api_request_pssh = None
if 'licurl' in api_request_data:
if api_request_data['licurl'] == '':
api_request_licurl = None
else:
api_request_licurl = api_request_data['licurl']
else:
api_request_licurl = None
if 'proxy' in api_request_data:
if api_request_data['proxy'] == '':
api_request_proxy = None
else:
api_request_proxy = api_request_data['proxy']
else:
api_request_proxy = None
if 'headers' in api_request_data:
if api_request_data['headers'] == '':
api_request_headers = None
else:
api_request_headers = api_request_data['headers']
else:
api_request_headers = None
if 'cookies' in api_request_data:
if api_request_data['cookies'] == '':
api_request_cookies = None
else:
api_request_cookies = api_request_data['cookies']
else:
api_request_cookies = None
if 'data' in api_request_data:
if api_request_data['data'] == '':
api_request_data_func = None
else:
api_request_data_func = api_request_data['data']
else: api_request_data_func = None
if 'device' in api_request_data:
if api_request_data['device'] == 'default' or api_request_data['device'] == 'CDRM-Project Public Widevine CDM' or api_request_data['device'] == 'CDRM-Project Public PlayReady CDM':
api_request_device = 'public'
else:
api_request_device = api_request_data['device']
else:
api_request_device = 'public'
username = None
if api_request_device != 'public':
username = session.get('username')
if not username:
return jsonify({"message": "Not logged in, not allowed"}), 400
if not user_allowed_to_use_device(device=api_request_device, username=username):
return jsonify({"message": "Not authorized / Not found"}), 403
return jsonify({'message': 'Not logged in, not allowed'}), 400
if user_allowed_to_use_device(device=api_request_device, username=username):
api_request_device = api_request_device
else:
return jsonify({'message': f'Not authorized / Not found'}), 403
result = api_decrypt(pssh=api_request_pssh, proxy=api_request_proxy, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data_func, device=api_request_device, username=username)
if result['status'] == 'success':
return jsonify({
'status': 'success',
'message': result['message']
})
else:
return jsonify({
'status': 'fail',
'message': result['message']
})
result = api_decrypt(
pssh=api_request_pssh,
proxy=api_request_proxy,
license_url=api_request_licurl,
headers=api_request_headers,
cookies=api_request_cookies,
json_data=api_request_data_func,
device=api_request_device,
username=username,
)
if result["status"] == "success":
return jsonify({"status": "success", "message": result["message"]})
return jsonify({"status": "fail", "message": result["message"]})
@api_bp.route("/api/links", methods=["GET"])
@api_bp.route('/api/links', methods=['GET'])
def get_links():
"""Get the links."""
return jsonify(
{
"discord": icon_data["discord"],
"telegram": icon_data["telegram"],
"gitea": icon_data["gitea"],
}
)
@api_bp.route("/api/extension", methods=["POST"])
def verify_extension():
"""Verify the extension."""
return jsonify(
{
"status": True,
}
)
return jsonify({
'discord': icon_data['discord'],
'telegram': icon_data['telegram'],
'gitea': icon_data['gitea'],
})

View File

@ -1,42 +1,37 @@
"""Module to handle the login process."""
from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import verify_user
login_bp = Blueprint(
"login_bp",
'login_bp',
__name__,
)
@login_bp.route("/login", methods=["POST"])
@login_bp.route('/login', methods=['POST'])
def login():
"""Handle the login process."""
data = request.get_json()
for required_field in ["username", "password"]:
if required_field not in data:
return (
jsonify({"error": f"Missing required field: {required_field}"}),
400,
)
if request.method == 'POST':
data = request.get_json()
for required_field in ['username', 'password']:
if required_field not in data:
return jsonify({'error': f'Missing required field: {required_field}'}), 400
if verify_user(data["username"], data["password"]):
session["username"] = data[
"username"
].lower() # Stored securely in a signed cookie
return jsonify({"message": "Successfully logged in!"})
return jsonify({"error": "Invalid username or password!"}), 401
if verify_user(data['username'], data['password']):
session['username'] = data['username'] # Stored securely in a signed cookie
return jsonify({'message': 'Successfully logged in!'})
else:
return jsonify({'error': 'Invalid username or password!'}), 401
@login_bp.route("/login/status", methods=["POST"])
@login_bp.route('/login/status', methods=['POST'])
def login_status():
"""Check if the user is logged in."""
username = session.get("username")
return jsonify({"message": "True" if username else "False"})
try:
username = session.get('username')
if username:
return jsonify({'message': 'True'})
else:
return jsonify({'message': 'False'})
except:
return jsonify({'message': 'False'})
@login_bp.route("/logout", methods=["POST"])
@login_bp.route('/logout', methods=['POST'])
def logout():
"""Logout the user."""
session.pop("username", None)
return jsonify({"message": "Successfully logged out!"})
session.pop('username', None)
return jsonify({'message': 'Successfully logged out!'})

View File

@ -1,48 +1,33 @@
"""Module to handle the React routes."""
import os
import sys
from flask import Blueprint, send_from_directory, render_template
import os
from flask import Blueprint, send_from_directory, request, render_template
from configs import index_tags
if getattr(sys, "frozen", False): # Running as a bundled app
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
if getattr(sys, 'frozen', False): # Running as a bundled app
base_path = sys._MEIPASS
else: # Running in a normal Python environment
base_path = os.path.abspath(".")
static_folder = os.path.join(base_path, "frontend-dist")
static_folder = os.path.join(base_path, 'cdrm-frontend', 'dist')
react_bp = Blueprint(
"react_bp",
'react_bp',
__name__,
static_folder=static_folder,
static_url_path="/",
template_folder=static_folder,
static_url_path='/',
template_folder=static_folder
)
@react_bp.route("/", methods=["GET"])
@react_bp.route("/<path:path>", methods=["GET"])
@react_bp.route("/<path>", methods=["GET"])
def index(path=""):
"""Handle the index route."""
# Ensure static_folder is not None
if react_bp.static_folder is None:
raise ValueError("Static folder is not configured for the blueprint")
# Normalize the path to prevent directory traversal
safe_path = os.path.normpath(path)
file_path = os.path.join(react_bp.static_folder, safe_path)
if path and os.path.exists(file_path):
return send_from_directory(react_bp.static_folder, safe_path)
# Only allow certain paths to render index.html with tags
allowed_paths = ["", "cache", "api", "testplayer", "account"]
if safe_path.lower() in allowed_paths:
data = index_tags.tags.get(safe_path.lower(), index_tags.tags.get("index", {}))
return render_template("index.html", data=data)
# Fallback: serve index.html for all other routes (SPA)
return send_from_directory(react_bp.static_folder, "index.html")
@react_bp.route('/', methods=['GET'])
@react_bp.route('/<path:path>', methods=["GET"])
@react_bp.route('/<path>', methods=["GET"])
def index(path=''):
if request.method == 'GET':
file_path = os.path.join(react_bp.static_folder, path)
if path != "" and os.path.exists(file_path):
return send_from_directory(react_bp.static_folder, path)
elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']:
data = index_tags.tags.get(path.lower(), index_tags.tags['index'])
return render_template('index.html', data=data)
else:
return send_from_directory(react_bp.static_folder, 'index.html')

View File

@ -1,56 +1,29 @@
"""Module to handle the register process."""
import re
import uuid
from flask import Blueprint, request, jsonify
from custom_functions.database.user_db import add_user
register_bp = Blueprint("register_bp", __name__)
register_bp = Blueprint(
'register_bp',
__name__,
)
USERNAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$")
PASSWORD_REGEX = re.compile(r"^\S+$")
@register_bp.route("/register", methods=["POST"])
@register_bp.route('/register', methods=['POST'])
def register():
"""Handle the register process."""
data = request.get_json()
if data is None:
return jsonify({"error": "Invalid or missing JSON in request body."}), 400
# Check required fields
for required_field in ["username", "password"]:
if required_field not in data:
return jsonify({"error": f"Missing required field: {required_field}"}), 400
username = data["username"].lower()
password = data["password"]
api_key = str(uuid.uuid4())
# Length checks
if not (3 <= len(username) <= 32):
return jsonify({"error": "Username must be 3-32 characters."}), 400
if not (8 <= len(password) <= 128):
return jsonify({"error": "Password must be 8-128 characters."}), 400
# Validate username and password
if not USERNAME_REGEX.fullmatch(username):
return (
jsonify(
{
"error": "Invalid username. Only letters, numbers, hyphens, and underscores are allowed."
}
),
400,
)
if not PASSWORD_REGEX.fullmatch(password):
return jsonify({"error": "Invalid password. Spaces are not allowed."}), 400
# Attempt to add user
if add_user(username, password, api_key):
return (
jsonify({"message": "User successfully registered!", "api_key": api_key}),
201,
)
return jsonify({"error": "User already exists!"}), 409
if request.method == 'POST':
data = request.get_json()
for required_field in ['username', 'password']:
if required_field not in data:
return jsonify({
'error': f'Missing required field: {required_field}'
})
if add_user(data['username'], data['password']):
return jsonify({
'message': 'User successfully registered!'
})
else:
return jsonify({
'error': 'User already exists!'
})
else:
return jsonify({
'error': 'Method not supported'
})

View File

@ -1,354 +1,200 @@
"""Module to handle the remote device PlayReady."""
import base64
import os
from pathlib import Path
import re
import yaml
from flask import Blueprint, jsonify, request, current_app, Response
import os
import yaml
from pyplayready.device import Device as PlayReadyDevice
from pyplayready.cdm import Cdm as PlayReadyCDM
from pyplayready import PSSH as PlayReadyPSSH
from pyplayready.exceptions import (
InvalidSession,
InvalidLicense,
InvalidPssh,
)
from custom_functions.database.user_db import fetch_username_by_api_key
from custom_functions.decrypt.api_decrypt import is_base64
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh)
remotecdm_pr_bp = Blueprint("remotecdm_pr", __name__)
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
remotecdm_pr_bp = Blueprint('remotecdm_pr', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
def make_response(status, message, data=None, http_status=200):
"""Make a response."""
resp = {"status": status, "message": message}
if data is not None:
resp["data"] = data
return jsonify(resp), http_status
def check_required_fields(body, required_fields):
"""Return a response tuple if a required field is missing, else None."""
for field in required_fields:
if not body.get(field):
return make_response(
"Error",
f'Missing required field "{field}" in JSON body',
http_status=400,
)
return None
@remotecdm_pr_bp.route("/remotecdm/playready", methods=["GET", "HEAD"])
@remotecdm_pr_bp.route('/remotecdm/playready', methods=['GET', 'HEAD'])
def remote_cdm_playready():
"""Handle the remote device PlayReady."""
if request.method == "GET":
return make_response(
"Success",
"OK",
http_status=200,
)
if request.method == "HEAD":
if request.method == 'GET':
return jsonify({
'message': 'OK'
})
if request.method == 'HEAD':
response = Response(status=200)
response.headers["Server"] = "playready serve"
response.headers['Server'] = 'playready serve'
return response
return make_response("Failed", "Method not allowed", http_status=405)
@remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo", methods=["GET"])
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo', methods=['GET'])
def remote_cdm_playready_deviceinfo():
"""Handle the remote device PlayReady device info."""
base_name = config["default_pr_cdm"]
device = PlayReadyDevice.load(
os.path.join(os.getcwd(), "configs", "CDMs", "PR", base_name + ".prd")
)
if not base_name.endswith(".prd"):
full_file_name = (base_name + ".prd")
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}')
cdm = PlayReadyCDM.from_device(device)
return jsonify(
{
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/playready',
"secret": f'{config["remote_cdm_secret"]}',
"device_name": Path(base_name).stem,
}
)
return jsonify({
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/playready',
'secret': f'{config["remote_cdm_secret"]}',
'device_name': f'{base_name}'
})
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
@remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo/<device>", methods=["GET"])
def remote_cdm_playready_deviceinfo_specific(device):
"""Handle the remote device PlayReady device info specific."""
base_name = Path(device).with_suffix(".prd").name
api_key = request.headers["X-Secret-Key"]
username = fetch_username_by_api_key(api_key)
if not username:
return jsonify({"message": "Invalid or missing API key."}), 403
safe_username = sanitize_username(username)
device = PlayReadyDevice.load(
os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"PR",
base_name,
)
)
cdm = PlayReadyCDM.from_device(device)
return jsonify(
{
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/playready',
"secret": f"{api_key}",
"device_name": Path(base_name).stem,
}
)
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/open", methods=["GET"])
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', methods=['GET'])
def remote_cdm_playready_open(device):
"""Handle the remote device PlayReady open."""
unauthorized_msg = {
"message": f"Device '{device}' is not found or you are not authorized to use it."
}
# Default device logic
if str(device).lower() == config["default_pr_cdm"].lower():
pr_device = PlayReadyDevice.load(
os.path.join(
os.getcwd(), "configs", "CDMs", "PR", config["default_pr_cdm"] + ".prd"
)
)
cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device)
if str(device).lower() == config['default_pr_cdm'].lower():
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd')
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return make_response(
"Success",
"Successfully opened the PlayReady CDM session",
{
"session_id": session_id.hex(),
"device": {"security_level": cdm.security_level},
},
http_status=200,
)
return jsonify({
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'security_level': cdm.security_level
}
}
})
# User device logic
api_key = request.headers.get("X-Secret-Key")
if api_key and str(device).lower() != config["default_pr_cdm"].lower():
user = fetch_username_by_api_key(api_key=api_key)
safe_username = sanitize_username(user)
if user and user_allowed_to_use_device(device=device, username=user):
pr_device = PlayReadyDevice.load(
os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"PR",
device + ".prd",
)
)
cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return make_response(
"Success",
"Successfully opened the PlayReady CDM session",
{
"session_id": session_id.hex(),
"device": {"security_level": cdm.security_level},
},
http_status=200,
)
return make_response("Failed", unauthorized_msg, http_status=403)
return make_response("Failed", unauthorized_msg, http_status=403)
def get_cdm_or_error(device):
"""Get the CDM or return an error response."""
cdm = current_app.config.get("CDM")
if not cdm:
return make_response(
"Error",
f'No CDM session for "{device}" has been opened yet. No session to use',
http_status=400,
)
return cdm
@remotecdm_pr_bp.route(
"/remotecdm/playready/<device>/close/<session_id>", methods=["GET"]
)
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_playready_close(device, session_id):
"""Handle the remote device PlayReady close."""
try:
if str(device).lower() == config['default_pr_cdm'].lower():
session_id = bytes.fromhex(session_id)
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close'
})
try:
cdm.close(session_id)
except InvalidSession:
return make_response(
"Error",
f'Invalid session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
return make_response(
"Success",
f'Successfully closed Session "{session_id.hex()}".',
http_status=200,
)
except Exception as error:
return make_response(
"Error",
f'Failed to close Session "{session_id.hex()}", {error}.',
http_status=400,
)
return jsonify({
'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
})
return jsonify({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".',
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_pr_bp.route(
"/remotecdm/playready/<device>/get_license_challenge", methods=["POST"]
)
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
def remote_cdm_playready_get_license_challenge(device):
"""Handle the remote device PlayReady get license challenge."""
body = request.get_json()
missing_field = check_required_fields(body, ("session_id", "init_data"))
if missing_field:
return missing_field
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"):
try:
pssh = PlayReadyPSSH(init_data)
if pssh.wrm_headers:
init_data = pssh.wrm_headers[0]
except InvalidPssh as e:
return jsonify({
'message': f'Unable to parse base64 PSSH, {e}'
})
try:
pssh = PlayReadyPSSH(init_data)
if pssh.wrm_headers:
init_data = pssh.wrm_headers[0]
except InvalidPssh as error:
return make_response(
"Error",
f"Unable to parse base64 PSSH, {error}",
http_status=400,
license_request = cdm.get_license_challenge(
session_id=session_id,
wrm_header=init_data
)
try:
license_request = cdm.get_license_challenge(
session_id=session_id, wrm_header=init_data
)
except InvalidSession:
return make_response(
"Error",
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
http_status=400,
)
except ValueError as error:
return make_response(
"Error",
f"Invalid License, {error}",
http_status=400,
)
return make_response(
"Success",
"Successfully got the License Challenge",
{"challenge_b64": base64.b64encode(license_request).decode()},
http_status=200,
)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f'Error, {e}'
})
return jsonify({
'message': 'success',
'data': {
'challenge': license_request
}
})
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/parse_license", methods=["POST"])
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST'])
def remote_cdm_playready_parse_license(device):
"""Handle the remote device PlayReady parse license."""
body = request.get_json()
missing_field = check_required_fields(body, ("license_message", "session_id"))
if missing_field:
return missing_field
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
if is_base64(license_message):
license_message = base64.b64decode(license_message).decode("utf-8")
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return make_response(
"Error",
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
http_status=400,
)
except InvalidLicense as e:
return make_response(
"Error",
f"Invalid License, {e}",
http_status=400,
)
except Exception as e:
return make_response(
"Error",
f"Error, {e}",
http_status=400,
)
return make_response(
"Success",
"Successfully parsed and loaded the Keys from the License message",
http_status=200,
)
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("license_message", "session_id"):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f"No Cdm session for {device} has been opened yet. No session to use."
})
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except InvalidLicense as e:
return jsonify({
'message': f"Invalid License, {e}"
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
return jsonify({
'message': 'Successfully parsed and loaded the Keys from the License message'
})
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/get_keys", methods=["POST"])
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
def remote_cdm_playready_get_keys(device):
"""Handle the remote device PlayReady get keys."""
body = request.get_json()
missing_field = check_required_fields(body, ("session_id",))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
key_type = body.get("key_type", None)
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
keys = cdm.get_keys(session_id, key_type)
except InvalidSession:
return make_response(
"Error",
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
http_status=400,
)
except ValueError as error:
return make_response(
"Error",
f"The Key Type value '{key_type}' is invalid, {error}",
http_status=400,
)
keys_json = [
{
"key_id": key.key_id.hex,
"key": key.key.hex(),
"type": key.key_type.value,
"cipher_type": key.cipher_type.value,
"key_length": key.key_length,
}
for key in keys
if not key_type or key.type == key_type
]
return make_response(
"Success",
"Successfully got the Keys",
{"keys": keys_json},
http_status=200,
)
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': f"Missing required field '{required_field}' in JSON body."
})
try:
keys = cdm.get_keys(session_id)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
keys_json = [
{
"key_id": key.key_id.hex,
"key": key.key.hex(),
"type": key.key_type.value,
"cipher_type": key.cipher_type.value,
"key_length": key.key_length,
}
for key in keys
]
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})

View File

@ -1,474 +1,369 @@
"""Module to handle the remote device Widevine."""
import os
import base64
import re
from pathlib import Path
import yaml
from flask import Blueprint, jsonify, request, current_app, Response
import base64
from typing import Any, Optional, Union
from google.protobuf.message import DecodeError
from pywidevine.pssh import PSSH as widevinePSSH
from pywidevine import __version__
from pywidevine.cdm import Cdm as widevineCDM
from pywidevine.device import Device as widevineDevice
from pywidevine.exceptions import (
InvalidContext,
InvalidInitData,
InvalidLicenseMessage,
InvalidLicenseType,
InvalidSession,
SignatureMismatch,
)
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
InvalidSession, SignatureMismatch, TooManySessions)
from custom_functions.database.user_db import fetch_username_by_api_key
from custom_functions.database.unified_db_ops import cache_to_db
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
import yaml
remotecdm_wv_bp = Blueprint("remotecdm_wv", __name__)
with open(
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
) as file:
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
def make_response(status, message, data=None, http_status=200):
"""Make a response."""
resp = {"status": status, "message": message}
if data is not None:
resp["data"] = data
return jsonify(resp), http_status
def check_required_fields(body, required_fields):
"""Return a response if a required field is missing, else None."""
for field in required_fields:
if not body.get(field):
return make_response(
"Error",
f'Missing required field "{field}" in JSON body',
http_status=400,
)
return None
@remotecdm_wv_bp.route("/remotecdm/widevine", methods=["GET", "HEAD"])
@remotecdm_wv_bp.route('/remotecdm/widevine', methods=['GET', 'HEAD'])
def remote_cdm_widevine():
"""Handle the remote device Widevine."""
if request.method == "GET":
return make_response(
"Success",
f"{config['fqdn'].upper()} Remote Widevine CDM.",
http_status=200,
)
if request.method == "HEAD":
if request.method == 'GET':
return jsonify({
'status': 200,
'message': f"{config['fqdn'].upper()} Remote Widevine CDM."
})
if request.method == 'HEAD':
response = Response(status=200)
response.headers["Server"] = (
f"https://github.com/devine-dl/pywidevine serve v{__version__}"
)
response.headers['Server'] = f'https://github.com/devine-dl/pywidevine serve v{__version__}'
return response
return make_response(
"Error",
"Invalid request method",
http_status=405,
)
@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo", methods=["GET"])
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo', methods=['GET'])
def remote_cdm_widevine_deviceinfo():
"""Handle the remote device Widevine device info."""
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
base_name = base_name + ".wvd"
device = widevineDevice.load(
os.path.join(os.getcwd(), "configs", "CDMs", "WV", base_name)
)
cdm = widevineCDM.from_device(device)
return make_response(
"Success",
"Successfully got the Widevine CDM device info",
{
"device_type": cdm.device_type.name,
"system_id": cdm.system_id,
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/widevine',
"secret": f'{config["remote_cdm_secret"]}',
"device_name": Path(base_name).stem,
},
http_status=200,
)
if request.method == 'GET':
base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"):
full_file_name = (base_name + ".wvd")
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{full_file_name}')
cdm = widevineCDM.from_device(device)
return jsonify({
'device_type': cdm.device_type.name,
'system_id': cdm.system_id,
'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine',
'secret': f'{config["remote_cdm_secret"]}',
'device_name': f'{base_name}'
})
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo/<device>", methods=["GET"])
def remote_cdm_widevine_deviceinfo_specific(device):
"""Handle the remote device Widevine device info specific."""
base_name = Path(device).with_suffix(".wvd").name
api_key = request.headers["X-Secret-Key"]
username = fetch_username_by_api_key(api_key)
safe_username = sanitize_username(username)
device = widevineDevice.load(
os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"WV",
base_name,
)
)
cdm = widevineCDM.from_device(device)
return make_response(
"Success",
"Successfully got the Widevine CDM device info (by user)",
{
"device_type": cdm.device_type.name,
"system_id": cdm.system_id,
"security_level": cdm.security_level,
"host": f'{config["fqdn"]}/remotecdm/widevine',
"secret": f"{api_key}",
"device_name": Path(base_name).stem,
},
http_status=200,
)
def load_widevine_device(device_name, api_key=None):
"""Load a Widevine device, either default or user-uploaded."""
try:
if device_name.lower() == config["default_wv_cdm"].lower():
path = os.path.join(
os.getcwd(), "configs", "CDMs", "WV", config["default_wv_cdm"] + ".wvd"
)
else:
if not api_key:
return None
username = fetch_username_by_api_key(api_key)
if not username or not user_allowed_to_use_device(
device=device_name, username=username
):
return None
safe_username = sanitize_username(username)
path = os.path.join(
os.getcwd(),
"configs",
"CDMs",
"users_uploaded",
safe_username,
"WV",
device_name + ".wvd",
)
return widevineDevice.load(path)
except (FileNotFoundError, ValueError):
return None
def get_cdm_or_error(device):
"""Get the CDM or return an error response."""
cdm = current_app.config.get("CDM")
if not cdm:
return make_response(
"Error",
f'No CDM session for "{device}" has been opened yet. No session to use',
http_status=400,
)
return cdm
@remotecdm_wv_bp.route("/remotecdm/widevine/<device>/open", methods=["GET"])
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET'])
def remote_cdm_widevine_open(device):
"""Handle the remote device Widevine open."""
api_key = request.headers.get("X-Secret-Key")
wv_device = load_widevine_device(device, api_key)
if not wv_device:
return make_response(
"Error",
f"Device '{device}' is not found or you are not authorized to use it.",
http_status=403,
)
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
session_id = cdm.open()
return make_response(
"Success",
"Successfully opened the Widevine CDM session",
{
"session_id": session_id.hex(),
"device": {
"system_id": cdm.system_id,
"security_level": cdm.security_level,
},
},
http_status=200,
)
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/close/<session_id>", methods=["GET"]
)
def remote_cdm_widevine_close(device, session_id):
"""Handle the remote device Widevine close."""
session_id = bytes.fromhex(session_id)
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
cdm.close(session_id)
except InvalidSession:
return make_response(
"Error",
f'Invalid session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
return make_response(
"Success",
f'Successfully closed Session "{session_id.hex()}".',
http_status=200,
)
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/set_service_certificate", methods=["POST"]
)
def remote_cdm_widevine_set_service_certificate(device):
"""Handle the remote device Widevine set service certificate."""
body = request.get_json()
missing_field = check_required_fields(body, ("session_id", "certificate"))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
certificate = body["certificate"]
try:
provider_id = cdm.set_service_certificate(session_id, certificate)
except InvalidSession:
return make_response(
"Error",
f'Invalid session id: "{session_id.hex()}", it may have expired',
http_status=400,
)
except DecodeError as error:
return make_response(
"Error",
f"Invalid Service Certificate, {error}",
http_status=400,
)
except SignatureMismatch:
return make_response(
"Error",
"Signature Validation failed on the Service Certificate, rejecting",
http_status=400,
)
return make_response(
"Success",
f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
{"provider_id": provider_id},
http_status=200,
)
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_service_certificate", methods=["POST"]
)
def remote_cdm_widevine_get_service_certificate(device):
"""Handle the remote device Widevine get service certificate."""
body = request.get_json()
missing_field = check_required_fields(body, ("session_id",))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
service_certificate = cdm.get_service_certificate(session_id)
except InvalidSession:
return make_response(
"Error",
f'Invalid Session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
if service_certificate:
service_certificate_b64 = base64.b64encode(
service_certificate.SerializeToString()
).decode()
if str(device).lower() == config['default_wv_cdm'].lower():
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd')
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
session_id = cdm.open()
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'system_id': cdm.system_id,
'security_level': cdm.security_level,
}
}
})
else:
service_certificate_b64 = None
return make_response(
"Success",
"Successfully got the Service Certificate",
{"service_certificate": service_certificate_b64},
http_status=200,
)
return jsonify({
'status': 400,
'message': 'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_widevine_close(device, session_id):
if str(device).lower() == config['default_wv_cdm'].lower():
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close'
})
try:
cdm.close(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
})
return jsonify({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".',
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_license_challenge/<license_type>",
methods=["POST"],
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
def remote_cdm_widevine_set_service_certificate(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "certificate"):
if required_field == "certificate":
has_field = required_field in body # it needs the key, but can be empty/null
else:
has_field = body.get(required_field)
if not has_field:
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
certificate = body["certificate"]
try:
provider_id = cdm.set_service_certificate(session_id, certificate)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
})
except DecodeError as error:
return jsonify({
'status': 400,
'message': f'Invalid Service Certificate, {error}'
})
except SignatureMismatch:
return jsonify({
'status': 400,
'message': 'Signature Validation failed on the Service Certificate, rejecting'
})
return jsonify({
'status': 200,
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
'data': {
'provider_id': provider_id,
}
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
def remote_cdm_widevine_get_service_certificate(device):
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
try:
service_certificate = cdm.get_service_certificate(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
if service_certificate:
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
else:
service_certificate_b64 = None
return jsonify({
'status': 200,
'message': 'Successfully got the Service Certificate',
'data': {
'service_certificate': service_certificate_b64,
}
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
def remote_cdm_widevine_get_license_challenge(device, license_type):
"""Handle the remote device Widevine get license challenge."""
body = request.get_json()
missing_field = check_required_fields(body, ("session_id", "init_data"))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True)
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
if current_app.config.get("force_privacy_mode"):
privacy_mode = True
if not cdm.get_service_certificate(session_id):
return make_response(
"Error",
"No Service Certificate set but Privacy Mode is Enforced.",
http_status=403,
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
if current_app.config.get("force_privacy_mode"):
privacy_mode = True
if not cdm.get_service_certificate(session_id):
return jsonify({
'status': 403,
'message': 'No Service Certificate set but Privacy Mode is Enforced.'
})
current_app.config['pssh'] = body['init_data']
init_data = widevinePSSH(body['init_data'])
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
pssh=init_data,
license_type=license_type,
privacy_mode=privacy_mode
)
current_app.config["pssh"] = body["init_data"]
init_data = widevinePSSH(body["init_data"])
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
pssh=init_data,
license_type=license_type,
privacy_mode=privacy_mode,
)
except InvalidSession:
return make_response(
"Error",
f'Invalid Session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
except InvalidInitData as error:
return make_response(
"Error",
f"Invalid Init Data, {error}",
http_status=400,
)
except InvalidLicenseType:
return make_response(
"Error",
f"Invalid License Type {license_type}",
http_status=400,
)
return make_response(
"Success",
"Successfully got the License Challenge",
{"challenge_b64": base64.b64encode(license_request).decode()},
http_status=200,
)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
except InvalidInitData as error:
return jsonify({
'status': 400,
'message': f'Invalid Init Data, {error}'
})
except InvalidLicenseType:
return jsonify({
'status': 400,
'message': f'Invalid License Type {license_type}'
})
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'challenge_b64': base64.b64encode(license_request).decode()
}
})
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route("/remotecdm/widevine/<device>/parse_license", methods=["POST"])
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
def remote_cdm_widevine_parse_license(device):
"""Handle the remote device Widevine parse license."""
body = request.get_json()
missing_field = check_required_fields(body, ("session_id", "license_message"))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
cdm.parse_license(session_id, body["license_message"])
except InvalidLicenseMessage as error:
return make_response(
"Error",
f"Invalid License Message, {error}",
http_status=400,
)
except InvalidContext as error:
return make_response(
"Error",
f"Invalid Context, {error}",
http_status=400,
)
except InvalidSession:
return make_response(
"Error",
f'Invalid Session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
except SignatureMismatch:
return make_response(
"Error",
"Signature Validation failed on the License Message, rejecting.",
http_status=400,
)
return make_response(
"Success",
"Successfully parsed and loaded the Keys from the License message.",
http_status=200,
)
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "license_message"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
try:
cdm.parse_license(session_id, body['license_message'])
except InvalidLicenseMessage as error:
return jsonify({
'status': 400,
'message': f'Invalid License Message, {error}'
})
except InvalidContext as error:
return jsonify({
'status': 400,
'message': f'Invalid Context, {error}'
})
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
except SignatureMismatch:
return jsonify({
'status': 400,
'message': f'Signature Validation failed on the License Message, rejecting.'
})
return jsonify({
'status': 200,
'message': 'Successfully parsed and loaded the Keys from the License message.',
})
else:
return jsonify({
'status': 400,
'message': 'Unauthorized'
})
@remotecdm_wv_bp.route(
"/remotecdm/widevine/<device>/get_keys/<key_type>", methods=["POST"]
)
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
def remote_cdm_widevine_get_keys(device, key_type):
"""Handle the remote device Widevine get keys."""
body = request.get_json()
missing_field = check_required_fields(body, ("session_id",))
if missing_field:
return missing_field
session_id = bytes.fromhex(body["session_id"])
if key_type == "ALL":
key_type = None
cdm = get_cdm_or_error(device)
if isinstance(cdm, tuple): # error response
return cdm
try:
keys = cdm.get_keys(session_id, key_type)
except InvalidSession:
return make_response(
"Error",
f'Invalid Session ID "{session_id.hex()}", it may have expired',
http_status=400,
)
except ValueError as error:
return make_response(
"Error",
f'The Key Type value "{key_type}" is invalid, {error}',
http_status=400,
)
keys_json = [
{
"key_id": key.kid.hex,
"key": key.key.hex(),
"type": key.type,
"permissions": key.permissions,
}
for key in keys
if not key_type or key.type == key_type
]
for entry in keys_json:
if entry["type"] != "SIGNING":
cache_to_db(
pssh=str(current_app.config["pssh"]),
kid=entry["key_id"],
key=entry["key"],
)
if str(device).lower() == config['default_wv_cdm'].lower():
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
key_type: Optional[str] = key_type
if key_type == 'ALL':
key_type = None
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
})
try:
keys = cdm.get_keys(session_id, key_type)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
except ValueError as error:
return jsonify({
'status': 400,
'message': f'The Key Type value "{key_type}" is invalid, {error}'
})
keys_json = [
{
"key_id": key.kid.hex,
"key": key.key.hex(),
"type": key.type,
"permissions": key.permissions
}
for key in keys
if not key_type or key.type == key_type
]
for entry in keys_json:
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import cache_to_db
elif config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import cache_to_db
if entry['type'] != 'SIGNING':
cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
return make_response(
"Success",
"Successfully got the Keys",
{"keys": keys_json},
http_status=200,
)
return jsonify({
'status': 200,
'message': 'Success',
'data': {
'keys': keys_json
}
})

View File

@ -1,57 +1,42 @@
"""Module to handle the upload process."""
from flask import Blueprint, request, jsonify, session
import os
import logging
import re
from flask import Blueprint, request, jsonify, session
upload_bp = Blueprint("upload_bp", __name__)
upload_bp = Blueprint('upload_bp', __name__)
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
@upload_bp.route("/upload/<cdmtype>", methods=["POST"])
@upload_bp.route('/upload/<cdmtype>', methods=['POST'])
def upload(cdmtype):
"""Handle the upload process."""
try:
username = session.get("username")
username = session.get('username')
if not username:
return jsonify({"message": "False", "error": "No username in session"}), 400
safe_username = sanitize_username(username)
return jsonify({'message': 'False', 'error': 'No username in session'}), 400
# Validate CDM type
if cdmtype not in ["PR", "WV"]:
return jsonify({"message": "False", "error": "Invalid CDM type"}), 400
if cdmtype not in ['PR', 'WV']:
return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400
# Set up user directory paths
base_path = os.path.join(
os.getcwd(), "configs", "CDMs", "users_uploaded", safe_username
)
pr_path = os.path.join(base_path, "PR")
wv_path = os.path.join(base_path, "WV")
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
pr_path = os.path.join(base_path, 'PR')
wv_path = os.path.join(base_path, 'WV')
# Create necessary directories if they don't exist
os.makedirs(pr_path, exist_ok=True)
os.makedirs(wv_path, exist_ok=True)
# Get uploaded file
uploaded_file = request.files.get("file")
uploaded_file = request.files.get('file')
if not uploaded_file:
return jsonify({"message": "False", "error": "No file provided"}), 400
return jsonify({'message': 'False', 'error': 'No file provided'}), 400
# Determine correct save path based on cdmtype
filename = uploaded_file.filename
assert filename is not None
target_path = pr_path if cdmtype == "PR" else wv_path
save_path = os.path.join(target_path, filename)
save_path = os.path.join(pr_path if cdmtype == 'PR' else wv_path, filename)
uploaded_file.save(save_path)
return jsonify({"message": "Success", "file_saved_to": save_path})
return jsonify({'message': 'Success', 'file_saved_to': save_path})
except (OSError, IOError, ValueError, AttributeError) as e:
logging.exception("Upload failed: %s", {e})
return jsonify({"message": "False", "error": "Server error"}), 500
except Exception as e:
logging.exception("Upload failed")
return jsonify({'message': 'False', 'error': 'Server error'}), 500

View File

@ -1,65 +0,0 @@
"""Module to handle the user changes."""
import re
from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import change_password, change_api_key
user_change_bp = Blueprint("user_change_bp", __name__)
# Define allowed characters regex (no spaces allowed)
PASSWORD_REGEX = re.compile(r'^[A-Za-z0-9!@#$%^&*()_+\-=\[\]{};\'":\\|,.<>\/?`~]+$')
@user_change_bp.route("/user/change_password", methods=["POST"])
def change_password_route():
"""Handle the change password route."""
username = session.get("username")
if not username:
return jsonify({"message": "False"}), 400
try:
data = request.get_json()
new_password = data.get("new_password", "")
if not PASSWORD_REGEX.match(new_password):
return jsonify({"message": "Invalid password format"}), 400
change_password(username=username, new_password=new_password)
return jsonify({"message": "True"}), 200
except Exception as e:
return jsonify({"message": "False", "error": str(e)}), 400
@user_change_bp.route("/user/change_api_key", methods=["POST"])
def change_api_key_route():
"""Handle the change API key route."""
# Ensure the user is logged in by checking session for 'username'
username = session.get("username")
if not username:
return jsonify({"message": "False", "error": "User not logged in"}), 400
# Get the new API key from the request body
new_api_key = request.get_json().get("new_api_key")
if not new_api_key:
return jsonify({"message": "False", "error": "New API key not provided"}), 400
try:
# Call the function to update the API key in the database
success = change_api_key(username=username, new_api_key=new_api_key)
if success:
return (
jsonify({"message": "True", "success": "API key changed successfully"}),
200,
)
else:
return (
jsonify({"message": "False", "error": "Failed to change API key"}),
500,
)
except Exception as e:
# Catch any unexpected errors and return a response
return jsonify({"message": "False", "error": str(e)}), 500

View File

@ -1,61 +1,26 @@
"""Module to handle the user info request."""
from flask import Blueprint, request, jsonify, session
import os
import glob
import logging
import re
from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import (
fetch_api_key,
fetch_styled_username,
fetch_username_by_api_key,
)
user_info_bp = Blueprint("user_info_bp", __name__)
user_info_bp = Blueprint('user_info_bp', __name__)
def sanitize_username(username):
"""Sanitize the username."""
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
@user_info_bp.route("/userinfo", methods=["POST"])
@user_info_bp.route('/userinfo', methods=['POST'])
def user_info():
"""Handle the user info request."""
username = session.get("username")
username = session.get('username')
if not username:
try:
headers = request.headers
api_key = headers["Api-Key"]
username = fetch_username_by_api_key(api_key)
except Exception as e:
logging.exception("Error retrieving username by API key, %s", {e})
return jsonify({"message": "False"}), 400
safe_username = sanitize_username(username)
return jsonify({'message': 'False'}), 400
try:
base_path = os.path.join(
os.getcwd(), "configs", "CDMs", "users_uploaded", safe_username
)
pr_files = [
os.path.basename(f)
for f in glob.glob(os.path.join(base_path, "PR", "*.prd"))
]
wv_files = [
os.path.basename(f)
for f in glob.glob(os.path.join(base_path, "WV", "*.wvd"))
]
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
return jsonify(
{
"Username": username,
"Widevine_Devices": wv_files,
"Playready_Devices": pr_files,
"API_Key": fetch_api_key(username),
"Styled_Username": fetch_styled_username(username),
}
)
return jsonify({
'Username': username,
'Widevine_Devices': wv_files,
'Playready_Devices': pr_files
})
except Exception as e:
logging.exception("Error retrieving device files, %s", {e})
return jsonify({"message": "False"}), 500
logging.exception("Error retrieving device files")
return jsonify({'message': 'False'}), 500