Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cc3b37db1d | ||
|
db7bea7951 | ||
|
7f9f04d829 | ||
|
7f84542cfb | ||
|
78d59b295c | ||
|
8e076a4298 | ||
|
bbeeffcd9d | ||
|
6890c6b464 | ||
|
454429ba7f | ||
|
003508aabd | ||
|
d6cf10ccaf | ||
|
c82d23aabc | ||
|
2e520da006 | ||
|
a2a12b4c49 | ||
|
8940d57b25 | ||
|
c756361da0 | ||
|
c82e493ef1 | ||
|
29be40ab95 | ||
|
1ef842978a | ||
|
e66e32ef0a | ||
|
5f217f2995 | ||
|
1328805fa5 | ||
|
fd2f38fe28 | ||
|
802fbdebd1 | ||
|
e84f43a702 | ||
|
bafd3db4f4 | ||
|
84999654ed | ||
|
2828edd6b7 | ||
|
a82a3fd106 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,3 +8,6 @@ build
|
|||||||
main.spec
|
main.spec
|
||||||
pyinstallericon.ico
|
pyinstallericon.ico
|
||||||
icon.ico
|
icon.ico
|
||||||
|
venv
|
||||||
|
frontend-dist
|
||||||
|
cdrm-frontend/dist/
|
||||||
|
55
README.md
55
README.md
@ -1,34 +1,51 @@
|
|||||||
|
|
||||||
## CDRM-Project
|
# CDRM-Project
|
||||||
|
|
||||||
   
|
   
|
||||||
|
|
||||||
## Prerequisites (from source only)
|
## Prerequisites (from source only)
|
||||||
|
|
||||||
- [Python](https://www.python.org/downloads/) version [3.12](https://www.python.org/downloads/release/python-3120/)+ with PIP and VENV installed
|
- [Python](https://www.python.org/downloads/) version 3.12+ with PIP 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
|
## Installation (Automatic) - Recommended
|
||||||
- Extract contents of CDRM-Project 2.0 git contents into a new folder
|
|
||||||
|
- Extract contents of CDRM-Project into a new folder
|
||||||
- Open a terminal and change directory into the new folder
|
- Open a terminal and change directory into the new folder
|
||||||
- Run `python main.py`
|
- Run `python build.py && python main.py`
|
||||||
- Follow the on-screen prompts
|
- Follow the on-screen prompts
|
||||||
|
|
||||||
## Installation (From binary)
|
## Installation (From binary)
|
||||||
|
|
||||||
- Download the latest release from the [releases](https://cdm-project.com/tpd94/CDRM-Project/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)
|
## 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
|
|
||||||
|
|
||||||
> Windows - change directory into the `Scripts` directory then `activate.bat`
|
- 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`
|
||||||
> Linux - `source bin/activate`
|
- Navigate to the `CDRM-Project` folder
|
||||||
|
- Create a new python virtual environment using `python -m venv venv`
|
||||||
|
- Activate the virtual environment
|
||||||
|
|
||||||
- Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder
|
- Windows:
|
||||||
- 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`
|
```bash
|
||||||
- (Optional) Create the folder structure `/config/CDMs/PR` and place your .PRD file into `/configs/CDMs/PR`
|
.\venv\Scripts\activate
|
||||||
- Run the application with `python main.py`
|
```
|
||||||
|
|
||||||
|
- Linux:
|
||||||
|
|
||||||
|
```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`
|
||||||
|
39
build.py
Normal file
39
build.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""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()
|
5
cdrm-frontend/.prettierignore
Normal file
5
cdrm-frontend/.prettierignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
src/assets/icons/
|
||||||
|
src/components/Functions/protobuf.min.js
|
||||||
|
src/components/Functions/license_protocol.min.js
|
8
cdrm-frontend/.prettierrc.json
Normal file
8
cdrm-frontend/.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
1
cdrm-frontend/dist/assets/index-DQNyIeaF.css
vendored
1
cdrm-frontend/dist/assets/index-DQNyIeaF.css
vendored
File diff suppressed because one or more lines are too long
160
cdrm-frontend/dist/assets/index-DWCLK6jB.js
vendored
160
cdrm-frontend/dist/assets/index-DWCLK6jB.js
vendored
File diff suppressed because one or more lines are too long
BIN
cdrm-frontend/dist/favico.png
vendored
BIN
cdrm-frontend/dist/favico.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 862 B |
21
cdrm-frontend/dist/index.html
vendored
21
cdrm-frontend/dist/index.html
vendored
@ -1,21 +0,0 @@
|
|||||||
<!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-DWCLK6jB.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DQNyIeaF.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
BIN
cdrm-frontend/dist/og-api.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 189 KiB |
BIN
cdrm-frontend/dist/og-cache.jpg
vendored
BIN
cdrm-frontend/dist/og-cache.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 207 KiB |
BIN
cdrm-frontend/dist/og-home.jpg
vendored
BIN
cdrm-frontend/dist/og-home.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 302 KiB |
BIN
cdrm-frontend/dist/og-testplayer.jpg
vendored
BIN
cdrm-frontend/dist/og-testplayer.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 99 KiB |
@ -1,33 +1,30 @@
|
|||||||
import js from '@eslint/js'
|
import js from "@eslint/js";
|
||||||
import globals from 'globals'
|
import globals from "globals";
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ ignores: ['dist'] },
|
{ ignores: ["dist"] },
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx}'],
|
files: ["**/*.{js,jsx}"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: "latest",
|
||||||
ecmaFeatures: { jsx: true },
|
ecmaFeatures: { jsx: true },
|
||||||
sourceType: 'module',
|
sourceType: "module",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
|
||||||
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
];
|
||||||
'react-hooks': reactHooks,
|
|
||||||
'react-refresh': reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...js.configs.recommended.rules,
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" class="w-full h-full">
|
<html lang="en" class="w-full h-full">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favico.png" />
|
<link rel="icon" type="image/svg+xml" href="/favico.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="{{ data.description }}"/>
|
<meta name="description" content="{{ data.description }}" />
|
||||||
<meta name="keywords" content="{{ data.keywords }}"/>
|
<meta name="keywords" content="{{ data.keywords }}" />
|
||||||
<meta property='og:title' content="{{ data.opengraph_title }}" />
|
<meta property="og:title" content="{{ data.opengraph_title }}" />
|
||||||
<meta property='og:description' content="{{ data.opengraph_description }}" />
|
<meta property="og:description" content="{{ data.opengraph_description }}" />
|
||||||
<meta property='og:image' content="{{ data.opengraph_image }}" />
|
<meta property="og:image" content="{{ data.opengraph_image }}" />
|
||||||
<meta property='og:url' content="{{ data.opengraph_url }}" />
|
<meta property="og:url" content="{{ data.opengraph_url }}" />
|
||||||
<meta property='og:locale' content='en_US' />
|
<meta property="og:locale" content="en_US" />
|
||||||
<title>{{ data.tab_title }}</title>
|
<title>{{ data.tab_title }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="w-full h-full">
|
<body class="w-full h-full">
|
||||||
<div id="root" class="w-full h-full"></div>
|
<div id="root" class="w-full h-full"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
7612
cdrm-frontend/package-lock.json
generated
7612
cdrm-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,33 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "cdrm-frontend",
|
"name": "cdrm-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.10.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-router-dom": "^7.7.0",
|
||||||
"react-router-dom": "^7.5.2",
|
"shaka-player": "^4.15.8",
|
||||||
"shaka-player": "^4.14.9",
|
"tailwindcss": "^4.1.11"
|
||||||
"tailwindcss": "^4.1.4"
|
},
|
||||||
},
|
"devDependencies": {
|
||||||
"devDependencies": {
|
"@eslint/js": "^9.31.0",
|
||||||
"@eslint/js": "^9.22.0",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react-swc": "^3.11.0",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.31.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.3.0",
|
||||||
"vite": "^6.3.1"
|
"vite": "^7.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,34 +10,40 @@ import Account from "./components/Pages/Account";
|
|||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
|
const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="appcontainer" className="flex flex-row w-full h-full bg-black">
|
<div id="appcontainer" className="flex flex-row w-full h-full bg-black">
|
||||||
{/* The SideMenu should be visible when isMenuOpen is true */}
|
{/* The SideMenu should be visible when isMenuOpen is true */}
|
||||||
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
|
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
|
||||||
|
|
||||||
<div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0">
|
<div
|
||||||
<NavBar />
|
id="navbarcontainer"
|
||||||
</div>
|
className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0"
|
||||||
|
>
|
||||||
|
<NavBar />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
|
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
|
||||||
<div id="navbarmaincontainer" className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10">
|
<div
|
||||||
<NavBarMain setIsMenuOpen={setIsMenuOpen} />
|
id="navbarmaincontainer"
|
||||||
|
className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10"
|
||||||
|
>
|
||||||
|
<NavBarMain setIsMenuOpen={setIsMenuOpen} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/cache" element={<Cache />} />
|
||||||
|
<Route path="/api" element={<API />} />
|
||||||
|
<Route path="/testplayer" element={<TestPlayer />} />
|
||||||
|
<Route path="/account" element={<Account />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Home />} />
|
|
||||||
<Route path="/cache" element={<Cache />} />
|
|
||||||
<Route path="/api" element={<API />} />
|
|
||||||
<Route path="/testplayer" element={<TestPlayer />} />
|
|
||||||
<Route path="/account" element={<Account />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -4,11 +4,11 @@ import "./license_protocol.min.js";
|
|||||||
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
|
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
|
||||||
|
|
||||||
function uint8ArrayToBase64(uint8Array) {
|
function uint8ArrayToBase64(uint8Array) {
|
||||||
const binaryString = Array.from(uint8Array)
|
const binaryString = Array.from(uint8Array)
|
||||||
.map(b => String.fromCharCode(b))
|
.map((b) => String.fromCharCode(b))
|
||||||
.join('');
|
.join("");
|
||||||
|
|
||||||
return btoa(binaryString);
|
return btoa(binaryString);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFetch(fetchString) {
|
function parseFetch(fetchString) {
|
||||||
@ -17,10 +17,13 @@ function parseFetch(fetchString) {
|
|||||||
|
|
||||||
// Use a more lenient regex to capture the fetch pattern (including complex bodies)
|
// Use a more lenient regex to capture the fetch pattern (including complex bodies)
|
||||||
const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
|
const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
|
||||||
const lines = fetchString.split('\n').map(line => line.trim()).filter(Boolean);
|
const lines = fetchString
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
const result = {
|
const result = {
|
||||||
method: 'UNDEFINED',
|
method: "UNDEFINED",
|
||||||
url: '',
|
url: "",
|
||||||
headers: {},
|
headers: {},
|
||||||
body: null,
|
body: null,
|
||||||
};
|
};
|
||||||
@ -47,9 +50,12 @@ function parseFetch(fetchString) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WIDEVINE_SYSTEM_ID = new Uint8Array([
|
||||||
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]);
|
0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed,
|
||||||
const PLAYREADY_SYSTEM_ID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]);
|
]);
|
||||||
|
const PLAYREADY_SYSTEM_ID = new Uint8Array([
|
||||||
|
0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
|
||||||
|
]);
|
||||||
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
|
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
|
||||||
|
|
||||||
function intToUint8Array(num, endian) {
|
function intToUint8Array(num, endian) {
|
||||||
@ -75,37 +81,37 @@ function psshDataToPsshBoxB64(pssh_data, system_id) {
|
|||||||
...new Uint8Array(4),
|
...new Uint8Array(4),
|
||||||
...system_id,
|
...system_id,
|
||||||
...intToUint8Array(dataLength, false),
|
...intToUint8Array(dataLength, false),
|
||||||
...pssh_data
|
...pssh_data,
|
||||||
]);
|
]);
|
||||||
return uint8ArrayToBase64(pssh);
|
return uint8ArrayToBase64(pssh);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrmHeaderToPlayReadyHeader(wrm_header){
|
function wrmHeaderToPlayReadyHeader(wrm_header) {
|
||||||
const playready_object = new Uint8Array([
|
const playready_object = new Uint8Array([
|
||||||
...shortToUint8Array(1, true),
|
...shortToUint8Array(1, true),
|
||||||
...shortToUint8Array(wrm_header.length, true),
|
...shortToUint8Array(wrm_header.length, true),
|
||||||
...wrm_header
|
...wrm_header,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return new Uint8Array([
|
return new Uint8Array([
|
||||||
...intToUint8Array(playready_object.length + 2 + 4, true),
|
...intToUint8Array(playready_object.length + 2 + 4, true),
|
||||||
...shortToUint8Array(1, true),
|
...shortToUint8Array(1, true),
|
||||||
...playready_object
|
...playready_object,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeUtf16LE(str) {
|
function encodeUtf16LE(str) {
|
||||||
const buffer = new Uint8Array(str.length * 2);
|
const buffer = new Uint8Array(str.length * 2);
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
const code = str.charCodeAt(i);
|
const code = str.charCodeAt(i);
|
||||||
buffer[i * 2] = code & 0xff;
|
buffer[i * 2] = code & 0xff;
|
||||||
buffer[i * 2 + 1] = code >> 8;
|
buffer[i * 2 + 1] = code >> 8;
|
||||||
}
|
}
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringToUint8Array(string) {
|
function stringToUint8Array(string) {
|
||||||
return Uint8Array.from(string.split("").map(x => x.charCodeAt()));
|
return Uint8Array.from(string.split("").map((x) => x.charCodeAt()));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readTextFromClipboard() {
|
export async function readTextFromClipboard() {
|
||||||
@ -136,11 +142,15 @@ export async function readTextFromClipboard() {
|
|||||||
license_request = LicenseRequest.decode(signed_message.msg);
|
license_request = LicenseRequest.decode(signed_message.msg);
|
||||||
} catch (decodeError) {
|
} catch (decodeError) {
|
||||||
// If error occurs during decoding, return an empty pssh
|
// If error occurs during decoding, return an empty pssh
|
||||||
console.error('Decoding failed, returning empty pssh', decodeError);
|
console.error("Decoding failed, returning empty pssh", decodeError);
|
||||||
pssh_data_string = ''; // Empty pssh if decoding fails
|
pssh_data_string = ""; // Empty pssh if decoding fails
|
||||||
}
|
}
|
||||||
|
|
||||||
if (license_request && license_request.contentId && license_request.contentId.widevinePsshData) {
|
if (
|
||||||
|
license_request &&
|
||||||
|
license_request.contentId &&
|
||||||
|
license_request.contentId.widevinePsshData
|
||||||
|
) {
|
||||||
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
|
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
|
||||||
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
|
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
|
||||||
}
|
}
|
||||||
@ -160,14 +170,12 @@ export async function readTextFromClipboard() {
|
|||||||
document.getElementById("pssh").value = pssh_data_string;
|
document.getElementById("pssh").value = pssh_data_string;
|
||||||
document.getElementById("data").value = payload_string;
|
document.getElementById("data").value = payload_string;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to read clipboard contents:', error);
|
console.error("Failed to read clipboard contents:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if the data is binary
|
// Helper function to check if the data is binary
|
||||||
function isBinary(uint8Array) {
|
function isBinary(uint8Array) {
|
||||||
// Check for non-text (non-ASCII) bytes (basic heuristic)
|
// Check for non-text (non-ASCII) bytes (basic heuristic)
|
||||||
return uint8Array.some(byte => byte > 127); // Non-ASCII byte indicates binary
|
return uint8Array.some((byte) => byte > 127); // Non-ASCII byte indicates binary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from "react-router-dom";
|
||||||
import homeIcon from '../assets/icons/home.svg';
|
import homeIcon from "../assets/icons/home.svg";
|
||||||
import cacheIcon from '../assets/icons/cache.svg';
|
import cacheIcon from "../assets/icons/cache.svg";
|
||||||
import apiIcon from '../assets/icons/api.svg';
|
import apiIcon from "../assets/icons/api.svg";
|
||||||
import testPlayerIcon from '../assets/icons/testplayer.svg';
|
import testPlayerIcon from "../assets/icons/testplayer.svg";
|
||||||
import accountIcon from '../assets/icons/account.svg';
|
import accountIcon from "../assets/icons/account.svg";
|
||||||
import discordIcon from '../assets/icons/discord.svg';
|
import discordIcon from "../assets/icons/discord.svg";
|
||||||
import telegramIcon from '../assets/icons/telegram.svg';
|
import telegramIcon from "../assets/icons/telegram.svg";
|
||||||
import giteaIcon from '../assets/icons/gitea.svg';
|
import giteaIcon from "../assets/icons/gitea.svg";
|
||||||
|
|
||||||
function NavBar() {
|
function NavBar() {
|
||||||
const [externalLinks, setExternalLinks] = useState({
|
const [externalLinks, setExternalLinks] = useState({
|
||||||
discord: '#',
|
discord: "#",
|
||||||
telegram: '#',
|
telegram: "#",
|
||||||
gitea: '#',
|
gitea: "#",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/links')
|
fetch("/api/links")
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(data => setExternalLinks(data))
|
.then((data) => setExternalLinks(data))
|
||||||
.catch(error => console.error('Error fetching links:', error));
|
.catch((error) => console.error("Error fetching links:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -40,8 +40,8 @@ function NavBar() {
|
|||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`flex flex-row p-3 border-l-3 ${
|
`flex flex-row p-3 border-l-3 ${
|
||||||
isActive
|
isActive
|
||||||
? 'border-l-sky-500/50 bg-black/50'
|
? "border-l-sky-500/50 bg-black/50"
|
||||||
: 'hover:border-l-sky-500/50 hover:bg-white/5'
|
: "hover:border-l-sky-500/50 hover:bg-white/5"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -58,8 +58,8 @@ function NavBar() {
|
|||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`flex flex-row p-3 border-l-3 ${
|
`flex flex-row p-3 border-l-3 ${
|
||||||
isActive
|
isActive
|
||||||
? 'border-l-emerald-500/50 bg-black/50'
|
? "border-l-emerald-500/50 bg-black/50"
|
||||||
: 'hover:border-l-emerald-500/50 hover:bg-white/5'
|
: "hover:border-l-emerald-500/50 hover:bg-white/5"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -76,8 +76,8 @@ function NavBar() {
|
|||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`flex flex-row p-3 border-l-3 ${
|
`flex flex-row p-3 border-l-3 ${
|
||||||
isActive
|
isActive
|
||||||
? 'border-l-indigo-500/50 bg-black/50'
|
? "border-l-indigo-500/50 bg-black/50"
|
||||||
: 'hover:border-l-indigo-500/50 hover:bg-white/5'
|
: "hover:border-l-indigo-500/50 hover:bg-white/5"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -94,13 +94,17 @@ function NavBar() {
|
|||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`flex flex-row p-3 border-l-3 ${
|
`flex flex-row p-3 border-l-3 ${
|
||||||
isActive
|
isActive
|
||||||
? 'border-l-rose-500/50 bg-black/50'
|
? "border-l-rose-500/50 bg-black/50"
|
||||||
: 'hover:border-l-rose-500/50 hover:bg-white/5'
|
: "hover:border-l-rose-500/50 hover:bg-white/5"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
|
<button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
|
||||||
<img src={testPlayerIcon} alt="Test Player" className="w-1/2 cursor-pointer" />
|
<img
|
||||||
|
src={testPlayerIcon}
|
||||||
|
alt="Test Player"
|
||||||
|
className="w-1/2 cursor-pointer"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
|
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
|
||||||
Test Player
|
Test Player
|
||||||
@ -114,8 +118,8 @@ function NavBar() {
|
|||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`flex flex-row p-3 border-l-3 ${
|
`flex flex-row p-3 border-l-3 ${
|
||||||
isActive
|
isActive
|
||||||
? 'border-l-yellow-500/50 bg-black/50'
|
? "border-l-yellow-500/50 bg-black/50"
|
||||||
: 'hover:border-l-yellow-500/50 hover:bg-white/5'
|
: "hover:border-l-yellow-500/50 hover:bg-white/5"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -137,7 +141,11 @@ function NavBar() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group"
|
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group"
|
||||||
>
|
>
|
||||||
<img src={discordIcon} alt="Discord" className="w-1/2 group-hover:animate-bounce" />
|
<img
|
||||||
|
src={discordIcon}
|
||||||
|
alt="Discord"
|
||||||
|
className="w-1/2 group-hover:animate-bounce"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={externalLinks.telegram}
|
href={externalLinks.telegram}
|
||||||
@ -145,7 +153,11 @@ function NavBar() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group"
|
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group"
|
||||||
>
|
>
|
||||||
<img src={telegramIcon} alt="Telegram" className="w-1/2 group-hover:animate-bounce" />
|
<img
|
||||||
|
src={telegramIcon}
|
||||||
|
alt="Telegram"
|
||||||
|
className="w-1/2 group-hover:animate-bounce"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={externalLinks.gitea}
|
href={externalLinks.gitea}
|
||||||
|
@ -2,21 +2,21 @@ import { useState } from "react";
|
|||||||
import hamburgerIcon from "../assets/icons/hamburger.svg";
|
import hamburgerIcon from "../assets/icons/hamburger.svg";
|
||||||
|
|
||||||
function NavBarMain({ setIsMenuOpen }) {
|
function NavBarMain({ setIsMenuOpen }) {
|
||||||
const handleMenuToggle = () => {
|
const handleMenuToggle = () => {
|
||||||
setIsMenuOpen((prevState) => !prevState); // Toggle the menu state
|
setIsMenuOpen((prevState) => !prevState); // Toggle the menu state
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row w-full h-full bg-white/1">
|
<div className="flex flex-row w-full h-full bg-white/1">
|
||||||
<button className="w-24 p-4" onClick={handleMenuToggle}>
|
<button className="w-24 p-4" onClick={handleMenuToggle}>
|
||||||
<img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" />
|
<img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
|
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
|
||||||
CDRM-Project
|
CDRM-Project
|
||||||
</p>
|
</p>
|
||||||
<div className="w-24 p-4"></div>
|
<div className="w-24 p-4"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NavBarMain;
|
export default NavBarMain;
|
||||||
|
@ -1,73 +1,73 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { Helmet } from 'react-helmet'; // Import Helmet
|
|
||||||
|
|
||||||
const { protocol, hostname, port } = window.location;
|
const { protocol, hostname, port } = window.location;
|
||||||
|
|
||||||
let fullHost = `${protocol}//${hostname}`;
|
let fullHost = `${protocol}//${hostname}`;
|
||||||
if (
|
if (
|
||||||
(protocol === 'http:' && port !== '80') ||
|
(protocol === "http:" && port !== "80") ||
|
||||||
(protocol === 'https:' && port !== '443' && port !== '')
|
(protocol === "https:" && port !== "443" && port !== "")
|
||||||
) {
|
) {
|
||||||
fullHost += `:${port}`;
|
fullHost += `:${port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function API() {
|
function API() {
|
||||||
const [deviceInfo, setDeviceInfo] = useState({
|
const [deviceInfo, setDeviceInfo] = useState({
|
||||||
device_type: '',
|
device_type: "",
|
||||||
system_id: '',
|
system_id: "",
|
||||||
security_level: '',
|
security_level: "",
|
||||||
host: '',
|
host: "",
|
||||||
secret: '',
|
secret: "",
|
||||||
device_name: ''
|
device_name: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [prDeviceInfo, setPrDeviceInfo] = useState({
|
const [prDeviceInfo, setPrDeviceInfo] = useState({
|
||||||
security_level: '',
|
security_level: "",
|
||||||
host: '',
|
host: "",
|
||||||
secret: '',
|
secret: "",
|
||||||
device_name: ''
|
device_name: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch Widevine info
|
// Fetch Widevine info
|
||||||
fetch('/remotecdm/widevine/deviceinfo')
|
fetch("/remotecdm/widevine/deviceinfo")
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
setDeviceInfo({
|
setDeviceInfo({
|
||||||
device_type: data.device_type,
|
device_type: data.device_type,
|
||||||
system_id: data.system_id,
|
system_id: data.system_id,
|
||||||
security_level: data.security_level,
|
security_level: data.security_level,
|
||||||
host: data.host,
|
host: data.host,
|
||||||
secret: data.secret,
|
secret: data.secret,
|
||||||
device_name: data.device_name
|
device_name: data.device_name,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching Widevine info:', error));
|
.catch((error) => console.error("Error fetching Widevine info:", error));
|
||||||
|
|
||||||
// Fetch PlayReady info
|
// Fetch PlayReady info
|
||||||
fetch('/remotecdm/playready/deviceinfo')
|
fetch("/remotecdm/playready/deviceinfo")
|
||||||
.then(response => response.json())
|
.then((response) => response.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
setPrDeviceInfo({
|
setPrDeviceInfo({
|
||||||
security_level: data.security_level,
|
security_level: data.security_level,
|
||||||
host: data.host,
|
host: data.host,
|
||||||
secret: data.secret,
|
secret: data.secret,
|
||||||
device_name: data.device_name
|
device_name: data.device_name,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error fetching PlayReady info:', error));
|
.catch((error) => console.error("Error fetching PlayReady info:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div className="flex flex-col w-full overflow-y-auto p-4 text-white">
|
document.title = "API | CDRM-Project";
|
||||||
<Helmet>
|
}, []);
|
||||||
<title>API</title>
|
|
||||||
</Helmet>
|
return (
|
||||||
<details open className='w-full list-none'>
|
<div className="flex flex-col w-full overflow-y-auto p-4 text-white">
|
||||||
<summary className='text-2xl'>Sending a decryption request</summary>
|
<details open className="w-full list-none">
|
||||||
<div className='mt-5 p-5 rounded-lg border-2 border-indigo-500/50'>
|
<summary className="text-2xl">Sending a decryption request</summary>
|
||||||
<pre className='rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto'>
|
<div className="mt-5 p-5 rounded-lg border-2 border-indigo-500/50">
|
||||||
{`import requests
|
<pre className="rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto">
|
||||||
|
{`import requests
|
||||||
|
|
||||||
print(requests.post(
|
print(requests.post(
|
||||||
url='${fullHost}/api/decrypt',
|
url='${fullHost}/api/decrypt',
|
||||||
@ -84,14 +84,14 @@ print(requests.post(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
).json()['message'])`}
|
).json()['message'])`}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details open className='w-full list-none mt-5'>
|
<details open className="w-full list-none mt-5">
|
||||||
<summary className='text-2xl'>Sending a search request</summary>
|
<summary className="text-2xl">Sending a search request</summary>
|
||||||
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg'>
|
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg">
|
||||||
<pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
|
<pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
|
||||||
{`import requests
|
{`import requests
|
||||||
|
|
||||||
print(requests.post(
|
print(requests.post(
|
||||||
url='${fullHost}/api/cache/search',
|
url='${fullHost}/api/cache/search',
|
||||||
@ -99,36 +99,40 @@ print(requests.post(
|
|||||||
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
|
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
|
||||||
}
|
}
|
||||||
).json())`}
|
).json())`}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details open className='w-full list-none mt-5'>
|
<details open className="w-full list-none mt-5">
|
||||||
<summary className='text-2xl'>PyWidevine RemoteCDM info</summary>
|
<summary className="text-2xl">PyWidevine RemoteCDM info</summary>
|
||||||
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'>
|
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
|
||||||
<p>
|
<p>
|
||||||
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
|
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
|
||||||
<strong>System ID:</strong> {deviceInfo.system_id}<br />
|
<strong>System ID:</strong> {deviceInfo.system_id}
|
||||||
<strong>Security Level:</strong> {deviceInfo.security_level}<br />
|
<br />
|
||||||
<strong>Host:</strong> {fullHost}/remotecdm/widevine<br />
|
<strong>Security Level:</strong> {deviceInfo.security_level}
|
||||||
<strong>Secret:</strong> '{deviceInfo.secret}'<br />
|
<br />
|
||||||
<strong>Device Name:</strong> {deviceInfo.device_name}
|
<strong>Host:</strong> {fullHost}/remotecdm/widevine
|
||||||
</p>
|
<br />
|
||||||
</div>
|
<strong>Secret:</strong> '{deviceInfo.secret}'<br />
|
||||||
</details>
|
<strong>Device Name:</strong> {deviceInfo.device_name}
|
||||||
<details open className='w-full list-none mt-5'>
|
</p>
|
||||||
<summary className='text-2xl'>PyPlayready RemoteCDM info</summary>
|
</div>
|
||||||
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'>
|
</details>
|
||||||
<p>
|
<details open className="w-full list-none mt-5">
|
||||||
<strong>Security Level:</strong> {prDeviceInfo.security_level}<br />
|
<summary className="text-2xl">PyPlayready RemoteCDM info</summary>
|
||||||
<strong>Host:</strong> {fullHost}/remotecdm/playready<br />
|
<div className="mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto">
|
||||||
<strong>Secret:</strong> '{prDeviceInfo.secret}'<br />
|
<p>
|
||||||
<strong>Device Name:</strong> {prDeviceInfo.device_name}
|
<strong>Security Level:</strong> {prDeviceInfo.security_level}
|
||||||
</p>
|
<br />
|
||||||
</div>
|
<strong>Host:</strong> {fullHost}/remotecdm/playready
|
||||||
</details>
|
<br />
|
||||||
|
<strong>Secret:</strong> '{prDeviceInfo.secret}'<br />
|
||||||
</div>
|
<strong>Device Name:</strong> {prDeviceInfo.device_name}
|
||||||
);
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default API;
|
export default API;
|
||||||
|
@ -3,36 +3,36 @@ import Register from "./Register";
|
|||||||
import MyAccount from "./MyAccount"; // <-- Import the MyAccount component
|
import MyAccount from "./MyAccount"; // <-- Import the MyAccount component
|
||||||
|
|
||||||
function Account() {
|
function Account() {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state
|
const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/login/status', {
|
fetch("/login/status", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
credentials: 'include', // Sends cookies with request
|
credentials: "include", // Sends cookies with request
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
if (data.message === 'True') {
|
if (data.message === "True") {
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
} else {
|
} else {
|
||||||
setIsLoggedIn(false);
|
setIsLoggedIn(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error("Error checking login status:", err);
|
console.error("Error checking login status:", err);
|
||||||
setIsLoggedIn(false); // Assume not logged in on error
|
setIsLoggedIn(false); // Assume not logged in on error
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoggedIn === null) {
|
if (isLoggedIn === null) {
|
||||||
return <div>Loading...</div>; // Optional loading UI
|
return <div>Loading...</div>; // Optional loading UI
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="accountpage" className="w-full h-full flex">
|
<div id="accountpage" className="w-full h-full flex">
|
||||||
{isLoggedIn ? <MyAccount /> : <Register />}
|
{isLoggedIn ? <MyAccount /> : <Register />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Account;
|
export default Account;
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Helmet } from 'react-helmet'; // Import Helmet
|
|
||||||
|
|
||||||
function Cache() {
|
function Cache() {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [cacheData, setCacheData] = useState([]);
|
const [cacheData, setCacheData] = useState([]);
|
||||||
const [keyCount, setKeyCount] = useState(0); // New state to store the key count
|
const [keyCount, setKeyCount] = useState(0); // New state to store the key count
|
||||||
const debounceTimeout = useRef(null);
|
const debounceTimeout = useRef(null);
|
||||||
@ -11,11 +10,11 @@ function Cache() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchKeyCount = async () => {
|
const fetchKeyCount = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/cache/keycount');
|
const response = await fetch("/api/cache/keycount");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setKeyCount(data.count); // Update key count
|
setKeyCount(data.count); // Update key count
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching key count:', error);
|
console.error("Error fetching key count:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -33,7 +32,7 @@ function Cache() {
|
|||||||
|
|
||||||
// Set a new timeout to send the API call after 1 second of no typing
|
// Set a new timeout to send the API call after 1 second of no typing
|
||||||
debounceTimeout.current = setTimeout(() => {
|
debounceTimeout.current = setTimeout(() => {
|
||||||
if (query.trim() !== '') {
|
if (query.trim() !== "") {
|
||||||
sendApiCall(query); // Only call the API if the query is not empty
|
sendApiCall(query); // Only call the API if the query is not empty
|
||||||
} else {
|
} else {
|
||||||
setCacheData([]); // Clear results if query is empty
|
setCacheData([]); // Clear results if query is empty
|
||||||
@ -42,21 +41,22 @@ function Cache() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sendApiCall = (text) => {
|
const sendApiCall = (text) => {
|
||||||
fetch('/api/cache/search', {
|
fetch("/api/cache/search", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ input: text }),
|
body: JSON.stringify({ input: text }),
|
||||||
})
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => setCacheData(data)) // Update cache data with the results
|
.then((data) => setCacheData(data)) // Update cache data with the results
|
||||||
.catch((error) => console.error('Error:', error));
|
.catch((error) => console.error("Error:", error));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = "Cache | CDRM-Project";
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-full overflow-y-auto p-4">
|
<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">
|
<div className="flex flex-col lg:flex-row w-full lg:h-12 items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -1,248 +1,272 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { readTextFromClipboard } from '../Functions/ParseChallenge';
|
import { readTextFromClipboard } from "../Functions/ParseChallenge";
|
||||||
import { Helmet } from 'react-helmet'; // Import Helmet
|
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
const [pssh, setPssh] = useState('');
|
const [pssh, setPssh] = useState("");
|
||||||
const [licurl, setLicurl] = useState('');
|
const [licurl, setLicurl] = useState("");
|
||||||
const [proxy, setProxy] = useState('');
|
const [proxy, setProxy] = useState("");
|
||||||
const [headers, setHeaders] = useState('');
|
const [headers, setHeaders] = useState("");
|
||||||
const [cookies, setCookies] = useState('');
|
const [cookies, setCookies] = useState("");
|
||||||
const [data, setData] = useState('');
|
const [data, setData] = useState("");
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState("");
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [selectedDevice, setSelectedDevice] = useState('default');
|
const [selectedDevice, setSelectedDevice] = useState("default");
|
||||||
|
|
||||||
const bottomRef = useRef(null);
|
useEffect(() => {
|
||||||
const messageRef = useRef(null); // Reference to result container
|
document.title = "Home | CDRM-Project";
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleReset = () => {
|
const bottomRef = useRef(null);
|
||||||
if (isVisible) {
|
const messageRef = useRef(null); // Reference to result container
|
||||||
setIsVisible(false);
|
|
||||||
}
|
|
||||||
setPssh('');
|
|
||||||
setLicurl('');
|
|
||||||
setProxy('');
|
|
||||||
setHeaders('');
|
|
||||||
setCookies('');
|
|
||||||
setData('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmitButton = (event) => {
|
const handleReset = () => {
|
||||||
event.preventDefault();
|
if (isVisible) {
|
||||||
|
setIsVisible(false);
|
||||||
fetch('/api/decrypt', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
pssh: pssh,
|
|
||||||
licurl: licurl,
|
|
||||||
proxy: proxy,
|
|
||||||
headers: headers,
|
|
||||||
cookies: cookies,
|
|
||||||
data: data,
|
|
||||||
device: selectedDevice, // Include selected device in the request
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
const resultMessage = data['message'].replace(/\n/g, '<br />');
|
|
||||||
setMessage(resultMessage);
|
|
||||||
setIsVisible(true);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error during decryption request:', error);
|
|
||||||
setMessage('Error: Unable to process request.');
|
|
||||||
setIsVisible(true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (messageRef.current) {
|
|
||||||
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
|
|
||||||
navigator.clipboard.writeText(textToCopy).catch(err => {
|
|
||||||
alert('Failed to copy!');
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFetchPaste = () => {
|
|
||||||
event.preventDefault();
|
|
||||||
readTextFromClipboard().then(() => {
|
|
||||||
setPssh(document.getElementById("pssh").value);
|
|
||||||
setLicurl(document.getElementById("licurl").value);
|
|
||||||
setHeaders(document.getElementById("headers").value);
|
|
||||||
setData(document.getElementById("data").value);
|
|
||||||
}).catch(err => {
|
|
||||||
alert('Failed to paste from fetch!');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isVisible && bottomRef.current) {
|
|
||||||
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}, [message, isVisible]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/login/status', {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(statusData => {
|
|
||||||
if (statusData.message === 'True') {
|
|
||||||
return fetch('/userinfo', { method: 'POST' });
|
|
||||||
} else {
|
|
||||||
throw new Error('Not logged in');
|
|
||||||
}
|
}
|
||||||
})
|
setPssh("");
|
||||||
.then(res => res.json())
|
setLicurl("");
|
||||||
.then(deviceData => {
|
setProxy("");
|
||||||
const combinedDevices = [
|
setHeaders("");
|
||||||
...deviceData.Widevine_Devices,
|
setCookies("");
|
||||||
...deviceData.Playready_Devices,
|
setData("");
|
||||||
];
|
};
|
||||||
|
|
||||||
// Add default devices if logged in
|
const handleSubmitButton = (event) => {
|
||||||
const allDevices = [
|
event.preventDefault();
|
||||||
"CDRM-Project Public Widevine CDM",
|
|
||||||
"CDRM-Project Public PlayReady CDM",
|
|
||||||
...combinedDevices,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Set devices and select a device if logged in
|
fetch("/api/decrypt", {
|
||||||
setDevices(allDevices.length > 0 ? allDevices : []);
|
method: "POST",
|
||||||
setSelectedDevice(allDevices.length > 0 ? allDevices[0] : 'default');
|
headers: {
|
||||||
})
|
"Content-Type": "application/json",
|
||||||
.catch(() => {
|
},
|
||||||
// User isn't logged in, set default device to 'default'
|
body: JSON.stringify({
|
||||||
setDevices([]); // Don't display devices list
|
pssh: pssh,
|
||||||
setSelectedDevice('default');
|
licurl: licurl,
|
||||||
});
|
proxy: proxy,
|
||||||
}, []);
|
headers: headers,
|
||||||
|
cookies: cookies,
|
||||||
|
data: data,
|
||||||
|
device: selectedDevice, // Include selected device in the request
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
const resultMessage = data["message"].replace(/\n/g, "<br />");
|
||||||
|
setMessage(resultMessage);
|
||||||
|
setIsVisible(true);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error during decryption request:", error);
|
||||||
|
setMessage("Error: Unable to process request.");
|
||||||
|
setIsVisible(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const handleCopy = (event) => {
|
||||||
<>
|
event.preventDefault();
|
||||||
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
|
if (messageRef.current) {
|
||||||
<Helmet>
|
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
|
||||||
<title>CDRM-Project</title>
|
navigator.clipboard.writeText(textToCopy).catch((err) => {
|
||||||
</Helmet>
|
alert("Failed to copy!");
|
||||||
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
|
console.error(err);
|
||||||
<label htmlFor="pssh" className="text-white w-8/10 self-center">PSSH: </label>
|
});
|
||||||
<input
|
}
|
||||||
type="text"
|
};
|
||||||
id="pssh"
|
|
||||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
|
||||||
value={pssh}
|
|
||||||
onChange={(e) => setPssh(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="licurl" className="text-white w-8/10 self-center">License URL: </label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="licurl"
|
|
||||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
|
||||||
value={licurl}
|
|
||||||
onChange={(e) => setLicurl(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="proxy" className="text-white w-8/10 self-center">Proxy: </label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="proxy"
|
|
||||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
|
||||||
value={proxy}
|
|
||||||
onChange={(e) => setProxy(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="headers" className="text-white w-8/10 self-center">Headers: </label>
|
|
||||||
<textarea
|
|
||||||
id="headers"
|
|
||||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
|
||||||
value={headers}
|
|
||||||
onChange={(e) => setHeaders(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="cookies" className="text-white w-8/10 self-center">Cookies: </label>
|
|
||||||
<textarea
|
|
||||||
id="cookies"
|
|
||||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
|
||||||
value={cookies}
|
|
||||||
onChange={(e) => setCookies(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="data" className="text-white w-8/10 self-center">Data: </label>
|
|
||||||
<textarea
|
|
||||||
id="data"
|
|
||||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
|
||||||
value={data}
|
|
||||||
onChange={(e) => setData(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Device Selection Dropdown, only show if logged in */}
|
const handleFetchPaste = () => {
|
||||||
{devices.length > 0 && (
|
event.preventDefault();
|
||||||
<>
|
readTextFromClipboard()
|
||||||
<label htmlFor="device" className="text-white w-8/10 self-center">Select Device:</label>
|
.then(() => {
|
||||||
<select
|
setPssh(document.getElementById("pssh").value);
|
||||||
id="device"
|
setLicurl(document.getElementById("licurl").value);
|
||||||
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
|
setHeaders(document.getElementById("headers").value);
|
||||||
value={selectedDevice}
|
setData(document.getElementById("data").value);
|
||||||
onChange={(e) => setSelectedDevice(e.target.value)}
|
})
|
||||||
>
|
.catch((err) => {
|
||||||
{devices.map((device, index) => (
|
alert("Failed to paste from fetch!");
|
||||||
<option key={index} value={device}>{device}</option>
|
});
|
||||||
))}
|
};
|
||||||
</select>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
|
useEffect(() => {
|
||||||
<button
|
if (isVisible && bottomRef.current) {
|
||||||
type="button"
|
bottomRef.current.scrollIntoView({ behavior: "smooth" });
|
||||||
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
|
}
|
||||||
onClick={handleSubmitButton}
|
}, [message, isVisible]);
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
<button onClick={handleFetchPaste} className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0">
|
|
||||||
Paste from fetch
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
|
|
||||||
onClick={handleReset}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isVisible && (
|
useEffect(() => {
|
||||||
<div id="main_content" className="flex-col w-full h-full p-10 items-center justify-center self-center">
|
fetch("/login/status", {
|
||||||
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
|
method: "POST",
|
||||||
<div className='w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5'>
|
})
|
||||||
<p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
|
.then((res) => res.json())
|
||||||
<p
|
.then((statusData) => {
|
||||||
className="w-full grow pt-10 break-words overflow-y-auto"
|
if (statusData.message === "True") {
|
||||||
ref={messageRef}
|
return fetch("/userinfo", { method: "POST" });
|
||||||
dangerouslySetInnerHTML={{ __html: message }}
|
} else {
|
||||||
/>
|
throw new Error("Not logged in");
|
||||||
<div ref={bottomRef} />
|
}
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((deviceData) => {
|
||||||
|
const combinedDevices = [
|
||||||
|
...deviceData.Widevine_Devices,
|
||||||
|
...deviceData.Playready_Devices,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add default devices if logged in
|
||||||
|
const allDevices = [
|
||||||
|
"CDRM-Project Public Widevine CDM",
|
||||||
|
"CDRM-Project Public PlayReady CDM",
|
||||||
|
...combinedDevices,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set devices and select a device if logged in
|
||||||
|
setDevices(allDevices.length > 0 ? allDevices : []);
|
||||||
|
setSelectedDevice(allDevices.length > 0 ? allDevices[0] : "default");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// User isn't logged in, set default device to 'default'
|
||||||
|
setDevices([]); // Don't display devices list
|
||||||
|
setSelectedDevice("default");
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
|
||||||
|
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
|
||||||
|
<label htmlFor="pssh" className="text-white w-8/10 self-center">
|
||||||
|
PSSH:{" "}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="pssh"
|
||||||
|
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
||||||
|
value={pssh}
|
||||||
|
onChange={(e) => setPssh(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="licurl" className="text-white w-8/10 self-center">
|
||||||
|
License URL:{" "}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="licurl"
|
||||||
|
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
||||||
|
value={licurl}
|
||||||
|
onChange={(e) => setLicurl(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="proxy" className="text-white w-8/10 self-center">
|
||||||
|
Proxy:{" "}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="proxy"
|
||||||
|
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
|
||||||
|
value={proxy}
|
||||||
|
onChange={(e) => setProxy(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="headers" className="text-white w-8/10 self-center">
|
||||||
|
Headers:{" "}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="headers"
|
||||||
|
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
||||||
|
value={headers}
|
||||||
|
onChange={(e) => setHeaders(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="cookies" className="text-white w-8/10 self-center">
|
||||||
|
Cookies:{" "}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="cookies"
|
||||||
|
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
||||||
|
value={cookies}
|
||||||
|
onChange={(e) => setCookies(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="data" className="text-white w-8/10 self-center">
|
||||||
|
Data:{" "}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="data"
|
||||||
|
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
|
||||||
|
value={data}
|
||||||
|
onChange={(e) => setData(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Device Selection Dropdown, only show if logged in */}
|
||||||
|
{devices.length > 0 && (
|
||||||
|
<>
|
||||||
|
<label htmlFor="device" className="text-white w-8/10 self-center">
|
||||||
|
Select Device:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="device"
|
||||||
|
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
|
||||||
|
value={selectedDevice}
|
||||||
|
onChange={(e) => setSelectedDevice(e.target.value)}
|
||||||
|
>
|
||||||
|
{devices.map((device, index) => (
|
||||||
|
<option key={index} value={device}>
|
||||||
|
{device}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
|
||||||
|
onClick={handleSubmitButton}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleFetchPaste}
|
||||||
|
className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
|
||||||
|
>
|
||||||
|
Paste from fetch
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
|
{isVisible && (
|
||||||
<button
|
<div
|
||||||
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
|
id="main_content"
|
||||||
onClick={handleCopy}
|
className="flex-col w-full h-full p-10 items-center justify-center self-center"
|
||||||
>
|
>
|
||||||
Copy Results
|
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
|
||||||
</button>
|
<div className="w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5">
|
||||||
</div>
|
<p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
|
||||||
</div>
|
<p
|
||||||
)}
|
className="w-full grow pt-10 break-words overflow-y-auto"
|
||||||
</>
|
ref={messageRef}
|
||||||
);
|
dangerouslySetInnerHTML={{ __html: message }}
|
||||||
|
/>
|
||||||
|
<div ref={bottomRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
|
||||||
|
<button
|
||||||
|
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
|
||||||
|
onClick={handleCopy}
|
||||||
|
>
|
||||||
|
Copy Results
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HomePage;
|
export default HomePage;
|
||||||
|
@ -1,262 +1,293 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import axios from "axios";
|
||||||
import axios from 'axios';
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
function MyAccount() {
|
function MyAccount() {
|
||||||
const [wvList, setWvList] = useState([]);
|
const [wvList, setWvList] = useState([]);
|
||||||
const [prList, setPrList] = useState([]);
|
const [prList, setPrList] = useState([]);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState("");
|
||||||
const [apiKey, setApiKey] = useState('');
|
const [apiKey, setApiKey] = useState("");
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
const [passwordError, setPasswordError] = useState('');
|
const [passwordError, setPasswordError] = useState("");
|
||||||
const [newApiKey, setNewApiKey] = useState('');
|
const [newApiKey, setNewApiKey] = useState("");
|
||||||
const [apiKeyError, setApiKeyError] = useState('');
|
const [apiKeyError, setApiKeyError] = useState("");
|
||||||
|
|
||||||
// Fetch user info
|
// Fetch user info
|
||||||
const fetchUserInfo = async () => {
|
const fetchUserInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/userinfo');
|
const response = await axios.post("/userinfo");
|
||||||
setWvList(response.data.Widevine_Devices || []);
|
setWvList(response.data.Widevine_Devices || []);
|
||||||
setPrList(response.data.Playready_Devices || []);
|
setPrList(response.data.Playready_Devices || []);
|
||||||
setUsername(response.data.Styled_Username || '');
|
setUsername(response.data.Styled_Username || "");
|
||||||
setApiKey(response.data.API_Key || '');
|
setApiKey(response.data.API_Key || "");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch user info', err);
|
console.error("Failed to fetch user info", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUserInfo();
|
fetchUserInfo();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle file upload
|
useEffect(() => {
|
||||||
const handleUpload = async (event, cdmType) => {
|
document.title = "My account | CDRM-Project";
|
||||||
const file = event.target.files[0];
|
}, []);
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const extension = file.name.split('.').pop();
|
// Handle file upload
|
||||||
if ((cdmType === 'PR' && extension !== 'prd') || (cdmType === 'WV' && extension !== 'wvd')) {
|
const handleUpload = async (event, cdmType) => {
|
||||||
alert(`Please upload a .${cdmType === 'PR' ? 'prd' : 'wvd'} file.`);
|
const file = event.target.files[0];
|
||||||
return;
|
if (!file) return;
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const extension = file.name.split(".").pop();
|
||||||
formData.append('file', file);
|
if (
|
||||||
|
(cdmType === "PR" && extension !== "prd") ||
|
||||||
|
(cdmType === "WV" && extension !== "wvd")
|
||||||
|
) {
|
||||||
|
alert(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setUploading(true);
|
const formData = new FormData();
|
||||||
try {
|
formData.append("file", file);
|
||||||
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
|
setUploading(true);
|
||||||
const handleLogout = async () => {
|
try {
|
||||||
try {
|
await axios.post(`/upload/${cdmType}`, formData);
|
||||||
await axios.post('/logout');
|
await fetchUserInfo(); // Refresh list after upload
|
||||||
window.location.reload();
|
} catch (err) {
|
||||||
} catch (error) {
|
console.error("Upload failed", err);
|
||||||
console.error('Logout failed:', error);
|
alert("Upload failed");
|
||||||
alert('Logout failed!');
|
} finally {
|
||||||
}
|
setUploading(false);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle change password
|
// Handle logout
|
||||||
const handleChangePassword = async () => {
|
const handleLogout = async () => {
|
||||||
if (passwordError || password === '') {
|
try {
|
||||||
alert('Please enter a valid password.');
|
await axios.post("/logout");
|
||||||
return;
|
window.location.reload();
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("Logout failed:", error);
|
||||||
|
alert("Logout failed!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
// Handle change password
|
||||||
const response = await axios.post('/user/change_password', {
|
const handleChangePassword = async () => {
|
||||||
new_password: password
|
if (passwordError || password === "") {
|
||||||
});
|
alert("Please enter a valid password.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.data.message === 'True') {
|
try {
|
||||||
alert('Password changed successfully.');
|
const response = await axios.post("/user/change_password", {
|
||||||
setPassword('');
|
new_password: password,
|
||||||
} 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
|
if (response.data.message === "True") {
|
||||||
const handleChangeApiKey = async () => {
|
alert("Password changed successfully.");
|
||||||
if (apiKeyError || newApiKey === '') {
|
setPassword("");
|
||||||
alert('Please enter a valid API key.');
|
} else {
|
||||||
return;
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
// Handle change API key
|
||||||
const response = await axios.post('/user/change_api_key', {
|
const handleChangeApiKey = async () => {
|
||||||
new_api_key: newApiKey,
|
if (apiKeyError || newApiKey === "") {
|
||||||
});
|
alert("Please enter a valid API key.");
|
||||||
if (response.data.message === 'True') {
|
return;
|
||||||
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 (
|
try {
|
||||||
<div id="myaccount" className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4">
|
const response = await axios.post("/user/change_api_key", {
|
||||||
<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">
|
new_api_key: newApiKey,
|
||||||
<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'}
|
if (response.data.message === "True") {
|
||||||
</h1>
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
{/* API Key Section */}
|
return (
|
||||||
<div className="w-full flex flex-col items-center">
|
<div
|
||||||
<label htmlFor="apiKey" className="text-white font-semibold mb-1">API Key</label>
|
id="myaccount"
|
||||||
<input
|
className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4"
|
||||||
id="apiKey"
|
|
||||||
type="text"
|
|
||||||
value={apiKey}
|
|
||||||
readOnly
|
|
||||||
className="w-full p-2 mb-4 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* New API Key Section */}
|
|
||||||
<label htmlFor="newApiKey" className="text-white font-semibold mt-4 mb-1">New API Key</label>
|
|
||||||
<input
|
|
||||||
id="newApiKey"
|
|
||||||
type="text"
|
|
||||||
value={newApiKey}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
const isValid = /^[^\s]+$/.test(value); // No spaces
|
|
||||||
if (!isValid) {
|
|
||||||
setApiKeyError('API key must not contain spaces.');
|
|
||||||
} else {
|
|
||||||
setApiKeyError('');
|
|
||||||
}
|
|
||||||
setNewApiKey(value);
|
|
||||||
}}
|
|
||||||
placeholder="Enter new API key"
|
|
||||||
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
|
||||||
/>
|
|
||||||
{apiKeyError && <p className="text-red-500 text-sm mb-3">{apiKeyError}</p>}
|
|
||||||
<button
|
|
||||||
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
|
||||||
onClick={handleChangeApiKey}
|
|
||||||
>
|
|
||||||
Change API Key
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Change Password Section */}
|
|
||||||
<label htmlFor="password" className="text-white font-semibold mt-4 mb-1">Change Password</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
const isValid = /^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(value);
|
|
||||||
if (!isValid) {
|
|
||||||
setPasswordError('Password must not contain spaces or invalid characters.');
|
|
||||||
} else {
|
|
||||||
setPasswordError('');
|
|
||||||
}
|
|
||||||
setPassword(value);
|
|
||||||
}}
|
|
||||||
placeholder="New Password"
|
|
||||||
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
|
||||||
/>
|
|
||||||
{passwordError && <p className="text-red-500 text-sm mb-3">{passwordError}</p>}
|
|
||||||
<button
|
|
||||||
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
|
||||||
onClick={handleChangePassword}
|
|
||||||
>
|
|
||||||
Change Password
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
|
||||||
>
|
>
|
||||||
Log out
|
<div className="flex-col w-full min-h-164 lg:h-full lg:w-96 border-2 border-yellow-500/50 rounded-2xl p-4 flex items-center overflow-y-auto">
|
||||||
</button>
|
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
|
||||||
</div>
|
{username ? `${username}` : "My Account"}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0">
|
{/* API Key Section */}
|
||||||
{/* Widevine Section */}
|
<div className="w-full flex flex-col items-center">
|
||||||
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto">
|
<label htmlFor="apiKey" className="text-white font-semibold mb-1">
|
||||||
<h1 className="bg-black text-2xl font-bold text-white border-b-2 border-white p-2">Widevine CDMs</h1>
|
API Key
|
||||||
<div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
|
</label>
|
||||||
{wvList.length === 0 ? (
|
<input
|
||||||
<div className="text-white text-center font-bold">No Widevine CDMs uploaded.</div>
|
id="apiKey"
|
||||||
) : (
|
type="text"
|
||||||
wvList.map((filename, i) => (
|
value={apiKey}
|
||||||
<div
|
readOnly
|
||||||
key={i}
|
className="w-full p-2 mb-4 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
||||||
className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`}
|
/>
|
||||||
>
|
|
||||||
{filename}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
|
|
||||||
{uploading ? 'Uploading...' : 'Upload CDM'}
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".wvd"
|
|
||||||
hidden
|
|
||||||
onChange={(e) => handleUpload(e, 'WV')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Playready Section */}
|
{/* New API Key Section */}
|
||||||
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl p-2 mt-2 lg:mt-2 overflow-y-auto">
|
<label htmlFor="newApiKey" className="text-white font-semibold mt-4 mb-1">
|
||||||
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 bg-black">Playready CDMs</h1>
|
New API Key
|
||||||
<div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2">
|
</label>
|
||||||
{prList.length === 0 ? (
|
<input
|
||||||
<div className="text-white text-center font-bold">No Playready CDMs uploaded.</div>
|
id="newApiKey"
|
||||||
) : (
|
type="text"
|
||||||
prList.map((filename, i) => (
|
value={newApiKey}
|
||||||
<div
|
onChange={(e) => {
|
||||||
key={i}
|
const value = e.target.value;
|
||||||
className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`}
|
const isValid = /^[^\s]+$/.test(value); // No spaces
|
||||||
>
|
if (!isValid) {
|
||||||
{filename}
|
setApiKeyError("API key must not contain spaces.");
|
||||||
|
} else {
|
||||||
|
setApiKeyError("");
|
||||||
|
}
|
||||||
|
setNewApiKey(value);
|
||||||
|
}}
|
||||||
|
placeholder="Enter new API key"
|
||||||
|
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
||||||
|
/>
|
||||||
|
{apiKeyError && <p className="text-red-500 text-sm mb-3">{apiKeyError}</p>}
|
||||||
|
<button
|
||||||
|
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
||||||
|
onClick={handleChangeApiKey}
|
||||||
|
>
|
||||||
|
Change API Key
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Change Password Section */}
|
||||||
|
<label htmlFor="password" className="text-white font-semibold mt-4 mb-1">
|
||||||
|
Change Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const isValid =
|
||||||
|
/^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(value);
|
||||||
|
if (!isValid) {
|
||||||
|
setPasswordError(
|
||||||
|
"Password must not contain spaces or invalid characters."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setPasswordError("");
|
||||||
|
}
|
||||||
|
setPassword(value);
|
||||||
|
}}
|
||||||
|
placeholder="New Password"
|
||||||
|
className="w-full p-2 mb-1 rounded bg-gray-800 text-white border border-gray-600 text-center"
|
||||||
|
/>
|
||||||
|
{passwordError && <p className="text-red-500 text-sm mb-3">{passwordError}</p>}
|
||||||
|
<button
|
||||||
|
className="w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
||||||
|
onClick={handleChangePassword}
|
||||||
|
>
|
||||||
|
Change Password
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))
|
|
||||||
)}
|
<button
|
||||||
</div>
|
onClick={handleLogout}
|
||||||
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
|
className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
|
||||||
{uploading ? 'Uploading...' : 'Upload CDM'}
|
>
|
||||||
<input
|
Log out
|
||||||
type="file"
|
</button>
|
||||||
accept=".prd"
|
</div>
|
||||||
hidden
|
|
||||||
onChange={(e) => handleUpload(e, 'PR')}
|
<div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0">
|
||||||
/>
|
{/* Widevine Section */}
|
||||||
</label>
|
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto">
|
||||||
|
<h1 className="bg-black text-2xl font-bold text-white border-b-2 border-white p-2">
|
||||||
|
Widevine CDMs
|
||||||
|
</h1>
|
||||||
|
<div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
|
||||||
|
{wvList.length === 0 ? (
|
||||||
|
<div className="text-white text-center font-bold">
|
||||||
|
No Widevine CDMs uploaded.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
wvList.map((filename, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`text-center font-bold text-white p-2 rounded ${
|
||||||
|
i % 2 === 0 ? "bg-black/30" : "bg-black/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filename}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
|
||||||
|
{uploading ? "Uploading..." : "Upload CDM"}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".wvd"
|
||||||
|
hidden
|
||||||
|
onChange={(e) => handleUpload(e, "WV")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Playready Section */}
|
||||||
|
<div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl p-2 mt-2 lg:mt-2 overflow-y-auto">
|
||||||
|
<h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 bg-black">
|
||||||
|
Playready CDMs
|
||||||
|
</h1>
|
||||||
|
<div className="flex flex-col w-full bg-white/5 grow rounded-2xl mt-2 text-white text-left p-2">
|
||||||
|
{prList.length === 0 ? (
|
||||||
|
<div className="text-white text-center font-bold">
|
||||||
|
No Playready CDMs uploaded.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
prList.map((filename, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`text-center font-bold text-white p-2 rounded ${
|
||||||
|
i % 2 === 0 ? "bg-black/30" : "bg-black/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{filename}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<label className="bg-yellow-500 text-white w-full min-h-16 lg:min-h-16 mt-4 rounded-2xl flex items-center justify-center cursor-pointer">
|
||||||
|
{uploading ? "Uploading..." : "Upload CDM"}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".prd"
|
||||||
|
hidden
|
||||||
|
onChange={(e) => handleUpload(e, "PR")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyAccount;
|
export default MyAccount;
|
||||||
|
@ -1,117 +1,121 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
function Register() {
|
function Register() {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
const [status, setStatus] = useState('');
|
const [status, setStatus] = useState("");
|
||||||
|
|
||||||
// Validation functions
|
// Validation functions
|
||||||
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
|
const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
|
||||||
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
|
const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
|
||||||
|
|
||||||
const handleRegister = async () => {
|
useEffect(() => {
|
||||||
if (!validateUsername(username)) {
|
document.title = "Register | CDRM-Project";
|
||||||
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
|
}, []);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!validatePassword(password)) {
|
|
||||||
setStatus("Invalid password. Spaces are not allowed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const handleRegister = async () => {
|
||||||
const response = await fetch('/register', {
|
if (!validateUsername(username)) {
|
||||||
method: 'POST',
|
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
|
||||||
headers: {
|
return;
|
||||||
'Content-Type': 'application/json'
|
}
|
||||||
},
|
if (!validatePassword(password)) {
|
||||||
body: JSON.stringify({ username, password })
|
setStatus("Invalid password. Spaces are not allowed.");
|
||||||
});
|
return;
|
||||||
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 () => {
|
try {
|
||||||
if (!validateUsername(username)) {
|
const response = await fetch("/register", {
|
||||||
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
|
method: "POST",
|
||||||
return;
|
headers: {
|
||||||
}
|
"Content-Type": "application/json",
|
||||||
if (!validatePassword(password)) {
|
},
|
||||||
setStatus("Invalid password. Spaces are not allowed.");
|
body: JSON.stringify({ username, password }),
|
||||||
return;
|
});
|
||||||
}
|
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.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
const handleLogin = async () => {
|
||||||
const response = await fetch('/login', {
|
if (!validateUsername(username)) {
|
||||||
method: 'POST',
|
setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
|
||||||
headers: {
|
return;
|
||||||
'Content-Type': 'application/json'
|
}
|
||||||
},
|
if (!validatePassword(password)) {
|
||||||
credentials: 'include', // Important to send cookies
|
setStatus("Invalid password. Spaces are not allowed.");
|
||||||
body: JSON.stringify({ username, password })
|
return;
|
||||||
});
|
}
|
||||||
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 (
|
try {
|
||||||
<div className="flex flex-col w-full h-full items-center justify-center p-4">
|
const response = await fetch("/login", {
|
||||||
<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">
|
method: "POST",
|
||||||
<div className="flex flex-col w-full">
|
headers: {
|
||||||
<label htmlFor="username" className="text-lg font-bold mb-2 text-white">Username:</label>
|
"Content-Type": "application/json",
|
||||||
<input
|
},
|
||||||
type="text"
|
credentials: "include", // Important to send cookies
|
||||||
value={username}
|
body: JSON.stringify({ username, password }),
|
||||||
onChange={e => setUsername(e.target.value)}
|
});
|
||||||
placeholder="Username"
|
const data = await response.json();
|
||||||
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
|
if (data.message) {
|
||||||
/>
|
// Successful login - reload the page to trigger Account check
|
||||||
<label htmlFor="password" className="text-lg font-bold mb-2 text-white">Password:</label>
|
window.location.reload();
|
||||||
<input
|
} else if (data.error) {
|
||||||
type="password"
|
setStatus(data.error);
|
||||||
value={password}
|
}
|
||||||
onChange={e => setPassword(e.target.value)}
|
} catch (err) {
|
||||||
placeholder="Password"
|
setStatus("An error occurred while logging in.");
|
||||||
className="mb-4 p-2 border border-gray-300 rounded text-white bg-transparent"
|
}
|
||||||
/>
|
};
|
||||||
|
|
||||||
|
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>
|
</div>
|
||||||
<div className="flex flex-col lg:flex-row w-8/10 items-center lg:justify-between mt-4">
|
);
|
||||||
<button
|
|
||||||
onClick={handleLogin}
|
|
||||||
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleRegister}
|
|
||||||
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded mt-4 w-1/3"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{status && (
|
|
||||||
<p className="text-sm text-white mt-4 p-4">
|
|
||||||
{status}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Register;
|
export default Register;
|
||||||
|
@ -1,158 +1,152 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import shaka from 'shaka-player';
|
import shaka from "shaka-player";
|
||||||
import { Helmet } from 'react-helmet'; // Import Helmet
|
|
||||||
|
|
||||||
function TestPlayer() {
|
function TestPlayer() {
|
||||||
const [mpdUrl, setMpdUrl] = useState(''); // State to hold the MPD URL
|
const [mpdUrl, setMpdUrl] = useState(""); // State to hold the MPD URL
|
||||||
const [kids, setKids] = useState(''); // State to hold KIDs (separated by line breaks)
|
const [kids, setKids] = useState(""); // State to hold KIDs (separated by line breaks)
|
||||||
const [keys, setKeys] = useState(''); // State to hold Keys (separated by line breaks)
|
const [keys, setKeys] = useState(""); // State to hold Keys (separated by line breaks)
|
||||||
const [headers, setHeaders] = useState(''); // State to hold request headers
|
const [headers, setHeaders] = useState(""); // State to hold request headers
|
||||||
|
|
||||||
const videoRef = useRef(null); // Ref for the video element
|
const videoRef = useRef(null); // Ref for the video element
|
||||||
const playerRef = useRef(null); // Ref for Shaka Player instance
|
const playerRef = useRef(null); // Ref for Shaka Player instance
|
||||||
|
|
||||||
// Function to update the MPD URL state
|
// Function to update the MPD URL state
|
||||||
const handleInputChange = (event) => {
|
const handleInputChange = (event) => {
|
||||||
setMpdUrl(event.target.value);
|
setMpdUrl(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to update KIDs and Keys
|
// Function to update KIDs and Keys
|
||||||
const handleKidsChange = (event) => {
|
const handleKidsChange = (event) => {
|
||||||
setKids(event.target.value);
|
setKids(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeysChange = (event) => {
|
const handleKeysChange = (event) => {
|
||||||
setKeys(event.target.value);
|
setKeys(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHeadersChange = (event) => {
|
const handleHeadersChange = (event) => {
|
||||||
setHeaders(event.target.value);
|
setHeaders(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to initialize Shaka Player
|
// Function to initialize Shaka Player
|
||||||
const initializePlayer = () => {
|
const initializePlayer = () => {
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
// Initialize Shaka Player only if it's not already initialized
|
// Initialize Shaka Player only if it's not already initialized
|
||||||
if (!playerRef.current) {
|
if (!playerRef.current) {
|
||||||
const player = new shaka.Player(videoRef.current);
|
const player = new shaka.Player(videoRef.current);
|
||||||
playerRef.current = player;
|
playerRef.current = player;
|
||||||
|
|
||||||
// Add error listener
|
// Add error listener
|
||||||
player.addEventListener('error', (event) => {
|
player.addEventListener("error", (event) => {
|
||||||
console.error('Error code', event.detail.code, 'object', event.detail);
|
console.error("Error code", event.detail.code, "object", event.detail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle submit and configure player with DRM keys and headers
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (mpdUrl && kids && keys) {
|
||||||
|
// Split the KIDs and Keys by new lines
|
||||||
|
const kidsArray = kids.split("\n").map((k) => k.trim());
|
||||||
|
const keysArray = keys.split("\n").map((k) => k.trim());
|
||||||
|
|
||||||
|
if (kidsArray.length !== keysArray.length) {
|
||||||
|
console.error("The number of KIDs and Keys must be the same.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Shaka Player only when the submit button is pressed
|
||||||
|
const player = new shaka.Player(videoRef.current);
|
||||||
|
|
||||||
|
// Widevine DRM configuration with the provided KIDs and Keys
|
||||||
|
const config = {
|
||||||
|
drm: {
|
||||||
|
clearKeys: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map KIDs to Keys
|
||||||
|
kidsArray.forEach((kid, index) => {
|
||||||
|
config.drm.clearKeys[kid] = keysArray[index];
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Configuring player with the following DRM config and headers:", config);
|
||||||
|
|
||||||
|
// Configure the player with ClearKey DRM and custom headers
|
||||||
|
player.configure(config);
|
||||||
|
|
||||||
|
// Load the video stream with MPD URL
|
||||||
|
player
|
||||||
|
.load(mpdUrl)
|
||||||
|
.then(() => {
|
||||||
|
console.log("Video loaded");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error loading the video", error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("MPD URL, KIDs, and Keys are required.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load the video stream whenever the MPD URL changes
|
||||||
|
useEffect(() => {
|
||||||
|
initializePlayer(); // Initialize the player if it's not initialized already
|
||||||
|
}, []); // This effect runs only once on mount
|
||||||
|
|
||||||
|
// Helper function to parse headers from the textarea input
|
||||||
|
const parseHeaders = (headersText) => {
|
||||||
|
const headersArr = headersText.split("\n");
|
||||||
|
const headersObj = {};
|
||||||
|
headersArr.forEach((line) => {
|
||||||
|
const [key, value] = line.split(":");
|
||||||
|
if (key && value) {
|
||||||
|
headersObj[key.trim()] = value.trim();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
return headersObj;
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Function to handle submit and configure player with DRM keys and headers
|
useEffect(() => {
|
||||||
const handleSubmit = () => {
|
document.title = "Test player | CDRM-Project";
|
||||||
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) {
|
return (
|
||||||
console.error("The number of KIDs and Keys must be the same.");
|
<div className="flex flex-col items-center w-full p-4">
|
||||||
return;
|
<div className="w-full flex flex-col">
|
||||||
}
|
<video ref={videoRef} width="100%" height="auto" controls className="h-96" />
|
||||||
|
<input
|
||||||
// Initialize Shaka Player only when the submit button is pressed
|
type="text"
|
||||||
const player = new shaka.Player(videoRef.current);
|
value={mpdUrl}
|
||||||
|
onChange={handleInputChange}
|
||||||
// Widevine DRM configuration with the provided KIDs and Keys
|
placeholder="MPD URL"
|
||||||
const config = {
|
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"
|
||||||
drm: {
|
/>
|
||||||
clearKeys: {},
|
<textarea
|
||||||
},
|
placeholder="KIDs (one per line)"
|
||||||
};
|
value={kids}
|
||||||
|
onChange={handleKidsChange}
|
||||||
// Map KIDs to Keys
|
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"
|
||||||
kidsArray.forEach((kid, index) => {
|
/>
|
||||||
config.drm.clearKeys[kid] = keysArray[index];
|
<textarea
|
||||||
});
|
placeholder="Keys (one per line)"
|
||||||
|
value={keys}
|
||||||
console.log("Configuring player with the following DRM config and headers:", config);
|
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"
|
||||||
// Configure the player with ClearKey DRM and custom headers
|
/>
|
||||||
player.configure(config);
|
<textarea
|
||||||
|
placeholder="Headers (one per line)"
|
||||||
// Load the video stream with MPD URL
|
value={headers}
|
||||||
player.load(mpdUrl).then(() => {
|
onChange={handleHeadersChange}
|
||||||
console.log('Video loaded');
|
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"
|
||||||
}).catch((error) => {
|
/>
|
||||||
console.error('Error loading the video', error);
|
<button onClick={handleSubmit} className="mt-4 p-2 bg-blue-500 text-white rounded">
|
||||||
});
|
Submit
|
||||||
} else {
|
</button>
|
||||||
console.error('MPD URL, KIDs, and Keys are required.');
|
</div>
|
||||||
}
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
// Load the video stream whenever the MPD URL changes
|
|
||||||
useEffect(() => {
|
|
||||||
initializePlayer(); // Initialize the player if it's not initialized already
|
|
||||||
}, []); // This effect runs only once on mount
|
|
||||||
|
|
||||||
// Helper function to parse headers from the textarea input
|
|
||||||
const parseHeaders = (headersText) => {
|
|
||||||
const headersArr = headersText.split('\n');
|
|
||||||
const headersObj = {};
|
|
||||||
headersArr.forEach((line) => {
|
|
||||||
const [key, value] = line.split(':');
|
|
||||||
if (key && value) {
|
|
||||||
headersObj[key.trim()] = value.trim();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return headersObj;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center w-full p-4">
|
|
||||||
<Helmet>
|
|
||||||
<title>Test Player</title>
|
|
||||||
</Helmet>
|
|
||||||
<div className="w-full flex flex-col">
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
width="100%"
|
|
||||||
height="auto"
|
|
||||||
controls
|
|
||||||
className="h-96"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={mpdUrl}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="MPD URL"
|
|
||||||
className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
placeholder="KIDs (one per line)"
|
|
||||||
value={kids}
|
|
||||||
onChange={handleKidsChange}
|
|
||||||
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
placeholder="Keys (one per line)"
|
|
||||||
value={keys}
|
|
||||||
onChange={handleKeysChange}
|
|
||||||
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
placeholder="Headers (one per line)"
|
|
||||||
value={headers}
|
|
||||||
onChange={handleHeadersChange}
|
|
||||||
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="mt-4 p-2 bg-blue-500 text-white rounded"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TestPlayer;
|
export default TestPlayer;
|
||||||
|
@ -1,178 +1,182 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from "react-router-dom";
|
||||||
import closeIcon from '../assets/icons/close.svg';
|
import closeIcon from "../assets/icons/close.svg";
|
||||||
import homeIcon from '../assets/icons/home.svg';
|
import homeIcon from "../assets/icons/home.svg";
|
||||||
import cacheIcon from '../assets/icons/cache.svg';
|
import cacheIcon from "../assets/icons/cache.svg";
|
||||||
import apiIcon from '../assets/icons/api.svg';
|
import apiIcon from "../assets/icons/api.svg";
|
||||||
import testPlayerIcon from '../assets/icons/testplayer.svg';
|
import testPlayerIcon from "../assets/icons/testplayer.svg";
|
||||||
import accountIcon from '../assets/icons/account.svg';
|
import accountIcon from "../assets/icons/account.svg";
|
||||||
import discordIcon from '../assets/icons/discord.svg';
|
import discordIcon from "../assets/icons/discord.svg";
|
||||||
import telegramIcon from '../assets/icons/telegram.svg';
|
import telegramIcon from "../assets/icons/telegram.svg";
|
||||||
import giteaIcon from '../assets/icons/gitea.svg';
|
import giteaIcon from "../assets/icons/gitea.svg";
|
||||||
|
|
||||||
function SideMenu({ isMenuOpen, setIsMenuOpen }) {
|
function SideMenu({ isMenuOpen, setIsMenuOpen }) {
|
||||||
const [externalLinks, setExternalLinks] = useState({
|
const [externalLinks, setExternalLinks] = useState({
|
||||||
discord: '#',
|
discord: "#",
|
||||||
telegram: '#',
|
telegram: "#",
|
||||||
gitea: '#',
|
gitea: "#",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/links')
|
fetch("/api/links")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => setExternalLinks(data))
|
.then((data) => setExternalLinks(data))
|
||||||
.catch((err) => console.error('Failed to fetch links:', err));
|
.catch((err) => console.error("Failed to fetch links:", err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${
|
className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${
|
||||||
isMenuOpen ? 'translate-x-0' : '-translate-x-full'
|
isMenuOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
} z-50`}
|
} z-50`}
|
||||||
style={{ transitionDuration: '0.3s' }}
|
style={{ transitionDuration: "0.3s" }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col bg-gray-950/55 h-full">
|
<div className="flex flex-col bg-gray-950/55 h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
|
<div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
|
||||||
<div className="w-1/4 h-full"></div>
|
<div className="w-1/4 h-full"></div>
|
||||||
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
|
<p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
|
||||||
CDRM-Project
|
CDRM-Project
|
||||||
</p>
|
</p>
|
||||||
<div className="w-1/4 h-full">
|
<div className="w-1/4 h-full">
|
||||||
<button
|
<button
|
||||||
className="w-full h-full flex items-center justify-center"
|
className="w-full h-full flex items-center justify-center"
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
>
|
>
|
||||||
<img src={closeIcon} alt="Close" className="w-1/2 h-1/2 cursor-pointer" />
|
<img
|
||||||
</button>
|
src={closeIcon}
|
||||||
</div>
|
alt="Close"
|
||||||
|
className="w-1/2 h-1/2 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable Navigation Links */}
|
||||||
|
<div className="overflow-y-auto flex flex-col p-5 w-full flex-grow">
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<NavLink
|
||||||
|
to="/"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
||||||
|
isActive
|
||||||
|
? "border-l-sky-500/50 bg-black/50 text-white"
|
||||||
|
: "border-transparent hover:border-l-sky-500/50 hover:bg-white/5 text-white/80"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<img src={homeIcon} alt="Home" className="w-5 h-5" />
|
||||||
|
<span className="text-lg">Home</span>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink
|
||||||
|
to="/cache"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
||||||
|
isActive
|
||||||
|
? "border-l-emerald-500/50 bg-black/50 text-white"
|
||||||
|
: "border-transparent hover:border-l-emerald-500/50 hover:bg-white/5 text-white/80"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<img src={cacheIcon} alt="Cache" className="w-5 h-5" />
|
||||||
|
<span className="text-lg">Cache</span>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink
|
||||||
|
to="/api"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
||||||
|
isActive
|
||||||
|
? "border-l-indigo-500/50 bg-black/50 text-white"
|
||||||
|
: "border-transparent hover:border-l-indigo-500/50 hover:bg-white/5 text-white/80"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<img src={apiIcon} alt="API" className="w-5 h-5" />
|
||||||
|
<span className="text-lg">API</span>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink
|
||||||
|
to="/testplayer"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
||||||
|
isActive
|
||||||
|
? "border-l-rose-700/50 bg-black/50 text-white"
|
||||||
|
: "border-transparent hover:border-l-rose-700/50 hover:bg-white/5 text-white/80"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<img src={testPlayerIcon} alt="Test Player" className="w-5 h-5" />
|
||||||
|
<span className="text-lg">Test Player</span>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* My Account Link at the Bottom of Scrollable Area */}
|
||||||
|
<div className="mt-auto pt-4">
|
||||||
|
<NavLink
|
||||||
|
to="/account"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
||||||
|
isActive
|
||||||
|
? "border-l-yellow-500/50 bg-black/50 text-white"
|
||||||
|
: "border-transparent hover:border-l-yellow-500/50 hover:bg-white/5 text-white/80"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<img src={accountIcon} alt="My Account" className="w-5 h-5" />
|
||||||
|
<span className="text-lg">My Account</span>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* External Links */}
|
||||||
|
<div className="h-16 w-full flex flex-row bg-black/5">
|
||||||
|
<a
|
||||||
|
href={externalLinks.discord}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-950 group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={discordIcon}
|
||||||
|
alt="Discord"
|
||||||
|
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={externalLinks.telegram}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-400 group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={telegramIcon}
|
||||||
|
alt="Telegram"
|
||||||
|
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={externalLinks.gitea}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-1/3 h-full flex items-center justify-center hover:bg-green-700 group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={giteaIcon}
|
||||||
|
alt="Gitea"
|
||||||
|
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{/* Scrollable Navigation Links */}
|
|
||||||
<div className="overflow-y-auto flex flex-col p-5 w-full flex-grow">
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<NavLink
|
|
||||||
to="/"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
|
||||||
isActive
|
|
||||||
? 'border-l-sky-500/50 bg-black/50 text-white'
|
|
||||||
: 'border-transparent hover:border-l-sky-500/50 hover:bg-white/5 text-white/80'
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
onClick={() => setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
<img src={homeIcon} alt="Home" className="w-5 h-5" />
|
|
||||||
<span className="text-lg">Home</span>
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<NavLink
|
|
||||||
to="/cache"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
|
||||||
isActive
|
|
||||||
? 'border-l-emerald-500/50 bg-black/50 text-white'
|
|
||||||
: 'border-transparent hover:border-l-emerald-500/50 hover:bg-white/5 text-white/80'
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
onClick={() => setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
<img src={cacheIcon} alt="Cache" className="w-5 h-5" />
|
|
||||||
<span className="text-lg">Cache</span>
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<NavLink
|
|
||||||
to="/api"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
|
||||||
isActive
|
|
||||||
? 'border-l-indigo-500/50 bg-black/50 text-white'
|
|
||||||
: 'border-transparent hover:border-l-indigo-500/50 hover:bg-white/5 text-white/80'
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
onClick={() => setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
<img src={apiIcon} alt="API" className="w-5 h-5" />
|
|
||||||
<span className="text-lg">API</span>
|
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
<NavLink
|
|
||||||
to="/testplayer"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
|
||||||
isActive
|
|
||||||
? 'border-l-rose-700/50 bg-black/50 text-white'
|
|
||||||
: 'border-transparent hover:border-l-rose-700/50 hover:bg-white/5 text-white/80'
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
onClick={() => setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
<img src={testPlayerIcon} alt="Test Player" className="w-5 h-5" />
|
|
||||||
<span className="text-lg">Test Player</span>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* My Account Link at the Bottom of Scrollable Area */}
|
|
||||||
<div className="mt-auto pt-4">
|
|
||||||
<NavLink
|
|
||||||
to="/account"
|
|
||||||
className={({ isActive }) =>
|
|
||||||
`flex flex-row items-center gap-3 p-3 border-l-4 ${
|
|
||||||
isActive
|
|
||||||
? 'border-l-yellow-500/50 bg-black/50 text-white'
|
|
||||||
: 'border-transparent hover:border-l-yellow-500/50 hover:bg-white/5 text-white/80'
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
onClick={() => setIsMenuOpen(false)}
|
|
||||||
>
|
|
||||||
<img src={accountIcon} alt="My Account" className="w-5 h-5" />
|
|
||||||
<span className="text-lg">My Account</span>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* External Links */}
|
|
||||||
<div className="h-16 w-full flex flex-row bg-black/5">
|
|
||||||
<a
|
|
||||||
href={externalLinks.discord}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-950 group"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={discordIcon}
|
|
||||||
alt="Discord"
|
|
||||||
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={externalLinks.telegram}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="w-1/3 h-full flex items-center justify-center hover:bg-blue-400 group"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={telegramIcon}
|
|
||||||
alt="Telegram"
|
|
||||||
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={externalLinks.gitea}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="w-1/3 h-full flex items-center justify-center hover:bg-green-700 group"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={giteaIcon}
|
|
||||||
alt="Gitea"
|
|
||||||
className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SideMenu;
|
export default SideMenu;
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
details summary::-webkit-details-marker {
|
details summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
details summary {
|
details summary {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import './index.css'
|
import "./index.css";
|
||||||
import App from './App.jsx'
|
import App from "./App.jsx";
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById("root")).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
);
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
})
|
});
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
"""Icon links module for social media links."""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'discord': 'https://discord.cdrm-project.com/',
|
"discord": "https://discord.cdrm-project.com/",
|
||||||
'telegram': 'https://telegram.cdrm-project.com/',
|
"telegram": "https://telegram.cdrm-project.com/",
|
||||||
'gitea': 'https://cdm-project.com/tpd94/cdrm-project'
|
"gitea": "https://cdm-project.com/tpd94/cdrm-project",
|
||||||
}
|
}
|
@ -1,47 +1,49 @@
|
|||||||
|
"""Index tags module for the index page."""
|
||||||
|
|
||||||
tags = {
|
tags = {
|
||||||
'index': {
|
"index": {
|
||||||
'description': 'Decrypt Widevine and PlayReady protected content',
|
"description": "Decrypt Widevine and PlayReady protected content",
|
||||||
'keywords': 'CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption',
|
"keywords": "CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption",
|
||||||
'opengraph_title': 'CDRM-Project',
|
"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_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_image": "https://cdrm-project.com/og-home.jpg",
|
||||||
'opengraph_url': 'https://cdm-project.com/tpd94/cdrm-project',
|
"opengraph_url": "https://cdm-project.com/tpd94/cdrm-project",
|
||||||
'tab_title': 'CDRM-Project',
|
"tab_title": "CDRM-Project",
|
||||||
},
|
},
|
||||||
'cache': {
|
"cache": {
|
||||||
'description': 'Search the cache by KID or PSSH for decryption keys',
|
"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',
|
"keywords": "Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption",
|
||||||
'opengraph_title': 'Search the Cache',
|
"opengraph_title": "Search the Cache",
|
||||||
'opengraph_description': 'Search the cache by KID or PSSH for decryption keys',
|
"opengraph_description": "Search the cache by KID or PSSH for decryption keys",
|
||||||
'opengraph_image': 'https://cdrm-project.com/og-cache.jpg',
|
"opengraph_image": "https://cdrm-project.com/og-cache.jpg",
|
||||||
'opengraph_url': 'https://cdrm-project.com/cache',
|
"opengraph_url": "https://cdrm-project.com/cache",
|
||||||
'tab_title': 'Cache',
|
"tab_title": "Cache",
|
||||||
},
|
},
|
||||||
'testplayer': {
|
"testplayer": {
|
||||||
'description': 'Shaka Player for testing decryption keys',
|
"description": "Shaka Player for testing decryption keys",
|
||||||
'keywords': 'Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY',
|
"keywords": "Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY",
|
||||||
'opengraph_title': 'Test Player',
|
"opengraph_title": "Test Player",
|
||||||
'opengraph_description': 'Shaka Player for testing decryption keys',
|
"opengraph_description": "Shaka Player for testing decryption keys",
|
||||||
'opengraph_image': 'https://cdrm-project.com/og-testplayer.jpg',
|
"opengraph_image": "https://cdrm-project.com/og-testplayer.jpg",
|
||||||
'opengraph_url': 'https://cdrm-project.com/testplayer',
|
"opengraph_url": "https://cdrm-project.com/testplayer",
|
||||||
'tab_title': 'Test Player',
|
"tab_title": "Test Player",
|
||||||
},
|
},
|
||||||
'api': {
|
"api": {
|
||||||
'description': 'API documentation for the program "CDRM-Project"',
|
"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',
|
"keywords": "API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault",
|
||||||
'opengraph_title': 'API',
|
"opengraph_title": "API",
|
||||||
'opengraph_description': 'Documentation for the program "CDRM-Project"',
|
"opengraph_description": 'Documentation for the program "CDRM-Project"',
|
||||||
'opengraph_image': 'https://cdrm-project.com/og-api.jpg',
|
"opengraph_image": "https://cdrm-project.com/og-api.jpg",
|
||||||
'opengraph_url': 'https://cdrm-project.com/api',
|
"opengraph_url": "https://cdrm-project.com/api",
|
||||||
'tab_title': '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',
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,28 +1,33 @@
|
|||||||
|
"""Module to cache data to MariaDB."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_db_config():
|
def get_db_config():
|
||||||
# Configure your MariaDB connection
|
"""Get the database configuration for MariaDB."""
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
with open(
|
||||||
|
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
|
||||||
|
) as file:
|
||||||
config = yaml.safe_load(file)
|
config = yaml.safe_load(file)
|
||||||
db_config = {
|
db_config = {
|
||||||
'host': f'{config["mariadb"]["host"]}',
|
"host": f'{config["mariadb"]["host"]}',
|
||||||
'user': f'{config["mariadb"]["user"]}',
|
"user": f'{config["mariadb"]["user"]}',
|
||||||
'password': f'{config["mariadb"]["password"]}',
|
"password": f'{config["mariadb"]["password"]}',
|
||||||
'database': f'{config["mariadb"]["database"]}'
|
"database": f'{config["mariadb"]["database"]}',
|
||||||
}
|
}
|
||||||
return db_config
|
return db_config
|
||||||
|
|
||||||
|
|
||||||
def create_database():
|
def create_database():
|
||||||
|
"""Create the database for MariaDB."""
|
||||||
try:
|
try:
|
||||||
with mysql.connector.connect(**get_db_config()) as conn:
|
with mysql.connector.connect(**get_db_config()) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS licenses (
|
CREATE TABLE IF NOT EXISTS licenses (
|
||||||
SERVICE VARCHAR(255),
|
SERVICE VARCHAR(255),
|
||||||
PSSH TEXT,
|
PSSH TEXT,
|
||||||
@ -33,20 +38,33 @@ def create_database():
|
|||||||
Cookies TEXT,
|
Cookies TEXT,
|
||||||
Data BLOB
|
Data BLOB
|
||||||
)
|
)
|
||||||
''')
|
"""
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
|
||||||
def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, headers=None, cookies=None, data=None):
|
|
||||||
|
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."""
|
||||||
try:
|
try:
|
||||||
with mysql.connector.connect(**get_db_config()) as conn:
|
with mysql.connector.connect(**get_db_config()) as conn:
|
||||||
cursor = conn.cursor()
|
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()
|
existing_record = cursor.fetchone()
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
|
INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
@ -57,7 +75,9 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h
|
|||||||
Headers = VALUES(Headers),
|
Headers = VALUES(Headers),
|
||||||
Cookies = VALUES(Cookies),
|
Cookies = VALUES(Cookies),
|
||||||
Data = VALUES(Data)
|
Data = VALUES(Data)
|
||||||
''', (service, pssh, kid, key, license_url, headers, cookies, data))
|
""",
|
||||||
|
(service, pssh, kid, key, license_url, headers, cookies, data),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
return True if existing_record else False
|
return True if existing_record else False
|
||||||
@ -65,61 +85,84 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h
|
|||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def search_by_pssh_or_kid(search_filter):
|
def search_by_pssh_or_kid(search_filter):
|
||||||
|
"""Search the database by PSSH or KID for MariaDB."""
|
||||||
results = set()
|
results = set()
|
||||||
try:
|
try:
|
||||||
with mysql.connector.connect(**get_db_config()) as conn:
|
with mysql.connector.connect(**get_db_config()) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
like_filter = f"%{search_filter}%"
|
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())
|
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())
|
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]
|
return final_results[:20]
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def get_key_by_kid_and_service(kid, service):
|
def get_key_by_kid_and_service(kid, service):
|
||||||
|
"""Get the key by KID and service for MariaDB."""
|
||||||
try:
|
try:
|
||||||
with mysql.connector.connect(**get_db_config()) as conn:
|
with mysql.connector.connect(**get_db_config()) as conn:
|
||||||
cursor = conn.cursor()
|
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()
|
result = cursor.fetchone()
|
||||||
return result[0] if result else None
|
return result[0] if result else None
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_kid_key_dict(service_name):
|
def get_kid_key_dict(service_name):
|
||||||
|
"""Get the KID and key dictionary for MariaDB."""
|
||||||
try:
|
try:
|
||||||
with mysql.connector.connect(**get_db_config()) as conn:
|
with mysql.connector.connect(**get_db_config()) as conn:
|
||||||
cursor = conn.cursor()
|
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()}
|
return {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def get_unique_services():
|
def get_unique_services():
|
||||||
|
"""Get the unique services for MariaDB."""
|
||||||
try:
|
try:
|
||||||
with mysql.connector.connect(**get_db_config()) as conn:
|
with mysql.connector.connect(**get_db_config()) as conn:
|
||||||
cursor = conn.cursor()
|
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()]
|
return [row[0] for row in cursor.fetchall()]
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def key_count():
|
def key_count():
|
||||||
|
"""Get the key count for MariaDB."""
|
||||||
try:
|
try:
|
||||||
with mysql.connector.connect(**get_db_config()) as conn:
|
with mysql.connector.connect(**get_db_config()) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT COUNT(KID) FROM licenses')
|
cursor.execute("SELECT COUNT(KID) FROM licenses")
|
||||||
return cursor.fetchone()[0]
|
return cursor.fetchone()[0]
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
|
"""Module to cache data to SQLite."""
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def create_database():
|
def create_database():
|
||||||
# Using with statement to manage the connection and cursor
|
"""Create the database for SQLite."""
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS licenses (
|
CREATE TABLE IF NOT EXISTS licenses (
|
||||||
SERVICE TEXT,
|
SERVICE TEXT,
|
||||||
PSSH TEXT,
|
PSSH TEXT,
|
||||||
@ -16,92 +22,138 @@ def create_database():
|
|||||||
Cookies TEXT,
|
Cookies TEXT,
|
||||||
Data TEXT
|
Data TEXT
|
||||||
)
|
)
|
||||||
''')
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
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:
|
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:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Check if the record with the given KID already exists
|
# 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()
|
existing_record = cursor.fetchone()
|
||||||
|
|
||||||
# Insert or replace the record
|
# Insert or replace the record
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data)
|
INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
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)
|
# If the record was existing and updated, return True (updated), else return False (added)
|
||||||
return True if existing_record else False
|
return True if existing_record else False
|
||||||
|
|
||||||
|
|
||||||
def search_by_pssh_or_kid(search_filter):
|
def search_by_pssh_or_kid(search_filter):
|
||||||
# Using with statement to automatically close the connection
|
"""Search the database by PSSH or KID for SQLite."""
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Initialize a set to store unique matching records
|
# Initialize a set to store unique matching records
|
||||||
results = set()
|
results = set()
|
||||||
|
|
||||||
# Search for records where PSSH contains the search_filter
|
# Search for records where PSSH contains the search_filter
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
SELECT * FROM licenses WHERE PSSH LIKE ?
|
SELECT * FROM licenses WHERE PSSH LIKE ?
|
||||||
''', ('%' + search_filter + '%',))
|
""",
|
||||||
|
("%" + search_filter + "%",),
|
||||||
|
)
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
|
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
|
||||||
|
|
||||||
# Search for records where KID contains the search_filter
|
# Search for records where KID contains the search_filter
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
SELECT * FROM licenses WHERE KID LIKE ?
|
SELECT * FROM licenses WHERE KID LIKE ?
|
||||||
''', ('%' + search_filter + '%',))
|
""",
|
||||||
|
("%" + search_filter + "%",),
|
||||||
|
)
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
|
results.add((row[1], row[2], row[3])) # (PSSH, KID, Key)
|
||||||
|
|
||||||
# Convert the set of results to a list of dictionaries for output
|
# 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]
|
return final_results[:20]
|
||||||
|
|
||||||
|
|
||||||
def get_key_by_kid_and_service(kid, service):
|
def get_key_by_kid_and_service(kid, service):
|
||||||
# Using 'with' to automatically close the connection when done
|
"""Get the key by KID and service for SQLite."""
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Query to search by KID and SERVICE
|
# Query to search by KID and SERVICE
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ?
|
SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ?
|
||||||
''', (kid, service))
|
""",
|
||||||
|
(kid, service),
|
||||||
|
)
|
||||||
|
|
||||||
# Fetch the result
|
# Fetch the result
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
# Check if a result was found
|
# 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):
|
def get_kid_key_dict(service_name):
|
||||||
# Using with statement to automatically manage the connection and cursor
|
"""Get the KID and key dictionary for SQLite."""
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Query to fetch KID and Key for the selected service
|
# Query to fetch KID and Key for the selected service
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
SELECT KID, Key FROM licenses WHERE SERVICE = ?
|
SELECT KID, Key FROM licenses WHERE SERVICE = ?
|
||||||
''', (service_name,))
|
""",
|
||||||
|
(service_name,),
|
||||||
|
)
|
||||||
|
|
||||||
# Fetch all results and create the dictionary
|
# Fetch all results and create the dictionary
|
||||||
kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()}
|
kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()}
|
||||||
|
|
||||||
return kid_key_dict
|
return kid_key_dict
|
||||||
|
|
||||||
|
|
||||||
def get_unique_services():
|
def get_unique_services():
|
||||||
# Using with statement to automatically manage the connection and cursor
|
"""Get the unique services for SQLite."""
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Query to get distinct services from the 'licenses' table
|
# 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
|
# Fetch all results and extract the unique services
|
||||||
services = cursor.fetchall()
|
services = cursor.fetchall()
|
||||||
@ -111,13 +163,16 @@ def get_unique_services():
|
|||||||
|
|
||||||
return unique_services
|
return unique_services
|
||||||
|
|
||||||
|
|
||||||
def key_count():
|
def key_count():
|
||||||
# Using with statement to automatically manage the connection and cursor
|
"""Get the key count for SQLite."""
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Count the number of KID entries in the licenses table
|
# 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
|
count = cursor.fetchone()[0] # Fetch the result and get the count
|
||||||
|
|
||||||
return count
|
return count
|
||||||
|
159
custom_functions/database/unified_db_ops.py
Normal file
159
custom_functions/database/unified_db_ops.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
"""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()
|
@ -1,30 +1,43 @@
|
|||||||
|
"""Module to manage the user database."""
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
|
|
||||||
def create_user_database():
|
def create_user_database():
|
||||||
os.makedirs(f'{os.getcwd()}/databases/sql', exist_ok=True)
|
"""Create the user database."""
|
||||||
|
os.makedirs(os.path.join(os.getcwd(), "databases", "sql"), exist_ok=True)
|
||||||
|
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "users.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS user_info (
|
CREATE TABLE IF NOT EXISTS user_info (
|
||||||
Username TEXT PRIMARY KEY,
|
Username TEXT PRIMARY KEY,
|
||||||
Password TEXT,
|
Password TEXT,
|
||||||
Styled_Username TEXT,
|
Styled_Username TEXT,
|
||||||
API_Key TEXT
|
API_Key TEXT
|
||||||
)
|
)
|
||||||
''')
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_user(username, password, api_key):
|
def add_user(username, password, api_key):
|
||||||
hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
"""Add a user to the database."""
|
||||||
|
hashed_pw = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||||
|
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "users.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
try:
|
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, Styled_Username, API_Key) VALUES (?, ?, ?, ?)",
|
||||||
|
(username.lower(), hashed_pw, username, api_key),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
@ -32,69 +45,100 @@ def add_user(username, password, api_key):
|
|||||||
|
|
||||||
|
|
||||||
def verify_user(username, password):
|
def verify_user(username, password):
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
|
"""Verify a user."""
|
||||||
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "users.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),))
|
cursor.execute(
|
||||||
|
"SELECT Password FROM user_info WHERE Username = ?", (username.lower(),)
|
||||||
|
)
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
stored_hash = result[0]
|
stored_hash = result[0]
|
||||||
# Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT)
|
# Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT)
|
||||||
if isinstance(stored_hash, str):
|
if isinstance(stored_hash, str):
|
||||||
stored_hash = stored_hash.encode('utf-8')
|
stored_hash = stored_hash.encode("utf-8")
|
||||||
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
|
return bcrypt.checkpw(password.encode("utf-8"), stored_hash)
|
||||||
else:
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
def fetch_api_key(username):
|
def fetch_api_key(username):
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
|
"""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 = conn.cursor()
|
||||||
cursor.execute('SELECT API_Key FROM user_info WHERE Username = ?', (username.lower(),))
|
cursor.execute(
|
||||||
|
"SELECT API_Key FROM user_info WHERE Username = ?", (username.lower(),)
|
||||||
|
)
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return result[0]
|
return result[0]
|
||||||
else:
|
return None
|
||||||
return None
|
|
||||||
|
|
||||||
def change_password(username, new_password):
|
def change_password(username, new_password):
|
||||||
|
"""Change the password for a user."""
|
||||||
# Hash the new password
|
# Hash the new password
|
||||||
new_hashed_pw = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
|
new_hashed_pw = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt())
|
||||||
|
|
||||||
# Update the password in the database
|
# Update the password in the database
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "users.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('UPDATE user_info SET Password = ? WHERE Username = ?', (new_hashed_pw, username.lower()))
|
cursor.execute(
|
||||||
|
"UPDATE user_info SET Password = ? WHERE Username = ?",
|
||||||
|
(new_hashed_pw, username.lower()),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def change_api_key(username, new_api_key):
|
def change_api_key(username, new_api_key):
|
||||||
|
"""Change the API key for a user."""
|
||||||
# Update the API key in the database
|
# Update the API key in the database
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
|
with sqlite3.connect(
|
||||||
|
os.path.join(os.getcwd(), "databases", "sql", "users.db")
|
||||||
|
) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('UPDATE user_info SET API_Key = ? WHERE Username = ?', (new_api_key, username.lower()))
|
cursor.execute(
|
||||||
|
"UPDATE user_info SET API_Key = ? WHERE Username = ?",
|
||||||
|
(new_api_key, username.lower()),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def fetch_styled_username(username):
|
def fetch_styled_username(username):
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
|
"""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 = conn.cursor()
|
||||||
cursor.execute('SELECT Styled_Username FROM user_info WHERE Username = ?', (username.lower(),))
|
cursor.execute(
|
||||||
|
"SELECT Styled_Username FROM user_info WHERE Username = ?",
|
||||||
|
(username.lower(),),
|
||||||
|
)
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return result[0]
|
return result[0]
|
||||||
else:
|
return None
|
||||||
return None
|
|
||||||
|
|
||||||
def fetch_username_by_api_key(api_key):
|
def fetch_username_by_api_key(api_key):
|
||||||
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
|
"""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 = conn.cursor()
|
||||||
cursor.execute('SELECT Username FROM user_info WHERE API_Key = ?', (api_key,))
|
cursor.execute("SELECT Username FROM user_info WHERE API_Key = ?", (api_key,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
return result[0] # Return the username
|
return result[0] # Return the username
|
||||||
else:
|
return None # If no user is found for the API key
|
||||||
return None # If no user is found for the API key
|
|
||||||
|
@ -1,31 +1,45 @@
|
|||||||
|
"""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.cdm import Cdm as widevineCdm
|
||||||
from pywidevine.device import Device as widevineDevice
|
from pywidevine.device import Device as widevineDevice
|
||||||
from pywidevine.pssh import PSSH as widevinePSSH
|
from pywidevine.pssh import PSSH as widevinePSSH
|
||||||
from pyplayready.cdm import Cdm as playreadyCdm
|
from pyplayready.cdm import Cdm as playreadyCdm
|
||||||
from pyplayready.device import Device as playreadyDevice
|
from pyplayready.device import Device as playreadyDevice
|
||||||
from pyplayready.system.pssh import PSSH as playreadyPSSH
|
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):
|
def find_license_key(data, keywords=None):
|
||||||
|
"""Find the license key in the data."""
|
||||||
if keywords is None:
|
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 the data is a dictionary, check each key
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if any(keyword in key.lower() for keyword in
|
if any(
|
||||||
keywords): # Check if any keyword is in the key (case-insensitive)
|
keyword in key.lower() for keyword in keywords
|
||||||
return value.replace("-", "+").replace("_", "/") # Return the value immediately when found
|
): # 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
|
# Recursively check if the value is a dictionary or list
|
||||||
if isinstance(value, (dict, list)):
|
if isinstance(value, (dict, list)):
|
||||||
result = find_license_key(value, keywords) # Recursively search
|
result = find_license_key(value, keywords) # Recursively search
|
||||||
@ -43,37 +57,52 @@ def find_license_key(data, keywords=None):
|
|||||||
|
|
||||||
|
|
||||||
def find_license_challenge(data, keywords=None, new_value=None):
|
def find_license_challenge(data, keywords=None, new_value=None):
|
||||||
|
"""Find the license challenge in the data."""
|
||||||
if keywords is None:
|
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 the data is a dictionary, check each key
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
for key, value in data.items():
|
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
|
data[key] = new_value # Modify the value in-place
|
||||||
# Recursively check if the value is a dictionary or list
|
# Recursively check if the value is a dictionary or list
|
||||||
elif isinstance(value, (dict, 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
|
# If the data is a list, iterate through each item
|
||||||
elif isinstance(data, list):
|
elif isinstance(data, list):
|
||||||
for i, item in enumerate(data):
|
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)
|
return data # Return the modified original data (no new structure is created)
|
||||||
|
|
||||||
|
|
||||||
def is_base64(string):
|
def is_base64(string):
|
||||||
|
"""Check if the string is base64 encoded."""
|
||||||
try:
|
try:
|
||||||
# Try decoding the string
|
# Try decoding the string
|
||||||
decoded_data = base64.b64decode(string)
|
decoded_data = base64.b64decode(string)
|
||||||
# Check if the decoded data, when re-encoded, matches the original string
|
# Check if the decoded data, when re-encoded, matches the original string
|
||||||
return base64.b64encode(decoded_data).decode('utf-8') == string
|
return base64.b64encode(decoded_data).decode("utf-8") == string
|
||||||
except Exception:
|
except (binascii.Error, TypeError):
|
||||||
# If decoding or encoding fails, it's not Base64
|
# If decoding or encoding fails, it's not Base64
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_url_and_split(input_str):
|
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)
|
parsed = urlparse(input_str)
|
||||||
|
|
||||||
# Check if it's a valid URL with scheme and netloc
|
# Check if it's a valid URL with scheme and netloc
|
||||||
@ -81,364 +110,286 @@ def is_url_and_split(input_str):
|
|||||||
protocol = parsed.scheme
|
protocol = parsed.scheme
|
||||||
fqdn = parsed.netloc
|
fqdn = parsed.netloc
|
||||||
return True, protocol, fqdn
|
return True, protocol, fqdn
|
||||||
else:
|
return False, None, None
|
||||||
return False, None, None
|
|
||||||
|
|
||||||
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}')
|
def load_device(device_type, device, username, config):
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
"""Load the appropriate device file for PlayReady or Widevine."""
|
||||||
config = yaml.safe_load(file)
|
if device_type == "PR":
|
||||||
if config['database_type'].lower() == 'sqlite':
|
ext, config_key, class_loader = ".prd", "default_pr_cdm", playreadyDevice.load
|
||||||
from custom_functions.database.cache_to_db_sqlite import cache_to_db
|
base_dir = "PR"
|
||||||
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': 'No PSSH provided'
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
if "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh): # PR
|
|
||||||
try:
|
|
||||||
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': 'No default .prd file found'
|
|
||||||
}
|
|
||||||
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'Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port'
|
|
||||||
}
|
|
||||||
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 processing PSSH\n\n{error}'
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
|
ext, config_key, class_loader = ".wvd", "default_wv_cdm", widevineDevice.load
|
||||||
|
base_dir = "WV"
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
wv_pssh = widevinePSSH(pssh)
|
format_json_data = ast.literal_eval(json_data)
|
||||||
except Exception as error:
|
if is_widevine:
|
||||||
return {
|
format_json_data = find_license_challenge(
|
||||||
'status': 'error',
|
data=format_json_data,
|
||||||
'message': f'An error occurred processing PSSH\n\n{error}'
|
new_value=base64.b64encode(challenge).decode(),
|
||||||
}
|
)
|
||||||
try:
|
except (ValueError, SyntaxError) as e:
|
||||||
if device == 'public':
|
raise ValueError(f"Invalid json_data format: {e}") from e
|
||||||
base_name = config["default_wv_cdm"]
|
except (TypeError, AttributeError) as e:
|
||||||
if not base_name.endswith(".wvd"):
|
raise ValueError(f"Error processing json_data: {e}") from e
|
||||||
base_name += ".wvd"
|
|
||||||
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
|
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:
|
else:
|
||||||
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
|
returned_keys += f"{kid.hex}:{key.key.hex()}"
|
||||||
if wvd_files:
|
return returned_keys, None
|
||||||
wv_device = widevineDevice.load(wvd_files[0])
|
except AttributeError as error:
|
||||||
else:
|
return None, f"Error accessing CDM keys: {error}"
|
||||||
return {
|
except (TypeError, ValueError) as error:
|
||||||
'status': 'error',
|
return None, f"Error processing keys: {error}"
|
||||||
'message': 'No default .wvd file found'
|
|
||||||
}
|
|
||||||
else:
|
def api_decrypt(
|
||||||
base_name = device
|
pssh: str = "",
|
||||||
if not base_name.endswith(".wvd"):
|
license_url: str = "",
|
||||||
base_name += ".wvd"
|
proxy: str = "",
|
||||||
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
|
headers: str = "",
|
||||||
else:
|
cookies: str = "",
|
||||||
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
|
json_data: str = "",
|
||||||
if wvd_files:
|
device: str = "public",
|
||||||
wv_device = widevineDevice.load(wvd_files[0])
|
username: str = "",
|
||||||
else:
|
):
|
||||||
return {
|
"""Decrypt the license using the API."""
|
||||||
'status': 'error',
|
print(f"Using device {device} for user {username}")
|
||||||
'message': f'{base_name} does not exist'
|
with open(f"{os.getcwd()}/configs/config.yaml", "r", encoding="utf-8") as file:
|
||||||
}
|
config = yaml.safe_load(file)
|
||||||
except Exception as error:
|
|
||||||
|
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:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": f"An error occurred processing PSSH\n\n{error}",
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
'status': 'error',
|
"status": "error",
|
||||||
'message': f'An error occurred location Widevine CDM file\n\n{error}'
|
"message": "Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port",
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
wv_cdm = widevineCdm.from_device(wv_device)
|
# Send license request
|
||||||
except Exception as error:
|
licence, req_err = send_license_request(
|
||||||
return {
|
license_url,
|
||||||
'status': 'error',
|
format_headers,
|
||||||
'message': f'An error occurred loading Widevine CDM\n\n{error}'
|
format_cookies,
|
||||||
}
|
format_json_data,
|
||||||
try:
|
challenge,
|
||||||
wv_session_id = wv_cdm.open()
|
proxies,
|
||||||
except Exception as error:
|
)
|
||||||
return {
|
if licence is None:
|
||||||
'status': 'error',
|
return {"status": "error", "message": req_err}
|
||||||
'message': f'An error occurred opening a CDM session\n\n{error}'
|
|
||||||
}
|
# Parse license
|
||||||
try:
|
try:
|
||||||
wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh)
|
if is_pr:
|
||||||
except Exception as error:
|
cdm.parse_license(session_id, licence.text)
|
||||||
return {
|
else:
|
||||||
'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:
|
try:
|
||||||
license_json = licence.json()
|
cdm.parse_license(session_id, licence.content) # type: ignore[arg-type]
|
||||||
license_value = find_license_key(license_json)
|
except (ValueError, TypeError):
|
||||||
wv_cdm.parse_license(wv_session_id, license_value)
|
# Try to extract license from JSON
|
||||||
except Exception as error:
|
try:
|
||||||
return {
|
license_json = licence.json()
|
||||||
'status': 'error',
|
license_value = find_license_key(license_json)
|
||||||
'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
|
if license_value is not None:
|
||||||
}
|
cdm.parse_license(session_id, license_value)
|
||||||
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:
|
else:
|
||||||
returned_keys += f"{key.kid.hex}:{key.key.hex()}"
|
return {
|
||||||
except Exception as error:
|
"status": "error",
|
||||||
return {
|
"message": f"Could not extract license from JSON: {license_json}",
|
||||||
'status': 'error',
|
}
|
||||||
'message': f'An error occurred formatting keys\n\n{error}'
|
except (ValueError, json.JSONDecodeError, AttributeError) as error:
|
||||||
}
|
return {
|
||||||
try:
|
"status": "error",
|
||||||
wv_cdm.close(wv_session_id)
|
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
|
||||||
except Exception as error:
|
}
|
||||||
return {
|
except (ValueError, TypeError, AttributeError) as error:
|
||||||
'status': 'error',
|
return {
|
||||||
'message': f'An error occurred closing session\n\n{error}'
|
"status": "error",
|
||||||
}
|
"message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
|
||||||
try:
|
}
|
||||||
return {
|
|
||||||
'status': 'success',
|
# Extract and cache keys
|
||||||
'message': returned_keys
|
returned_keys, key_err = extract_and_cache_keys(
|
||||||
}
|
cdm,
|
||||||
except Exception as error:
|
session_id,
|
||||||
return {
|
cache_to_db,
|
||||||
'status': 'error',
|
pssh,
|
||||||
'message': f'An error occurred getting returned_keys\n\n{error}'
|
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}
|
||||||
|
@ -1,68 +1,106 @@
|
|||||||
|
"""Module to check for and download CDM files."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
CONFIG_PATH = os.path.join(os.getcwd(), "configs", "config.yaml")
|
||||||
|
|
||||||
|
|
||||||
def check_for_wvd_cdm():
|
def load_config():
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
"""Load the config file."""
|
||||||
config = yaml.safe_load(file)
|
with open(CONFIG_PATH, "r", encoding="utf-8") as file:
|
||||||
if config['default_wv_cdm'] == '':
|
return yaml.safe_load(file)
|
||||||
answer = ' '
|
|
||||||
while answer[0].upper() != 'Y' and answer[0].upper() != 'N':
|
|
||||||
answer = input('No default Widevine CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ')
|
def save_config(config):
|
||||||
if answer[0].upper() == 'Y':
|
"""Save the config file."""
|
||||||
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd')
|
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:
|
if response.status_code == 200:
|
||||||
with open(f'{os.getcwd()}/configs/CDMs/WV/public.wvd', 'wb') as file:
|
file_path = os.path.join(cdm_dir_path, f"public.{file_ext}")
|
||||||
|
with open(file_path, "wb") as file:
|
||||||
file.write(response.content)
|
file.write(response.content)
|
||||||
config['default_wv_cdm'] = 'public'
|
config[config_key] = "public"
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
|
save_config(config)
|
||||||
yaml.dump(config, file)
|
print(f"Successfully downloaded {cdm_name} CDM")
|
||||||
print("Successfully downloaded Widevine CDM")
|
|
||||||
else:
|
else:
|
||||||
exit(f"Download failed, please try again or place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml")
|
sys.exit(
|
||||||
if answer[0].upper() == 'N':
|
f"Download failed, please try again, or place a .{file_ext} file "
|
||||||
exit(f"Place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml")
|
f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
|
||||||
else:
|
)
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
|
sys.exit(
|
||||||
|
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
|
||||||
def check_for_prd_cdm():
|
)
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
else:
|
||||||
config = yaml.safe_load(file)
|
base_name = (
|
||||||
if config['default_pr_cdm'] == '':
|
cdm_value
|
||||||
answer = ' '
|
if cdm_value.endswith(f".{file_ext}")
|
||||||
while answer[0].upper() != 'Y' and answer[0].upper() != 'N':
|
else f"{cdm_value}.{file_ext}"
|
||||||
answer = input('No default PlayReady CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ')
|
)
|
||||||
if answer[0].upper() == 'Y':
|
file_path = os.path.join(cdm_dir_path, base_name)
|
||||||
response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd')
|
if os.path.exists(file_path):
|
||||||
|
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:
|
if response.status_code == 200:
|
||||||
with open(f'{os.getcwd()}/configs/CDMs/PR/public.prd', 'wb') as file:
|
with open(file_path, "wb") as file:
|
||||||
file.write(response.content)
|
file.write(response.content)
|
||||||
config['default_pr_cdm'] = 'public'
|
config[config_key] = base_name.replace(f".{file_ext}", "")
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
|
save_config(config)
|
||||||
yaml.dump(config, file)
|
print(f"Successfully downloaded {cdm_name} CDM")
|
||||||
print("Successfully downloaded PlayReady CDM")
|
|
||||||
else:
|
else:
|
||||||
exit(f"Download failed, please try again or place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml")
|
sys.exit(
|
||||||
if answer[0].upper() == 'N':
|
f"Download failed, please try again, or place a .{file_ext} file "
|
||||||
exit(f"Place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml")
|
f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
|
||||||
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:
|
else:
|
||||||
exit(f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
|
sys.exit(
|
||||||
|
f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_for_cdms():
|
def check_for_cdms():
|
||||||
check_for_wvd_cdm()
|
"""Check for CDM files."""
|
||||||
check_for_prd_cdm()
|
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",
|
||||||
|
)
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
"""Module to check for the config file."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def check_for_config_file():
|
def check_for_config_file():
|
||||||
if os.path.exists(f'{os.getcwd()}/configs/config.yaml'):
|
"""Check for the config file."""
|
||||||
|
if os.path.exists(os.path.join(os.getcwd(), "configs", "config.yaml")):
|
||||||
return
|
return
|
||||||
else:
|
default_config = """
|
||||||
default_config = """\
|
|
||||||
default_wv_cdm: ''
|
default_wv_cdm: ''
|
||||||
default_pr_cdm: ''
|
default_pr_cdm: ''
|
||||||
secret_key_flask: 'secretkey'
|
secret_key_flask: 'secretkey'
|
||||||
@ -21,6 +24,8 @@ remote_cdm_secret: ''
|
|||||||
# port: ''
|
# port: ''
|
||||||
# database: ''
|
# database: ''
|
||||||
"""
|
"""
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'w') as f:
|
with open(
|
||||||
|
os.path.join(os.getcwd(), "configs", "config.yaml"), "w", encoding="utf-8"
|
||||||
|
) as f:
|
||||||
f.write(default_config)
|
f.write(default_config)
|
||||||
return
|
return
|
@ -1,37 +1,159 @@
|
|||||||
|
"""Module to check for the database with unified backend support."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from typing import Dict, Any
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
def check_for_sqlite_database():
|
from custom_functions.database.unified_db_ops import (
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
db_ops,
|
||||||
config = yaml.safe_load(file)
|
get_backend_info,
|
||||||
if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
|
key_count,
|
||||||
return
|
)
|
||||||
else:
|
from custom_functions.database.user_db import create_user_database
|
||||||
if config['database_type'].lower() != 'mariadb':
|
|
||||||
from custom_functions.database.cache_to_db_sqlite import create_database
|
|
||||||
create_database()
|
def get_database_config() -> Dict[str, Any]:
|
||||||
return
|
"""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:
|
else:
|
||||||
return
|
print(f"SQLite database found at: {sqlite_path}")
|
||||||
|
|
||||||
def check_for_user_database():
|
|
||||||
if os.path.exists(f'{os.getcwd()}/databases/users.db'):
|
def check_for_mariadb_database() -> None:
|
||||||
return
|
"""Check for the MariaDB database and create if needed."""
|
||||||
else:
|
config = get_database_config()
|
||||||
from custom_functions.database.user_db import create_user_database
|
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()
|
create_user_database()
|
||||||
|
print(f"User database created at: {user_db_path}")
|
||||||
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:
|
else:
|
||||||
return
|
print(f"User database found at: {user_db_path}")
|
||||||
|
|
||||||
def check_for_sql_database():
|
|
||||||
check_for_sqlite_database()
|
def check_for_sql_database() -> None:
|
||||||
check_for_mariadb_database()
|
"""Check for the SQL database based on configuration."""
|
||||||
|
print("=== Database Check Starting ===")
|
||||||
|
|
||||||
|
# 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()
|
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")
|
||||||
|
@ -1,42 +1,51 @@
|
|||||||
|
"""Module to check for the folders."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def check_for_config_folder():
|
def check_for_config_folder():
|
||||||
if os.path.isdir(f'{os.getcwd()}/configs'):
|
"""Check for the config folder."""
|
||||||
return
|
if os.path.isdir(os.path.join(os.getcwd(), "configs")):
|
||||||
else:
|
|
||||||
os.mkdir(f'{os.getcwd()}/configs')
|
|
||||||
return
|
return
|
||||||
|
os.mkdir(os.path.join(os.getcwd(), "configs"))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def check_for_database_folder():
|
def check_for_database_folder():
|
||||||
if os.path.isdir(f'{os.getcwd()}/databases'):
|
"""Check for the database folder."""
|
||||||
return
|
if os.path.isdir(os.path.join(os.getcwd(), "databases")):
|
||||||
else:
|
|
||||||
os.mkdir(f'{os.getcwd()}/databases')
|
|
||||||
os.mkdir(f'{os.getcwd()}/databases/sql')
|
|
||||||
return
|
return
|
||||||
|
os.mkdir(os.path.join(os.getcwd(), "databases"))
|
||||||
|
os.mkdir(os.path.join(os.getcwd(), "databases", "sql"))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def check_for_cdm_folder():
|
def check_for_cdm_folder():
|
||||||
if os.path.isdir(f'{os.getcwd()}/configs/CDMs'):
|
"""Check for the CDM folder."""
|
||||||
return
|
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs")):
|
||||||
else:
|
|
||||||
os.mkdir(f'{os.getcwd()}/configs/CDMs')
|
|
||||||
return
|
return
|
||||||
|
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs"))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def check_for_wv_cdm_folder():
|
def check_for_wv_cdm_folder():
|
||||||
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'):
|
"""Check for the Widevine CDM folder."""
|
||||||
return
|
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV")):
|
||||||
else:
|
|
||||||
os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
|
|
||||||
return
|
return
|
||||||
|
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV"))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def check_for_cdm_pr_folder():
|
def check_for_cdm_pr_folder():
|
||||||
if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'):
|
"""Check for the PlayReady CDM folder."""
|
||||||
return
|
if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR")):
|
||||||
else:
|
|
||||||
os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
|
|
||||||
return
|
return
|
||||||
|
os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR"))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def folder_checks():
|
def folder_checks():
|
||||||
|
"""Check for the folders."""
|
||||||
check_for_config_folder()
|
check_for_config_folder()
|
||||||
check_for_database_folder()
|
check_for_database_folder()
|
||||||
check_for_cdm_folder()
|
check_for_cdm_folder()
|
||||||
|
@ -1,11 +1,29 @@
|
|||||||
|
"""Module to run the prechecks."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from custom_functions.prechecks.folder_checks import folder_checks
|
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.config_file_checks import check_for_config_file
|
||||||
from custom_functions.prechecks.database_checks import check_for_sql_database
|
from custom_functions.prechecks.database_checks import check_for_sql_database
|
||||||
from custom_functions.prechecks.cdm_checks import check_for_cdms
|
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():
|
def run_precheck():
|
||||||
|
"""Run the prechecks."""
|
||||||
|
check_frontend_built()
|
||||||
folder_checks()
|
folder_checks()
|
||||||
check_for_config_file()
|
check_for_config_file()
|
||||||
check_for_cdms()
|
check_for_cdms()
|
||||||
check_for_sql_database()
|
check_for_sql_database()
|
||||||
return
|
|
@ -1,49 +1,52 @@
|
|||||||
|
"""Module to check for the Python version and environment."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import venv
|
import venv
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
|
||||||
def version_check():
|
def version_check():
|
||||||
major_version = sys.version_info.major
|
"""Check for the Python version."""
|
||||||
minor_version = sys.version_info.minor
|
if sys.version_info < (3, 12):
|
||||||
|
sys.exit("Python version 3.12 or higher is required")
|
||||||
|
|
||||||
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():
|
def pip_check():
|
||||||
try:
|
"""Check for the pip installation."""
|
||||||
import pip
|
if importlib.util.find_spec("pip") is None:
|
||||||
return
|
sys.exit("Pip is not installed")
|
||||||
except ImportError:
|
|
||||||
exit("Pip is not installed")
|
|
||||||
|
|
||||||
def venv_check():
|
def venv_check():
|
||||||
# Check if we're already inside a virtual environment
|
"""Check for the virtual environment."""
|
||||||
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
|
if hasattr(sys, "real_prefix") or (
|
||||||
|
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
venv_path = os.path.join(os.getcwd(), 'cdrm-venv')
|
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_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):
|
if os.path.exists(venv_path):
|
||||||
subprocess.call([venv_python] + sys.argv)
|
subprocess.call([venv_python] + sys.argv)
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
# Ask user for permission to create a virtual environment
|
answer = (
|
||||||
answer = ''
|
input(
|
||||||
while not answer or answer[0].upper() not in {'Y', 'N'}:
|
"Program is not running from a virtual environment. To maintain "
|
||||||
answer = input(
|
"compatibility, this program must be run from one.\n"
|
||||||
'Program is not running from a venv. To maintain compatibility and dependencies, this program must be run from one.\n'
|
"Would you like to create one? (Y/N): "
|
||||||
'Would you like me to create one for you? (Y/N): '
|
|
||||||
)
|
)
|
||||||
|
.strip()
|
||||||
if answer[0].upper() == 'Y':
|
.upper()
|
||||||
|
)
|
||||||
|
if answer.startswith("Y"):
|
||||||
print("Creating virtual environment...")
|
print("Creating virtual environment...")
|
||||||
venv.create(venv_path, with_pip=True)
|
venv.create(venv_path, with_pip=True)
|
||||||
subprocess.call([venv_python] + sys.argv)
|
subprocess.call([venv_python] + sys.argv)
|
||||||
@ -54,30 +57,55 @@ def venv_check():
|
|||||||
|
|
||||||
|
|
||||||
def requirements_check():
|
def requirements_check():
|
||||||
try:
|
"""Check for the requirements."""
|
||||||
import pywidevine
|
required_packages = [
|
||||||
import pyplayready
|
"pywidevine",
|
||||||
import flask
|
"pyplayready",
|
||||||
import flask_cors
|
"flask",
|
||||||
import yaml
|
"flask_cors",
|
||||||
import mysql.connector
|
"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:
|
||||||
return
|
return
|
||||||
except ImportError:
|
|
||||||
while True:
|
while True:
|
||||||
user_input = input("Missing packages. Do you want to install them? (Y/N): ").strip().upper()
|
user_input = (
|
||||||
if user_input == 'Y':
|
input(
|
||||||
print("Installing packages from requirements.txt...")
|
f"Missing packages: {', '.join(missing)}. Do you want to install them? (Y/N): "
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
|
)
|
||||||
print("Installation complete.")
|
.strip()
|
||||||
break
|
.upper()
|
||||||
elif user_input == 'N':
|
)
|
||||||
print("Dependencies required, please install them and run again.")
|
if user_input == "Y":
|
||||||
sys.exit()
|
print("Installing packages from requirements.txt...")
|
||||||
else:
|
subprocess.check_call(
|
||||||
print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
|
[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.")
|
||||||
|
|
||||||
|
|
||||||
def run_python_checks():
|
def run_python_checks():
|
||||||
if getattr(sys, 'frozen', False): # Check if running from PyInstaller
|
"""Run the Python checks."""
|
||||||
|
if getattr(sys, "frozen", False): # Check if running from PyInstaller
|
||||||
return
|
return
|
||||||
version_check()
|
version_check()
|
||||||
pip_check()
|
pip_check()
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
|
"""Module to check if the user is allowed to use the device."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
|
|
||||||
def user_allowed_to_use_device(device, username):
|
def user_allowed_to_use_device(device, username):
|
||||||
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
|
"""Check if the user is allowed to use the device."""
|
||||||
|
base_path = os.path.join(os.getcwd(), "configs", "CDMs", username)
|
||||||
|
|
||||||
# Get filenames with extensions
|
# Get filenames with extensions
|
||||||
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
|
pr_files = [
|
||||||
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
|
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
|
# Combine all filenames
|
||||||
all_files = pr_files + wv_files
|
all_files = pr_files + wv_files
|
||||||
|
28
main.py
28
main.py
@ -1,9 +1,10 @@
|
|||||||
from custom_functions.prechecks.python_checks import run_python_checks
|
"""Main module to run the application."""
|
||||||
run_python_checks()
|
|
||||||
from custom_functions.prechecks.precheck import run_precheck
|
import os
|
||||||
run_precheck()
|
import yaml
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
from routes.react import react_bp
|
from routes.react import react_bp
|
||||||
from routes.api import api_bp
|
from routes.api import api_bp
|
||||||
from routes.remote_device_wv import remotecdm_wv_bp
|
from routes.remote_device_wv import remotecdm_wv_bp
|
||||||
@ -13,12 +14,19 @@ from routes.user_info import user_info_bp
|
|||||||
from routes.register import register_bp
|
from routes.register import register_bp
|
||||||
from routes.login import login_bp
|
from routes.login import login_bp
|
||||||
from routes.user_changes import user_change_bp
|
from routes.user_changes import user_change_bp
|
||||||
import os
|
from custom_functions.prechecks.python_checks import run_python_checks
|
||||||
import yaml
|
from custom_functions.prechecks.precheck import run_precheck
|
||||||
|
|
||||||
|
run_python_checks()
|
||||||
|
run_precheck()
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
with open(
|
||||||
|
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
|
||||||
|
) as file:
|
||||||
config = yaml.safe_load(file)
|
config = yaml.safe_load(file)
|
||||||
app.secret_key = config['secret_key_flask']
|
app.secret_key = config["secret_key_flask"]
|
||||||
|
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
@ -33,5 +41,5 @@ app.register_blueprint(remotecdm_wv_bp)
|
|||||||
app.register_blueprint(remotecdm_pr_bp)
|
app.register_blueprint(remotecdm_pr_bp)
|
||||||
app.register_blueprint(user_change_bp)
|
app.register_blueprint(user_change_bp)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, host='0.0.0.0')
|
app.run(debug=True, host="0.0.0.0")
|
||||||
|
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[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
|
||||||
|
)/
|
||||||
|
'''
|
@ -1,9 +1,10 @@
|
|||||||
Flask~=3.1.0
|
Flask
|
||||||
Flask-Cors
|
Flask-Cors
|
||||||
pywidevine~=1.8.0
|
pywidevine~=1.8.0
|
||||||
pyplayready~=0.6.0
|
pyplayready~=0.6.0
|
||||||
requests~=2.32.3
|
requests
|
||||||
protobuf~=4.25.6
|
protobuf~=4.25.6
|
||||||
PyYAML~=6.0.2
|
PyYAML
|
||||||
mysql-connector-python
|
mysql-connector-python
|
||||||
bcrypt
|
bcrypt
|
||||||
|
black
|
||||||
|
402
routes/api.py
402
routes/api.py
@ -1,113 +1,140 @@
|
|||||||
|
"""Module to handle the API routes."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from flask import Blueprint, jsonify, request, send_file, session
|
|
||||||
import json
|
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 shutil
|
||||||
import math
|
import math
|
||||||
import yaml
|
|
||||||
import mysql.connector
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
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
|
from configs.icon_links import data as icon_data
|
||||||
|
|
||||||
api_bp = Blueprint('api', __name__)
|
api_bp = Blueprint("api", __name__)
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
with open(os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8") as file:
|
||||||
config = yaml.safe_load(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():
|
def get_db_config():
|
||||||
# Configure your MariaDB connection
|
"""Get the MariaDB database configuration."""
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
with open(
|
||||||
config = yaml.safe_load(file)
|
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
|
||||||
|
) as file_mariadb:
|
||||||
|
config_mariadb = yaml.safe_load(file_mariadb)
|
||||||
db_config = {
|
db_config = {
|
||||||
'host': f'{config["mariadb"]["host"]}',
|
"host": f'{config_mariadb["mariadb"]["host"]}',
|
||||||
'user': f'{config["mariadb"]["user"]}',
|
"user": f'{config_mariadb["mariadb"]["user"]}',
|
||||||
'password': f'{config["mariadb"]["password"]}',
|
"password": f'{config_mariadb["mariadb"]["password"]}',
|
||||||
'database': f'{config["mariadb"]["database"]}'
|
"database": f'{config_mariadb["mariadb"]["database"]}',
|
||||||
}
|
}
|
||||||
return db_config
|
return db_config
|
||||||
|
|
||||||
@api_bp.route('/api/cache/search', methods=['POST'])
|
|
||||||
|
@api_bp.route("/api/cache/search", methods=["POST"])
|
||||||
def get_data():
|
def get_data():
|
||||||
search_argument = json.loads(request.data)['input']
|
"""Get the data from the database."""
|
||||||
|
search_argument = json.loads(request.data)["input"]
|
||||||
results = search_by_pssh_or_kid(search_filter=search_argument)
|
results = search_by_pssh_or_kid(search_filter=search_argument)
|
||||||
return jsonify(results)
|
return jsonify(results)
|
||||||
|
|
||||||
@api_bp.route('/api/cache/<service>/<kid>', methods=['GET'])
|
|
||||||
def get_single_key_service(service, kid):
|
|
||||||
result = get_key_by_kid_and_service(kid=kid, service=service)
|
|
||||||
return jsonify({
|
|
||||||
'code': 0,
|
|
||||||
'content_key': result,
|
|
||||||
})
|
|
||||||
|
|
||||||
@api_bp.route('/api/cache/<service>', 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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/api/cache/<service>", methods=["GET"])
|
||||||
def get_multiple_key_service(service):
|
def get_multiple_key_service(service):
|
||||||
|
"""Get the multiple keys from the database."""
|
||||||
result = get_kid_key_dict(service_name=service)
|
result = get_kid_key_dict(service_name=service)
|
||||||
pages = math.ceil(len(result) / 10)
|
pages = math.ceil(len(result) / 10)
|
||||||
return jsonify({
|
return jsonify({"code": 0, "content_keys": result, "pages": pages})
|
||||||
'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):
|
def add_single_key_service(service, kid):
|
||||||
|
"""Add the single key to the database."""
|
||||||
body = request.get_json()
|
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)
|
result = cache_to_db(service=service, kid=kid, key=content_key)
|
||||||
if result:
|
if result:
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'code': 0,
|
{
|
||||||
'updated': True,
|
"code": 0,
|
||||||
})
|
"updated": True,
|
||||||
elif result is False:
|
}
|
||||||
return jsonify({
|
)
|
||||||
'code': 0,
|
return jsonify(
|
||||||
'updated': True,
|
{
|
||||||
})
|
"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):
|
def add_multiple_key_service(service):
|
||||||
|
"""Add the multiple keys to the database."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
keys_added = 0
|
keys_added = 0
|
||||||
keys_updated = 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)
|
result = cache_to_db(service=service, kid=kid, key=key)
|
||||||
if result is True:
|
if result is True:
|
||||||
keys_updated += 1
|
keys_updated += 1
|
||||||
elif result is False:
|
else:
|
||||||
keys_added += 1
|
keys_added += 1
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'code': 0,
|
{
|
||||||
'added': str(keys_added),
|
"code": 0,
|
||||||
'updated': str(keys_updated),
|
"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():
|
def unique_service():
|
||||||
|
"""Get the unique services from the database."""
|
||||||
services = get_unique_services()
|
services = get_unique_services()
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'code': 0,
|
{
|
||||||
'service_list': services,
|
"code": 0,
|
||||||
})
|
"service_list": services,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route('/api/cache/download', methods=['GET'])
|
@api_bp.route("/api/cache/download", methods=["GET"])
|
||||||
def download_database():
|
def download_database():
|
||||||
if config['database_type'].lower() != 'mariadb':
|
"""Download the database."""
|
||||||
original_database_path = f'{os.getcwd()}/databases/sql/key_cache.db'
|
if config["database_type"].lower() != "mariadb":
|
||||||
|
original_database_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
|
||||||
|
|
||||||
# Make a copy of the original database (without locking the original)
|
# Make a copy of the original database (without locking the original)
|
||||||
modified_database_path = f'{os.getcwd()}/databases/sql/key_cache_modified.db'
|
modified_database_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache_modified.db")
|
||||||
|
|
||||||
# Using shutil.copy2 to preserve metadata (timestamps, etc.)
|
# Using shutil.copy2 to preserve metadata (timestamps, etc.)
|
||||||
shutil.copy2(original_database_path, modified_database_path)
|
shutil.copy2(original_database_path, modified_database_path)
|
||||||
@ -117,151 +144,156 @@ def download_database():
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Update all rows to remove Headers and Cookies (set them to NULL or empty strings)
|
# Update all rows to remove Headers and Cookies (set them to NULL or empty strings)
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
UPDATE licenses
|
UPDATE licenses
|
||||||
SET Headers = NULL,
|
SET Headers = NULL,
|
||||||
Cookies = NULL
|
Cookies = NULL
|
||||||
''')
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# No need for explicit commit, it's done automatically with the 'with' block
|
# 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
|
# The connection will automatically be committed and closed when the block ends
|
||||||
|
|
||||||
# Send the modified database as an attachment
|
# Send the modified database as an attachment
|
||||||
return send_file(modified_database_path, as_attachment=True, download_name='key_cache.db')
|
return send_file(
|
||||||
if config['database_type'].lower() == 'mariadb':
|
modified_database_path, as_attachment=True, download_name="key_cache.db"
|
||||||
try:
|
)
|
||||||
# Connect to MariaDB
|
try:
|
||||||
conn = mysql.connector.connect(**get_db_config())
|
conn = mysql.connector.connect(**get_db_config())
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Update sensitive data (this updates the live DB, you may want to duplicate rows instead)
|
# Get column names
|
||||||
cursor.execute('''
|
cursor.execute("SHOW COLUMNS FROM licenses")
|
||||||
UPDATE licenses
|
columns = [row[0] for row in cursor.fetchall()]
|
||||||
SET Headers = NULL,
|
|
||||||
Cookies = NULL
|
|
||||||
''')
|
|
||||||
|
|
||||||
conn.commit()
|
# 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()
|
||||||
|
|
||||||
# Now export the table
|
# Dump to SQL-like format
|
||||||
cursor.execute('SELECT * FROM licenses')
|
output = StringIO()
|
||||||
rows = cursor.fetchall()
|
output.write("-- Dump of `licenses` table (Headers and Cookies are NULL)\n")
|
||||||
column_names = [desc[0] for desc in cursor.description]
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
# Dump to SQL-like format
|
# Write to a temp file for download
|
||||||
output = StringIO()
|
temp_dir = tempfile.gettempdir()
|
||||||
output.write(f"-- Dump of `licenses` table\n")
|
temp_path = os.path.join(temp_dir, "key_cache.sql")
|
||||||
for row in rows:
|
with open(temp_path, "w", encoding="utf-8") as f:
|
||||||
values = ', '.join(f"'{str(v).replace('\'', '\\\'')}'" if v is not None else 'NULL' for v in row)
|
f.write(output.getvalue())
|
||||||
output.write(f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n")
|
|
||||||
|
|
||||||
# Write to a temp file for download
|
@after_this_request
|
||||||
temp_dir = tempfile.gettempdir()
|
def remove_file(response):
|
||||||
temp_path = os.path.join(temp_dir, 'key_cache.sql')
|
try:
|
||||||
with open(temp_path, 'w', encoding='utf-8') as f:
|
os.remove(temp_path)
|
||||||
f.write(output.getvalue())
|
except Exception:
|
||||||
|
pass
|
||||||
|
return response
|
||||||
|
|
||||||
return send_file(temp_path, as_attachment=True, download_name='licenses_dump.sql')
|
return send_file(
|
||||||
except mysql.connector.Error as err:
|
temp_path, as_attachment=True, download_name="licenses_dump.sql"
|
||||||
return {"error": str(err)}, 500
|
)
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
return {"error": str(err)}, 500
|
||||||
|
|
||||||
_keycount_cache = {
|
|
||||||
'count': None,
|
|
||||||
'timestamp': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@api_bp.route('/api/cache/keycount', methods=['GET'])
|
_keycount_cache = {"count": None, "timestamp": 0}
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route("/api/cache/keycount", methods=["GET"])
|
||||||
def get_count():
|
def get_count():
|
||||||
|
"""Get the count of the keys in the database."""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - _keycount_cache['timestamp'] > 10 or _keycount_cache['count'] is None:
|
if now - _keycount_cache["timestamp"] > 10 or _keycount_cache["count"] is None:
|
||||||
_keycount_cache['count'] = key_count()
|
_keycount_cache["count"] = key_count()
|
||||||
_keycount_cache['timestamp'] = now
|
_keycount_cache["timestamp"] = now
|
||||||
return jsonify({
|
return jsonify({"count": _keycount_cache["count"]})
|
||||||
'count': _keycount_cache['count']
|
|
||||||
})
|
|
||||||
|
|
||||||
@api_bp.route('/api/decrypt', methods=['POST'])
|
|
||||||
|
@api_bp.route("/api/decrypt", methods=["POST"])
|
||||||
def decrypt_data():
|
def decrypt_data():
|
||||||
api_request_data = json.loads(request.data)
|
"""Decrypt the data."""
|
||||||
if 'pssh' in api_request_data:
|
api_request_data = request.get_json(force=True)
|
||||||
if api_request_data['pssh'] == '':
|
|
||||||
api_request_pssh = None
|
# Helper to get fields or None if missing/empty
|
||||||
else:
|
def get_field(key, default=""):
|
||||||
api_request_pssh = api_request_data['pssh']
|
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"
|
||||||
else:
|
else:
|
||||||
api_request_pssh = None
|
api_request_device = device
|
||||||
if 'licurl' in api_request_data:
|
|
||||||
if api_request_data['licurl'] == '':
|
username = ""
|
||||||
api_request_licurl = None
|
if api_request_device != "public":
|
||||||
else:
|
username = session.get("username")
|
||||||
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:
|
if not username:
|
||||||
return jsonify({'message': 'Not logged in, not allowed'}), 400
|
return jsonify({"message": "Not logged in, not allowed"}), 400
|
||||||
if user_allowed_to_use_device(device=api_request_device, username=username):
|
if not user_allowed_to_use_device(device=api_request_device, username=username):
|
||||||
api_request_device = api_request_device
|
return jsonify({"message": "Not authorized / Not found"}), 403
|
||||||
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']
|
|
||||||
})
|
|
||||||
|
|
||||||
@api_bp.route('/api/links', methods=['GET'])
|
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"])
|
||||||
def get_links():
|
def get_links():
|
||||||
return jsonify({
|
"""Get the links."""
|
||||||
'discord': icon_data['discord'],
|
return jsonify(
|
||||||
'telegram': icon_data['telegram'],
|
{
|
||||||
'gitea': icon_data['gitea'],
|
"discord": icon_data["discord"],
|
||||||
})
|
"telegram": icon_data["telegram"],
|
||||||
|
"gitea": icon_data["gitea"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@api_bp.route('/api/extension', methods=['POST'])
|
|
||||||
|
@api_bp.route("/api/extension", methods=["POST"])
|
||||||
def verify_extension():
|
def verify_extension():
|
||||||
return jsonify({
|
"""Verify the extension."""
|
||||||
'status': True,
|
return jsonify(
|
||||||
})
|
{
|
||||||
|
"status": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -1,37 +1,42 @@
|
|||||||
|
"""Module to handle the login process."""
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify, session
|
from flask import Blueprint, request, jsonify, session
|
||||||
from custom_functions.database.user_db import verify_user
|
from custom_functions.database.user_db import verify_user
|
||||||
|
|
||||||
login_bp = Blueprint(
|
login_bp = Blueprint(
|
||||||
'login_bp',
|
"login_bp",
|
||||||
__name__,
|
__name__,
|
||||||
)
|
)
|
||||||
|
|
||||||
@login_bp.route('/login', methods=['POST'])
|
|
||||||
|
@login_bp.route("/login", methods=["POST"])
|
||||||
def login():
|
def login():
|
||||||
if request.method == 'POST':
|
"""Handle the login process."""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
for required_field in ['username', 'password']:
|
for required_field in ["username", "password"]:
|
||||||
if required_field not in data:
|
if required_field not in data:
|
||||||
return jsonify({'error': f'Missing required field: {required_field}'}), 400
|
return (
|
||||||
|
jsonify({"error": f"Missing required field: {required_field}"}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
if verify_user(data['username'], data['password']):
|
if verify_user(data["username"], data["password"]):
|
||||||
session['username'] = data['username'].lower() # Stored securely in a signed cookie
|
session["username"] = data[
|
||||||
return jsonify({'message': 'Successfully logged in!'})
|
"username"
|
||||||
else:
|
].lower() # Stored securely in a signed cookie
|
||||||
return jsonify({'error': 'Invalid username or password!'}), 401
|
return jsonify({"message": "Successfully logged in!"})
|
||||||
|
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():
|
def login_status():
|
||||||
try:
|
"""Check if the user is logged in."""
|
||||||
username = session.get('username')
|
username = session.get("username")
|
||||||
if username:
|
return jsonify({"message": "True" if username else "False"})
|
||||||
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():
|
def logout():
|
||||||
session.pop('username', None)
|
"""Logout the user."""
|
||||||
return jsonify({'message': 'Successfully logged out!'})
|
session.pop("username", None)
|
||||||
|
return jsonify({"message": "Successfully logged out!"})
|
||||||
|
@ -1,33 +1,48 @@
|
|||||||
import sys
|
"""Module to handle the React routes."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from flask import Blueprint, send_from_directory, request, render_template
|
import sys
|
||||||
|
|
||||||
|
from flask import Blueprint, send_from_directory, render_template
|
||||||
from configs import index_tags
|
from configs import index_tags
|
||||||
|
|
||||||
if getattr(sys, 'frozen', False): # Running as a bundled app
|
if getattr(sys, "frozen", False): # Running as a bundled app
|
||||||
base_path = sys._MEIPASS
|
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
|
||||||
else: # Running in a normal Python environment
|
else: # Running in a normal Python environment
|
||||||
base_path = os.path.abspath(".")
|
base_path = os.path.abspath(".")
|
||||||
|
|
||||||
static_folder = os.path.join(base_path, 'cdrm-frontend', 'dist')
|
static_folder = os.path.join(base_path, "frontend-dist")
|
||||||
|
|
||||||
react_bp = Blueprint(
|
react_bp = Blueprint(
|
||||||
'react_bp',
|
"react_bp",
|
||||||
__name__,
|
__name__,
|
||||||
static_folder=static_folder,
|
static_folder=static_folder,
|
||||||
static_url_path='/',
|
static_url_path="/",
|
||||||
template_folder=static_folder
|
template_folder=static_folder,
|
||||||
)
|
)
|
||||||
|
|
||||||
@react_bp.route('/', methods=['GET'])
|
|
||||||
@react_bp.route('/<path:path>', methods=["GET"])
|
@react_bp.route("/", methods=["GET"])
|
||||||
@react_bp.route('/<path>', methods=["GET"])
|
@react_bp.route("/<path:path>", methods=["GET"])
|
||||||
def index(path=''):
|
@react_bp.route("/<path>", methods=["GET"])
|
||||||
if request.method == 'GET':
|
def index(path=""):
|
||||||
file_path = os.path.join(react_bp.static_folder, path)
|
"""Handle the index route."""
|
||||||
if path != "" and os.path.exists(file_path):
|
# Ensure static_folder is not None
|
||||||
return send_from_directory(react_bp.static_folder, path)
|
if react_bp.static_folder is None:
|
||||||
elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']:
|
raise ValueError("Static folder is not configured for the blueprint")
|
||||||
data = index_tags.tags.get(path.lower(), index_tags.tags['index'])
|
|
||||||
return render_template('index.html', data=data)
|
# Normalize the path to prevent directory traversal
|
||||||
else:
|
safe_path = os.path.normpath(path)
|
||||||
return send_from_directory(react_bp.static_folder, 'index.html')
|
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")
|
||||||
|
@ -1,42 +1,56 @@
|
|||||||
|
"""Module to handle the register process."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from custom_functions.database.user_db import add_user
|
from custom_functions.database.user_db import add_user
|
||||||
import uuid
|
|
||||||
|
|
||||||
register_bp = Blueprint('register_bp', __name__)
|
register_bp = Blueprint("register_bp", __name__)
|
||||||
|
|
||||||
USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$')
|
USERNAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||||
PASSWORD_REGEX = re.compile(r'^\S+$')
|
PASSWORD_REGEX = re.compile(r"^\S+$")
|
||||||
|
|
||||||
@register_bp.route('/register', methods=['POST'])
|
|
||||||
|
@register_bp.route("/register", methods=["POST"])
|
||||||
def register():
|
def register():
|
||||||
if request.method != 'POST':
|
"""Handle the register process."""
|
||||||
return jsonify({'error': 'Method not supported'}), 405
|
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
if data is None:
|
||||||
|
return jsonify({"error": "Invalid or missing JSON in request body."}), 400
|
||||||
|
|
||||||
# Check required fields
|
# Check required fields
|
||||||
for required_field in ['username', 'password']:
|
for required_field in ["username", "password"]:
|
||||||
if required_field not in data:
|
if required_field not in data:
|
||||||
return jsonify({'error': f'Missing required field: {required_field}'}), 400
|
return jsonify({"error": f"Missing required field: {required_field}"}), 400
|
||||||
|
|
||||||
username = data['username']
|
username = data["username"].lower()
|
||||||
password = data['password']
|
password = data["password"]
|
||||||
api_key = str(uuid.uuid4())
|
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
|
# Validate username and password
|
||||||
if not USERNAME_REGEX.fullmatch(username):
|
if not USERNAME_REGEX.fullmatch(username):
|
||||||
return jsonify({
|
return (
|
||||||
'error': 'Invalid username. Only letters, numbers, hyphens, and underscores are allowed.'
|
jsonify(
|
||||||
}), 400
|
{
|
||||||
|
"error": "Invalid username. Only letters, numbers, hyphens, and underscores are allowed."
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
if not PASSWORD_REGEX.fullmatch(password):
|
if not PASSWORD_REGEX.fullmatch(password):
|
||||||
return jsonify({
|
return jsonify({"error": "Invalid password. Spaces are not allowed."}), 400
|
||||||
'error': 'Invalid password. Spaces are not allowed.'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
# Attempt to add user
|
# Attempt to add user
|
||||||
if add_user(username, password, api_key):
|
if add_user(username, password, api_key):
|
||||||
return jsonify({'message': 'User successfully registered!'}), 201
|
return (
|
||||||
else:
|
jsonify({"message": "User successfully registered!", "api_key": api_key}),
|
||||||
return jsonify({'error': 'User already exists!'}), 409
|
201,
|
||||||
|
)
|
||||||
|
return jsonify({"error": "User already exists!"}), 409
|
||||||
|
@ -1,142 +1,236 @@
|
|||||||
import base64
|
"""Module to handle the remote device PlayReady."""
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request, current_app, Response
|
import base64
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import yaml
|
import yaml
|
||||||
|
from flask import Blueprint, jsonify, request, current_app, Response
|
||||||
|
|
||||||
from pyplayready.device import Device as PlayReadyDevice
|
from pyplayready.device import Device as PlayReadyDevice
|
||||||
from pyplayready.cdm import Cdm as PlayReadyCDM
|
from pyplayready.cdm import Cdm as PlayReadyCDM
|
||||||
from pyplayready import PSSH as PlayReadyPSSH
|
from pyplayready import PSSH as PlayReadyPSSH
|
||||||
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh)
|
from pyplayready.exceptions import (
|
||||||
|
InvalidSession,
|
||||||
|
InvalidLicense,
|
||||||
|
InvalidPssh,
|
||||||
|
)
|
||||||
from custom_functions.database.user_db import fetch_username_by_api_key
|
from custom_functions.database.user_db import fetch_username_by_api_key
|
||||||
from custom_functions.decrypt.api_decrypt import is_base64
|
from custom_functions.decrypt.api_decrypt import is_base64
|
||||||
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
|
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
|
remotecdm_pr_bp = Blueprint("remotecdm_pr", __name__)
|
||||||
|
with open(
|
||||||
remotecdm_pr_bp = Blueprint('remotecdm_pr', __name__)
|
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
) as file:
|
||||||
config = yaml.safe_load(file)
|
config = yaml.safe_load(file)
|
||||||
|
|
||||||
@remotecdm_pr_bp.route('/remotecdm/playready', methods=['GET', 'HEAD'])
|
|
||||||
|
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"])
|
||||||
def remote_cdm_playready():
|
def remote_cdm_playready():
|
||||||
if request.method == 'GET':
|
"""Handle the remote device PlayReady."""
|
||||||
return jsonify({
|
if request.method == "GET":
|
||||||
'message': 'OK'
|
return make_response(
|
||||||
})
|
"Success",
|
||||||
if request.method == 'HEAD':
|
"OK",
|
||||||
|
http_status=200,
|
||||||
|
)
|
||||||
|
if request.method == "HEAD":
|
||||||
response = Response(status=200)
|
response = Response(status=200)
|
||||||
response.headers['Server'] = 'playready serve'
|
response.headers["Server"] = "playready serve"
|
||||||
return response
|
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():
|
def remote_cdm_playready_deviceinfo():
|
||||||
|
"""Handle the remote device PlayReady device info."""
|
||||||
base_name = config["default_pr_cdm"]
|
base_name = config["default_pr_cdm"]
|
||||||
if not base_name.endswith(".prd"):
|
device = PlayReadyDevice.load(
|
||||||
full_file_name = (base_name + ".prd")
|
os.path.join(os.getcwd(), "configs", "CDMs", "PR", base_name + ".prd")
|
||||||
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}')
|
)
|
||||||
cdm = PlayReadyCDM.from_device(device)
|
cdm = PlayReadyCDM.from_device(device)
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'security_level': cdm.security_level,
|
{
|
||||||
'host': f'{config["fqdn"]}/remotecdm/playready',
|
"security_level": cdm.security_level,
|
||||||
'secret': f'{config["remote_cdm_secret"]}',
|
"host": f'{config["fqdn"]}/remotecdm/playready',
|
||||||
'device_name': Path(base_name).stem
|
"secret": f'{config["remote_cdm_secret"]}',
|
||||||
})
|
"device_name": Path(base_name).stem,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo/<device>', methods=['GET'])
|
|
||||||
|
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):
|
def remote_cdm_playready_deviceinfo_specific(device):
|
||||||
if request.method == 'GET':
|
"""Handle the remote device PlayReady device info specific."""
|
||||||
base_name = Path(device).with_suffix('.prd').name
|
base_name = Path(device).with_suffix(".prd").name
|
||||||
api_key = request.headers['X-Secret-Key']
|
api_key = request.headers["X-Secret-Key"]
|
||||||
username = fetch_username_by_api_key(api_key)
|
username = fetch_username_by_api_key(api_key)
|
||||||
device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
|
if not username:
|
||||||
cdm = PlayReadyCDM.from_device(device)
|
return jsonify({"message": "Invalid or missing API key."}), 403
|
||||||
return jsonify({
|
safe_username = sanitize_username(username)
|
||||||
'security_level': cdm.security_level,
|
device = PlayReadyDevice.load(
|
||||||
'host': f'{config["fqdn"]}/remotecdm/widevine',
|
os.path.join(
|
||||||
'secret': f'{api_key}',
|
os.getcwd(),
|
||||||
'device_name': Path(base_name).stem
|
"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):
|
def remote_cdm_playready_open(device):
|
||||||
if str(device).lower() == config['default_pr_cdm'].lower():
|
"""Handle the remote device PlayReady open."""
|
||||||
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd')
|
unauthorized_msg = {
|
||||||
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
|
"message": f"Device '{device}' is not found or you are not authorized to use it."
|
||||||
session_id = cdm.open()
|
}
|
||||||
return jsonify({
|
|
||||||
'message': 'Success',
|
|
||||||
'data': {
|
|
||||||
'session_id': session_id.hex(),
|
|
||||||
'device': {
|
|
||||||
'security_level': cdm.security_level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if request.headers['X-Secret-Key'] and str(device).lower() != config['default_pr_cdm'].lower():
|
|
||||||
api_key = request.headers['X-Secret-Key']
|
|
||||||
user = fetch_username_by_api_key(api_key=api_key)
|
|
||||||
if user:
|
|
||||||
if user_allowed_to_use_device(device=device, username=user):
|
|
||||||
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/PR/{device}.prd')
|
|
||||||
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
|
|
||||||
session_id = cdm.open()
|
|
||||||
return jsonify({
|
|
||||||
'message': 'Success',
|
|
||||||
'data': {
|
|
||||||
'session_id': session_id.hex(),
|
|
||||||
'device': {
|
|
||||||
'security_level': cdm.security_level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'message': f"Device '{device}' is not found or you are not authorized to use it.",
|
|
||||||
}), 403
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'message': f"Device '{device}' is not found or you are not authorized to use it.",
|
|
||||||
}), 403
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'message': f"Device '{device}' is not found or you are not authorized to use it.",
|
|
||||||
}), 403
|
|
||||||
|
|
||||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
|
# 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)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
)
|
||||||
def remote_cdm_playready_close(device, session_id):
|
def remote_cdm_playready_close(device, session_id):
|
||||||
|
"""Handle the remote device PlayReady close."""
|
||||||
try:
|
try:
|
||||||
session_id = bytes.fromhex(session_id)
|
session_id = bytes.fromhex(session_id)
|
||||||
cdm = current_app.config["CDM"]
|
cdm = get_cdm_or_error(device)
|
||||||
if not cdm:
|
if isinstance(cdm, tuple): # error response
|
||||||
return jsonify({
|
return cdm
|
||||||
'message': f'No CDM for "{device}" has been opened yet. No session to close'
|
|
||||||
}), 400
|
|
||||||
try:
|
try:
|
||||||
cdm.close(session_id)
|
cdm.close(session_id)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
|
"Error",
|
||||||
}), 400
|
f'Invalid session ID "{session_id.hex()}", it may have expired',
|
||||||
return jsonify({
|
http_status=400,
|
||||||
'message': f'Successfully closed Session "{session_id.hex()}".',
|
)
|
||||||
}), 200
|
return make_response(
|
||||||
except Exception as e:
|
"Success",
|
||||||
return jsonify({
|
f'Successfully closed Session "{session_id.hex()}".',
|
||||||
'message': f'Failed to close Session "{session_id.hex()}".'
|
http_status=200,
|
||||||
}), 400
|
)
|
||||||
|
except Exception as error:
|
||||||
|
return make_response(
|
||||||
|
"Error",
|
||||||
|
f'Failed to close Session "{session_id.hex()}", {error}.',
|
||||||
|
http_status=400,
|
||||||
|
)
|
||||||
|
|
||||||
@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):
|
def remote_cdm_playready_get_license_challenge(device):
|
||||||
|
"""Handle the remote device PlayReady get license challenge."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
for required_field in ("session_id", "init_data"):
|
missing_field = check_required_fields(body, ("session_id", "init_data"))
|
||||||
if not body.get(required_field):
|
if missing_field:
|
||||||
return jsonify({
|
return missing_field
|
||||||
'message': f'Missing required field "{required_field}" in JSON body'
|
cdm = get_cdm_or_error(device)
|
||||||
}), 400
|
if isinstance(cdm, tuple): # error response
|
||||||
cdm = current_app.config["CDM"]
|
return cdm
|
||||||
session_id = bytes.fromhex(body["session_id"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
init_data = body["init_data"]
|
init_data = body["init_data"]
|
||||||
if not init_data.startswith("<WRMHEADER"):
|
if not init_data.startswith("<WRMHEADER"):
|
||||||
@ -144,43 +238,46 @@ def remote_cdm_playready_get_license_challenge(device):
|
|||||||
pssh = PlayReadyPSSH(init_data)
|
pssh = PlayReadyPSSH(init_data)
|
||||||
if pssh.wrm_headers:
|
if pssh.wrm_headers:
|
||||||
init_data = pssh.wrm_headers[0]
|
init_data = pssh.wrm_headers[0]
|
||||||
except InvalidPssh as e:
|
except InvalidPssh as error:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'message': f'Unable to parse base64 PSSH, {e}'
|
"Error",
|
||||||
})
|
f"Unable to parse base64 PSSH, {error}",
|
||||||
|
http_status=400,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
license_request = cdm.get_license_challenge(
|
license_request = cdm.get_license_challenge(
|
||||||
session_id=session_id,
|
session_id=session_id, wrm_header=init_data
|
||||||
wrm_header=init_data
|
|
||||||
)
|
)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
|
"Error",
|
||||||
})
|
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
|
||||||
except Exception as e:
|
http_status=400,
|
||||||
return jsonify({
|
)
|
||||||
'message': f'Error, {e}'
|
except ValueError as error:
|
||||||
})
|
return make_response(
|
||||||
return jsonify({
|
"Error",
|
||||||
'message': 'success',
|
f"Invalid License, {error}",
|
||||||
'data': {
|
http_status=400,
|
||||||
'challenge': license_request
|
)
|
||||||
}
|
return make_response(
|
||||||
})
|
"Success",
|
||||||
|
"Successfully got the License Challenge",
|
||||||
|
{"challenge_b64": base64.b64encode(license_request).decode()},
|
||||||
|
http_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
@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):
|
def remote_cdm_playready_parse_license(device):
|
||||||
|
"""Handle the remote device PlayReady parse license."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
for required_field in ("license_message", "session_id"):
|
missing_field = check_required_fields(body, ("license_message", "session_id"))
|
||||||
if not body.get(required_field):
|
if missing_field:
|
||||||
return jsonify({
|
return missing_field
|
||||||
'message': f'Missing required field "{required_field}" in JSON body'
|
cdm = get_cdm_or_error(device)
|
||||||
})
|
if isinstance(cdm, tuple): # error response
|
||||||
cdm = current_app.config["CDM"]
|
return 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"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
license_message = body["license_message"]
|
license_message = body["license_message"]
|
||||||
if is_base64(license_message):
|
if is_base64(license_message):
|
||||||
@ -188,45 +285,56 @@ def remote_cdm_playready_parse_license(device):
|
|||||||
try:
|
try:
|
||||||
cdm.parse_license(session_id, license_message)
|
cdm.parse_license(session_id, license_message)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
|
"Error",
|
||||||
})
|
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
|
||||||
|
http_status=400,
|
||||||
|
)
|
||||||
except InvalidLicense as e:
|
except InvalidLicense as e:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'message': f"Invalid License, {e}"
|
"Error",
|
||||||
})
|
f"Invalid License, {e}",
|
||||||
|
http_status=400,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'message': f"Error, {e}"
|
"Error",
|
||||||
})
|
f"Error, {e}",
|
||||||
return jsonify({
|
http_status=400,
|
||||||
'message': 'Successfully parsed and loaded the Keys from the License message'
|
)
|
||||||
})
|
return make_response(
|
||||||
|
"Success",
|
||||||
|
"Successfully parsed and loaded the Keys from the License message",
|
||||||
|
http_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
@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):
|
def remote_cdm_playready_get_keys(device):
|
||||||
|
"""Handle the remote device PlayReady get keys."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
for required_field in ("session_id",):
|
missing_field = check_required_fields(body, ("session_id",))
|
||||||
if not body.get(required_field):
|
if missing_field:
|
||||||
return jsonify({
|
return missing_field
|
||||||
'message': f'Missing required field "{required_field}" in JSON body'
|
|
||||||
})
|
|
||||||
session_id = bytes.fromhex(body["session_id"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
cdm = current_app.config["CDM"]
|
key_type = body.get("key_type", None)
|
||||||
if not cdm:
|
cdm = get_cdm_or_error(device)
|
||||||
return jsonify({
|
if isinstance(cdm, tuple): # error response
|
||||||
'message': f"Missing required field '{required_field}' in JSON body."
|
return cdm
|
||||||
})
|
|
||||||
try:
|
try:
|
||||||
keys = cdm.get_keys(session_id)
|
keys = cdm.get_keys(session_id, key_type)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
|
"Error",
|
||||||
})
|
f"Invalid Session ID '{session_id.hex()}', it may have expired.",
|
||||||
except Exception as e:
|
http_status=400,
|
||||||
return jsonify({
|
)
|
||||||
'message': f"Error, {e}"
|
except ValueError as error:
|
||||||
})
|
return make_response(
|
||||||
|
"Error",
|
||||||
|
f"The Key Type value '{key_type}' is invalid, {error}",
|
||||||
|
http_status=400,
|
||||||
|
)
|
||||||
keys_json = [
|
keys_json = [
|
||||||
{
|
{
|
||||||
"key_id": key.key_id.hex,
|
"key_id": key.key_id.hex,
|
||||||
@ -236,10 +344,11 @@ def remote_cdm_playready_get_keys(device):
|
|||||||
"key_length": key.key_length,
|
"key_length": key.key_length,
|
||||||
}
|
}
|
||||||
for key in keys
|
for key in keys
|
||||||
|
if not key_type or key.type == key_type
|
||||||
]
|
]
|
||||||
return jsonify({
|
return make_response(
|
||||||
'message': 'success',
|
"Success",
|
||||||
'data': {
|
"Successfully got the Keys",
|
||||||
'keys': keys_json
|
{"keys": keys_json},
|
||||||
}
|
http_status=200,
|
||||||
})
|
)
|
||||||
|
@ -1,387 +1,474 @@
|
|||||||
|
"""Module to handle the remote device Widevine."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from flask import Blueprint, jsonify, request, current_app, Response
|
|
||||||
import base64
|
import base64
|
||||||
from typing import Any, Optional, Union
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
from flask import Blueprint, jsonify, request, current_app, Response
|
||||||
|
|
||||||
from google.protobuf.message import DecodeError
|
from google.protobuf.message import DecodeError
|
||||||
from pywidevine.pssh import PSSH as widevinePSSH
|
from pywidevine.pssh import PSSH as widevinePSSH
|
||||||
from pywidevine import __version__
|
from pywidevine import __version__
|
||||||
from pywidevine.cdm import Cdm as widevineCDM
|
from pywidevine.cdm import Cdm as widevineCDM
|
||||||
from pywidevine.device import Device as widevineDevice
|
from pywidevine.device import Device as widevineDevice
|
||||||
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
|
from pywidevine.exceptions import (
|
||||||
InvalidSession, SignatureMismatch, TooManySessions)
|
InvalidContext,
|
||||||
|
InvalidInitData,
|
||||||
|
InvalidLicenseMessage,
|
||||||
|
InvalidLicenseType,
|
||||||
|
InvalidSession,
|
||||||
|
SignatureMismatch,
|
||||||
|
)
|
||||||
|
|
||||||
import yaml
|
from custom_functions.database.user_db import fetch_username_by_api_key
|
||||||
from custom_functions.database.user_db import fetch_api_key, 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
|
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
|
|
||||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
|
remotecdm_wv_bp = Blueprint("remotecdm_wv", __name__)
|
||||||
|
with open(
|
||||||
|
os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
|
||||||
|
) as file:
|
||||||
config = yaml.safe_load(file)
|
config = yaml.safe_load(file)
|
||||||
|
|
||||||
@remotecdm_wv_bp.route('/remotecdm/widevine', methods=['GET', 'HEAD'])
|
|
||||||
|
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"])
|
||||||
def remote_cdm_widevine():
|
def remote_cdm_widevine():
|
||||||
if request.method == 'GET':
|
"""Handle the remote device Widevine."""
|
||||||
return jsonify({
|
if request.method == "GET":
|
||||||
'status': 200,
|
return make_response(
|
||||||
'message': f"{config['fqdn'].upper()} Remote Widevine CDM."
|
"Success",
|
||||||
})
|
f"{config['fqdn'].upper()} Remote Widevine CDM.",
|
||||||
if request.method == 'HEAD':
|
http_status=200,
|
||||||
|
)
|
||||||
|
if request.method == "HEAD":
|
||||||
response = Response(status=200)
|
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 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():
|
def remote_cdm_widevine_deviceinfo():
|
||||||
if request.method == 'GET':
|
"""Handle the remote device Widevine device info."""
|
||||||
base_name = config["default_wv_cdm"]
|
base_name = config["default_wv_cdm"]
|
||||||
if not base_name.endswith(".wvd"):
|
if not base_name.endswith(".wvd"):
|
||||||
base_name = (base_name + ".wvd")
|
base_name = base_name + ".wvd"
|
||||||
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
|
device = widevineDevice.load(
|
||||||
cdm = widevineCDM.from_device(device)
|
os.path.join(os.getcwd(), "configs", "CDMs", "WV", base_name)
|
||||||
return jsonify({
|
)
|
||||||
'device_type': cdm.device_type.name,
|
cdm = widevineCDM.from_device(device)
|
||||||
'system_id': cdm.system_id,
|
return make_response(
|
||||||
'security_level': cdm.security_level,
|
"Success",
|
||||||
'host': f'{config["fqdn"]}/remotecdm/widevine',
|
"Successfully got the Widevine CDM device info",
|
||||||
'secret': f'{config["remote_cdm_secret"]}',
|
{
|
||||||
'device_name': Path(base_name).stem
|
"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,
|
||||||
|
)
|
||||||
|
|
||||||
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo/<device>', methods=['GET'])
|
|
||||||
|
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):
|
def remote_cdm_widevine_deviceinfo_specific(device):
|
||||||
if request.method == 'GET':
|
"""Handle the remote device Widevine device info specific."""
|
||||||
base_name = Path(device).with_suffix('.wvd').name
|
base_name = Path(device).with_suffix(".wvd").name
|
||||||
api_key = request.headers['X-Secret-Key']
|
api_key = request.headers["X-Secret-Key"]
|
||||||
username = fetch_username_by_api_key(api_key)
|
username = fetch_username_by_api_key(api_key)
|
||||||
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
|
safe_username = sanitize_username(username)
|
||||||
cdm = widevineCDM.from_device(device)
|
device = widevineDevice.load(
|
||||||
return jsonify({
|
os.path.join(
|
||||||
'device_type': cdm.device_type.name,
|
os.getcwd(),
|
||||||
'system_id': cdm.system_id,
|
"configs",
|
||||||
'security_level': cdm.security_level,
|
"CDMs",
|
||||||
'host': f'{config["fqdn"]}/remotecdm/widevine',
|
"users_uploaded",
|
||||||
'secret': f'{api_key}',
|
safe_username,
|
||||||
'device_name': Path(base_name).stem
|
"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,
|
||||||
|
)
|
||||||
|
|
||||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET'])
|
|
||||||
|
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"])
|
||||||
def remote_cdm_widevine_open(device):
|
def remote_cdm_widevine_open(device):
|
||||||
if str(device).lower() == config['default_wv_cdm'].lower():
|
"""Handle the remote device Widevine open."""
|
||||||
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd')
|
api_key = request.headers.get("X-Secret-Key")
|
||||||
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
|
wv_device = load_widevine_device(device, api_key)
|
||||||
session_id = cdm.open()
|
if not wv_device:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 200,
|
"Error",
|
||||||
'message': 'Success',
|
f"Device '{device}' is not found or you are not authorized to use it.",
|
||||||
'data': {
|
http_status=403,
|
||||||
'session_id': session_id.hex(),
|
)
|
||||||
'device': {
|
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
|
||||||
'system_id': cdm.system_id,
|
session_id = cdm.open()
|
||||||
'security_level': cdm.security_level,
|
return make_response(
|
||||||
}
|
"Success",
|
||||||
}
|
"Successfully opened the Widevine CDM session",
|
||||||
}), 200
|
{
|
||||||
if request.headers['X-Secret-Key'] and str(device).lower() != config['default_wv_cdm'].lower():
|
"session_id": session_id.hex(),
|
||||||
api_key = request.headers['X-Secret-Key']
|
"device": {
|
||||||
user = fetch_username_by_api_key(api_key=api_key)
|
"system_id": cdm.system_id,
|
||||||
if user:
|
"security_level": cdm.security_level,
|
||||||
if user_allowed_to_use_device(device=device, username=user):
|
},
|
||||||
wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/WV/{device}.wvd')
|
},
|
||||||
cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
|
http_status=200,
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}), 200
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'message': f"Device '{device}' is not found or you are not authorized to use it.",
|
|
||||||
'status': 403
|
|
||||||
}), 403
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'message': f"Device '{device}' is not found or you are not authorized to use it.",
|
|
||||||
'status': 403
|
|
||||||
}), 403
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'message': f"Device '{device}' is not found or you are not authorized to use it.",
|
|
||||||
'status': 403
|
|
||||||
}), 403
|
|
||||||
|
|
||||||
|
|
||||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
|
@remotecdm_wv_bp.route(
|
||||||
|
"/remotecdm/widevine/<device>/close/<session_id>", methods=["GET"]
|
||||||
|
)
|
||||||
def remote_cdm_widevine_close(device, session_id):
|
def remote_cdm_widevine_close(device, session_id):
|
||||||
session_id = bytes.fromhex(session_id)
|
"""Handle the remote device Widevine close."""
|
||||||
cdm = current_app.config["CDM"]
|
session_id = bytes.fromhex(session_id)
|
||||||
if not cdm:
|
cdm = get_cdm_or_error(device)
|
||||||
return jsonify({
|
if isinstance(cdm, tuple): # error response
|
||||||
'status': 400,
|
return cdm
|
||||||
'message': f'No CDM for "{device}" has been opened yet. No session to close'
|
try:
|
||||||
}), 400
|
cdm.close(session_id)
|
||||||
try:
|
except InvalidSession:
|
||||||
cdm.close(session_id)
|
return make_response(
|
||||||
except InvalidSession:
|
"Error",
|
||||||
return jsonify({
|
f'Invalid session ID "{session_id.hex()}", it may have expired',
|
||||||
'status': 400,
|
http_status=400,
|
||||||
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
|
)
|
||||||
}), 400
|
|
||||||
return jsonify({
|
|
||||||
'status': 200,
|
|
||||||
'message': f'Successfully closed Session "{session_id.hex()}".',
|
|
||||||
}), 200
|
|
||||||
|
|
||||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
|
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):
|
def remote_cdm_widevine_set_service_certificate(device):
|
||||||
|
"""Handle the remote device Widevine set service certificate."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
for required_field in ("session_id", "certificate"):
|
missing_field = check_required_fields(body, ("session_id", "certificate"))
|
||||||
if required_field == "certificate":
|
if missing_field:
|
||||||
has_field = required_field in body # it needs the key, but can be empty/null
|
return missing_field
|
||||||
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'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
session_id = bytes.fromhex(body["session_id"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
|
|
||||||
cdm = current_app.config["CDM"]
|
cdm = get_cdm_or_error(device)
|
||||||
if not cdm:
|
if isinstance(cdm, tuple): # error response
|
||||||
return jsonify({
|
return cdm
|
||||||
'status': 400,
|
|
||||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
certificate = body["certificate"]
|
certificate = body["certificate"]
|
||||||
try:
|
try:
|
||||||
provider_id = cdm.set_service_certificate(session_id, certificate)
|
provider_id = cdm.set_service_certificate(session_id, certificate)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
|
f'Invalid session id: "{session_id.hex()}", it may have expired',
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
except DecodeError as error:
|
except DecodeError as error:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid Service Certificate, {error}'
|
f"Invalid Service Certificate, {error}",
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
except SignatureMismatch:
|
except SignatureMismatch:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': 'Signature Validation failed on the Service Certificate, rejecting'
|
"Signature Validation failed on the Service Certificate, rejecting",
|
||||||
}), 400
|
http_status=400,
|
||||||
return jsonify({
|
)
|
||||||
'status': 200,
|
return make_response(
|
||||||
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
|
"Success",
|
||||||
'data': {
|
f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
|
||||||
'provider_id': provider_id,
|
{"provider_id": provider_id},
|
||||||
}
|
http_status=200,
|
||||||
}), 200
|
)
|
||||||
|
|
||||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
|
|
||||||
|
@remotecdm_wv_bp.route(
|
||||||
|
"/remotecdm/widevine/<device>/get_service_certificate", methods=["POST"]
|
||||||
|
)
|
||||||
def remote_cdm_widevine_get_service_certificate(device):
|
def remote_cdm_widevine_get_service_certificate(device):
|
||||||
|
"""Handle the remote device Widevine get service certificate."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
for required_field in ("session_id",):
|
missing_field = check_required_fields(body, ("session_id",))
|
||||||
if not body.get(required_field):
|
if missing_field:
|
||||||
return jsonify({
|
return missing_field
|
||||||
'status': 400,
|
|
||||||
'message': f'Missing required field "{required_field}" in JSON body'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
session_id = bytes.fromhex(body["session_id"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
|
|
||||||
cdm = current_app.config["CDM"]
|
cdm = get_cdm_or_error(device)
|
||||||
|
if isinstance(cdm, tuple): # error response
|
||||||
if not cdm:
|
return cdm
|
||||||
return jsonify({
|
|
||||||
'status': 400,
|
|
||||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
service_certificate = cdm.get_service_certificate(session_id)
|
service_certificate = cdm.get_service_certificate(session_id)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
|
f'Invalid Session ID "{session_id.hex()}", it may have expired',
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
if service_certificate:
|
if service_certificate:
|
||||||
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
|
service_certificate_b64 = base64.b64encode(
|
||||||
|
service_certificate.SerializeToString()
|
||||||
|
).decode()
|
||||||
else:
|
else:
|
||||||
service_certificate_b64 = None
|
service_certificate_b64 = None
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 200,
|
"Success",
|
||||||
'message': 'Successfully got the Service Certificate',
|
"Successfully got the Service Certificate",
|
||||||
'data': {
|
{"service_certificate": service_certificate_b64},
|
||||||
'service_certificate': service_certificate_b64,
|
http_status=200,
|
||||||
}
|
)
|
||||||
}), 200
|
|
||||||
|
|
||||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
|
|
||||||
|
@remotecdm_wv_bp.route(
|
||||||
|
"/remotecdm/widevine/<device>/get_license_challenge/<license_type>",
|
||||||
|
methods=["POST"],
|
||||||
|
)
|
||||||
def remote_cdm_widevine_get_license_challenge(device, license_type):
|
def remote_cdm_widevine_get_license_challenge(device, license_type):
|
||||||
|
"""Handle the remote device Widevine get license challenge."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
for required_field in ("session_id", "init_data"):
|
missing_field = check_required_fields(body, ("session_id", "init_data"))
|
||||||
if not body.get(required_field):
|
if missing_field:
|
||||||
return jsonify({
|
return missing_field
|
||||||
'status': 400,
|
|
||||||
'message': f'Missing required field "{required_field}" in JSON body'
|
|
||||||
}), 400
|
|
||||||
session_id = bytes.fromhex(body["session_id"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
privacy_mode = body.get("privacy_mode", True)
|
privacy_mode = body.get("privacy_mode", True)
|
||||||
cdm = current_app.config["CDM"]
|
cdm = get_cdm_or_error(device)
|
||||||
if not cdm:
|
if isinstance(cdm, tuple): # error response
|
||||||
return jsonify({
|
return cdm
|
||||||
'status': 400,
|
|
||||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
|
||||||
}), 400
|
|
||||||
if current_app.config.get("force_privacy_mode"):
|
if current_app.config.get("force_privacy_mode"):
|
||||||
privacy_mode = True
|
privacy_mode = True
|
||||||
if not cdm.get_service_certificate(session_id):
|
if not cdm.get_service_certificate(session_id):
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 403,
|
"Error",
|
||||||
'message': 'No Service Certificate set but Privacy Mode is Enforced.'
|
"No Service Certificate set but Privacy Mode is Enforced.",
|
||||||
}), 403
|
http_status=403,
|
||||||
|
)
|
||||||
|
|
||||||
current_app.config['pssh'] = body['init_data']
|
current_app.config["pssh"] = body["init_data"]
|
||||||
init_data = widevinePSSH(body['init_data'])
|
init_data = widevinePSSH(body["init_data"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
license_request = cdm.get_license_challenge(
|
license_request = cdm.get_license_challenge(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
pssh=init_data,
|
pssh=init_data,
|
||||||
license_type=license_type,
|
license_type=license_type,
|
||||||
privacy_mode=privacy_mode
|
privacy_mode=privacy_mode,
|
||||||
)
|
)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
|
f'Invalid Session ID "{session_id.hex()}", it may have expired',
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
except InvalidInitData as error:
|
except InvalidInitData as error:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid Init Data, {error}'
|
f"Invalid Init Data, {error}",
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
except InvalidLicenseType:
|
except InvalidLicenseType:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid License Type {license_type}'
|
f"Invalid License Type {license_type}",
|
||||||
}), 400
|
http_status=400,
|
||||||
return jsonify({
|
)
|
||||||
'status': 200,
|
return make_response(
|
||||||
'message': 'Success',
|
"Success",
|
||||||
'data': {
|
"Successfully got the License Challenge",
|
||||||
'challenge_b64': base64.b64encode(license_request).decode()
|
{"challenge_b64": base64.b64encode(license_request).decode()},
|
||||||
}
|
http_status=200,
|
||||||
}), 200
|
)
|
||||||
|
|
||||||
|
|
||||||
@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):
|
def remote_cdm_widevine_parse_license(device):
|
||||||
|
"""Handle the remote device Widevine parse license."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
for required_field in ("session_id", "license_message"):
|
missing_field = check_required_fields(body, ("session_id", "license_message"))
|
||||||
if not body.get(required_field):
|
if missing_field:
|
||||||
return jsonify({
|
return missing_field
|
||||||
'status': 400,
|
|
||||||
'message': f'Missing required field "{required_field}" in JSON body'
|
|
||||||
}), 400
|
|
||||||
session_id = bytes.fromhex(body["session_id"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
cdm = current_app.config["CDM"]
|
cdm = get_cdm_or_error(device)
|
||||||
if not cdm:
|
if isinstance(cdm, tuple): # error response
|
||||||
return jsonify({
|
return cdm
|
||||||
'status': 400,
|
|
||||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
|
||||||
}), 400
|
|
||||||
try:
|
try:
|
||||||
cdm.parse_license(session_id, body['license_message'])
|
cdm.parse_license(session_id, body["license_message"])
|
||||||
except InvalidLicenseMessage as error:
|
except InvalidLicenseMessage as error:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid License Message, {error}'
|
f"Invalid License Message, {error}",
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
except InvalidContext as error:
|
except InvalidContext as error:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid Context, {error}'
|
f"Invalid Context, {error}",
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
|
f'Invalid Session ID "{session_id.hex()}", it may have expired',
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
except SignatureMismatch:
|
except SignatureMismatch:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Signature Validation failed on the License Message, rejecting.'
|
"Signature Validation failed on the License Message, rejecting.",
|
||||||
}), 400
|
http_status=400,
|
||||||
return jsonify({
|
)
|
||||||
'status': 200,
|
return make_response(
|
||||||
'message': 'Successfully parsed and loaded the Keys from the License message.',
|
"Success",
|
||||||
}), 200
|
"Successfully parsed and loaded the Keys from the License message.",
|
||||||
|
http_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
@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):
|
def remote_cdm_widevine_get_keys(device, key_type):
|
||||||
|
"""Handle the remote device Widevine get keys."""
|
||||||
body = request.get_json()
|
body = request.get_json()
|
||||||
for required_field in ("session_id",):
|
missing_field = check_required_fields(body, ("session_id",))
|
||||||
if not body.get(required_field):
|
if missing_field:
|
||||||
return jsonify({
|
return missing_field
|
||||||
'status': 400,
|
|
||||||
'message': f'Missing required field "{required_field}" in JSON body'
|
|
||||||
}), 400
|
|
||||||
session_id = bytes.fromhex(body["session_id"])
|
session_id = bytes.fromhex(body["session_id"])
|
||||||
key_type: Optional[str] = key_type
|
if key_type == "ALL":
|
||||||
if key_type == 'ALL':
|
|
||||||
key_type = None
|
key_type = None
|
||||||
cdm = current_app.config["CDM"]
|
cdm = get_cdm_or_error(device)
|
||||||
if not cdm:
|
if isinstance(cdm, tuple): # error response
|
||||||
return jsonify({
|
return cdm
|
||||||
'status': 400,
|
|
||||||
'message': f'No CDM session for "{device}" has been opened yet. No session to use'
|
|
||||||
}), 400
|
|
||||||
try:
|
try:
|
||||||
keys = cdm.get_keys(session_id, key_type)
|
keys = cdm.get_keys(session_id, key_type)
|
||||||
except InvalidSession:
|
except InvalidSession:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
|
f'Invalid Session ID "{session_id.hex()}", it may have expired',
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 400,
|
"Error",
|
||||||
'message': f'The Key Type value "{key_type}" is invalid, {error}'
|
f'The Key Type value "{key_type}" is invalid, {error}',
|
||||||
}), 400
|
http_status=400,
|
||||||
|
)
|
||||||
keys_json = [
|
keys_json = [
|
||||||
{
|
{
|
||||||
"key_id": key.kid.hex,
|
"key_id": key.kid.hex,
|
||||||
"key": key.key.hex(),
|
"key": key.key.hex(),
|
||||||
"type": key.type,
|
"type": key.type,
|
||||||
"permissions": key.permissions
|
"permissions": key.permissions,
|
||||||
}
|
}
|
||||||
for key in keys
|
for key in keys
|
||||||
if not key_type or key.type == key_type
|
if not key_type or key.type == key_type
|
||||||
]
|
]
|
||||||
for entry in keys_json:
|
for entry in keys_json:
|
||||||
if config['database_type'].lower() != 'mariadb':
|
if entry["type"] != "SIGNING":
|
||||||
from custom_functions.database.cache_to_db_sqlite import cache_to_db
|
cache_to_db(
|
||||||
elif config['database_type'].lower() == 'mariadb':
|
pssh=str(current_app.config["pssh"]),
|
||||||
from custom_functions.database.cache_to_db_mariadb import cache_to_db
|
kid=entry["key_id"],
|
||||||
if entry['type'] != 'SIGNING':
|
key=entry["key"],
|
||||||
cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
|
)
|
||||||
|
|
||||||
return jsonify({
|
return make_response(
|
||||||
'status': 200,
|
"Success",
|
||||||
'message': 'Success',
|
"Successfully got the Keys",
|
||||||
'data': {
|
{"keys": keys_json},
|
||||||
'keys': keys_json
|
http_status=200,
|
||||||
}
|
)
|
||||||
}), 200
|
|
||||||
|
@ -1,42 +1,57 @@
|
|||||||
from flask import Blueprint, request, jsonify, session
|
"""Module to handle the upload process."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
from flask import Blueprint, request, jsonify, session
|
||||||
|
|
||||||
upload_bp = Blueprint('upload_bp', __name__)
|
upload_bp = Blueprint("upload_bp", __name__)
|
||||||
|
|
||||||
|
|
||||||
@upload_bp.route('/upload/<cdmtype>', methods=['POST'])
|
def sanitize_username(username):
|
||||||
|
"""Sanitize the username."""
|
||||||
|
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
|
||||||
|
|
||||||
|
|
||||||
|
@upload_bp.route("/upload/<cdmtype>", methods=["POST"])
|
||||||
def upload(cdmtype):
|
def upload(cdmtype):
|
||||||
|
"""Handle the upload process."""
|
||||||
try:
|
try:
|
||||||
username = session.get('username')
|
username = session.get("username")
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({'message': 'False', 'error': 'No username in session'}), 400
|
return jsonify({"message": "False", "error": "No username in session"}), 400
|
||||||
|
|
||||||
|
safe_username = sanitize_username(username)
|
||||||
|
|
||||||
# Validate CDM type
|
# Validate CDM type
|
||||||
if cdmtype not in ['PR', 'WV']:
|
if cdmtype not in ["PR", "WV"]:
|
||||||
return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400
|
return jsonify({"message": "False", "error": "Invalid CDM type"}), 400
|
||||||
|
|
||||||
# Set up user directory paths
|
# Set up user directory paths
|
||||||
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
|
base_path = os.path.join(
|
||||||
pr_path = os.path.join(base_path, 'PR')
|
os.getcwd(), "configs", "CDMs", "users_uploaded", safe_username
|
||||||
wv_path = os.path.join(base_path, 'WV')
|
)
|
||||||
|
pr_path = os.path.join(base_path, "PR")
|
||||||
|
wv_path = os.path.join(base_path, "WV")
|
||||||
|
|
||||||
# Create necessary directories if they don't exist
|
# Create necessary directories if they don't exist
|
||||||
os.makedirs(pr_path, exist_ok=True)
|
os.makedirs(pr_path, exist_ok=True)
|
||||||
os.makedirs(wv_path, exist_ok=True)
|
os.makedirs(wv_path, exist_ok=True)
|
||||||
|
|
||||||
# Get uploaded file
|
# Get uploaded file
|
||||||
uploaded_file = request.files.get('file')
|
uploaded_file = request.files.get("file")
|
||||||
if not uploaded_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
|
# Determine correct save path based on cdmtype
|
||||||
filename = uploaded_file.filename
|
filename = uploaded_file.filename
|
||||||
save_path = os.path.join(pr_path if cdmtype == 'PR' else wv_path, filename)
|
assert filename is not None
|
||||||
|
target_path = pr_path if cdmtype == "PR" else wv_path
|
||||||
|
save_path = os.path.join(target_path, filename)
|
||||||
uploaded_file.save(save_path)
|
uploaded_file.save(save_path)
|
||||||
|
|
||||||
return jsonify({'message': 'Success', 'file_saved_to': save_path})
|
return jsonify({"message": "Success", "file_saved_to": save_path})
|
||||||
|
|
||||||
except Exception as e:
|
except (OSError, IOError, ValueError, AttributeError) as e:
|
||||||
logging.exception("Upload failed")
|
logging.exception("Upload failed: %s", {e})
|
||||||
return jsonify({'message': 'False', 'error': 'Server error'}), 500
|
return jsonify({"message": "False", "error": "Server error"}), 500
|
||||||
|
@ -1,54 +1,65 @@
|
|||||||
|
"""Module to handle the user changes."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from flask import Blueprint, request, jsonify, session
|
from flask import Blueprint, request, jsonify, session
|
||||||
from custom_functions.database.user_db import change_password, change_api_key
|
from custom_functions.database.user_db import change_password, change_api_key
|
||||||
|
|
||||||
user_change_bp = Blueprint('user_change_bp', __name__)
|
user_change_bp = Blueprint("user_change_bp", __name__)
|
||||||
|
|
||||||
# Define allowed characters regex (no spaces allowed)
|
# Define allowed characters regex (no spaces allowed)
|
||||||
PASSWORD_REGEX = re.compile(r'^[A-Za-z0-9!@#$%^&*()_+\-=\[\]{};\'":\\|,.<>\/?`~]+$')
|
PASSWORD_REGEX = re.compile(r'^[A-Za-z0-9!@#$%^&*()_+\-=\[\]{};\'":\\|,.<>\/?`~]+$')
|
||||||
|
|
||||||
@user_change_bp.route('/user/change_password', methods=['POST'])
|
|
||||||
|
@user_change_bp.route("/user/change_password", methods=["POST"])
|
||||||
def change_password_route():
|
def change_password_route():
|
||||||
username = session.get('username')
|
"""Handle the change password route."""
|
||||||
|
username = session.get("username")
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({'message': 'False'}), 400
|
return jsonify({"message": "False"}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
new_password = data.get('new_password', '')
|
new_password = data.get("new_password", "")
|
||||||
|
|
||||||
if not PASSWORD_REGEX.match(new_password):
|
if not PASSWORD_REGEX.match(new_password):
|
||||||
return jsonify({'message': 'Invalid password format'}), 400
|
return jsonify({"message": "Invalid password format"}), 400
|
||||||
|
|
||||||
change_password(username=username, new_password=new_password)
|
change_password(username=username, new_password=new_password)
|
||||||
return jsonify({'message': 'True'}), 200
|
return jsonify({"message": "True"}), 200
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'message': 'False'}), 400
|
return jsonify({"message": "False", "error": str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
@user_change_bp.route('/user/change_api_key', methods=['POST'])
|
@user_change_bp.route("/user/change_api_key", methods=["POST"])
|
||||||
def change_api_key_route():
|
def change_api_key_route():
|
||||||
|
"""Handle the change API key route."""
|
||||||
# Ensure the user is logged in by checking session for 'username'
|
# Ensure the user is logged in by checking session for 'username'
|
||||||
username = session.get('username')
|
username = session.get("username")
|
||||||
if not username:
|
if not username:
|
||||||
return jsonify({'message': 'False', 'error': 'User not logged in'}), 400
|
return jsonify({"message": "False", "error": "User not logged in"}), 400
|
||||||
|
|
||||||
# Get the new API key from the request body
|
# Get the new API key from the request body
|
||||||
new_api_key = request.json.get('new_api_key')
|
new_api_key = request.get_json().get("new_api_key")
|
||||||
|
|
||||||
if not new_api_key:
|
if not new_api_key:
|
||||||
return jsonify({'message': 'False', 'error': 'New API key not provided'}), 400
|
return jsonify({"message": "False", "error": "New API key not provided"}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Call the function to update the API key in the database
|
# Call the function to update the API key in the database
|
||||||
success = change_api_key(username=username, new_api_key=new_api_key)
|
success = change_api_key(username=username, new_api_key=new_api_key)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return jsonify({'message': 'True', 'success': 'API key changed successfully'}), 200
|
return (
|
||||||
|
jsonify({"message": "True", "success": "API key changed successfully"}),
|
||||||
|
200,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return jsonify({'message': 'False', 'error': 'Failed to change API key'}), 500
|
return (
|
||||||
|
jsonify({"message": "False", "error": "Failed to change API key"}),
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Catch any unexpected errors and return a response
|
# Catch any unexpected errors and return a response
|
||||||
return jsonify({'message': 'False', 'error': str(e)}), 500
|
return jsonify({"message": "False", "error": str(e)}), 500
|
||||||
|
@ -1,34 +1,61 @@
|
|||||||
from flask import Blueprint, request, jsonify, session
|
"""Module to handle the user info request."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
from custom_functions.database.user_db import fetch_api_key, fetch_styled_username, fetch_username_by_api_key
|
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__)
|
||||||
|
|
||||||
@user_info_bp.route('/userinfo', methods=['POST'])
|
|
||||||
|
def sanitize_username(username):
|
||||||
|
"""Sanitize the username."""
|
||||||
|
return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
|
||||||
|
|
||||||
|
|
||||||
|
@user_info_bp.route("/userinfo", methods=["POST"])
|
||||||
def user_info():
|
def user_info():
|
||||||
username = session.get('username')
|
"""Handle the user info request."""
|
||||||
|
username = session.get("username")
|
||||||
if not username:
|
if not username:
|
||||||
try:
|
try:
|
||||||
headers = request.headers
|
headers = request.headers
|
||||||
api_key = headers['Api-Key']
|
api_key = headers["Api-Key"]
|
||||||
username = fetch_username_by_api_key(api_key)
|
username = fetch_username_by_api_key(api_key)
|
||||||
except:
|
except Exception as e:
|
||||||
return jsonify({'message': 'False'}), 400
|
logging.exception("Error retrieving username by API key, %s", {e})
|
||||||
|
return jsonify({"message": "False"}), 400
|
||||||
|
|
||||||
|
safe_username = sanitize_username(username)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username.lower())
|
base_path = os.path.join(
|
||||||
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
|
os.getcwd(), "configs", "CDMs", "users_uploaded", safe_username
|
||||||
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"))
|
||||||
|
]
|
||||||
|
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'Username': username,
|
{
|
||||||
'Widevine_Devices': wv_files,
|
"Username": username,
|
||||||
'Playready_Devices': pr_files,
|
"Widevine_Devices": wv_files,
|
||||||
'API_Key': fetch_api_key(username),
|
"Playready_Devices": pr_files,
|
||||||
'Styled_Username': fetch_styled_username(username)
|
"API_Key": fetch_api_key(username),
|
||||||
})
|
"Styled_Username": fetch_styled_username(username),
|
||||||
|
}
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception("Error retrieving device files")
|
logging.exception("Error retrieving device files, %s", {e})
|
||||||
return jsonify({'message': 'False'}), 500
|
return jsonify({"message": "False"}), 500
|
||||||
|
Loading…
x
Reference in New Issue
Block a user