Compare commits
	
		
			31 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8697342e9c | ||
| 
						 | 
					f83d22c09e | ||
| 
						 | 
					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
 | 
			
		||||
pyinstallericon.ico
 | 
			
		||||
icon.ico
 | 
			
		||||
venv
 | 
			
		||||
frontend-dist
 | 
			
		||||
cdrm-frontend/dist/
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								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 3.13 was used at the time of writing  
 | 
			
		||||
- [Python](https://www.python.org/downloads/) version 3.12+ with PIP installed  
 | 
			
		||||
 | 
			
		||||
## Installation (Automatic) - Recommended   
 | 
			
		||||
- Extract contents of CDRM-Project 2.0 git contents into a new folder
 | 
			
		||||
  Python 3.13 was used at the time of writing
 | 
			
		||||
 | 
			
		||||
- [Node.js](https://nodejs.org/en/download/) v20+
 | 
			
		||||
 | 
			
		||||
## Installation (Automatic) - Recommended
 | 
			
		||||
 | 
			
		||||
- Extract contents of CDRM-Project into a new folder
 | 
			
		||||
- 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
 | 
			
		||||
 | 
			
		||||
## Installation (From binary)
 | 
			
		||||
 | 
			
		||||
- Download the latest release from the [releases](https://cdm-project.com/tpd94/CDRM-Project/releases) page and run the `.exe`
 | 
			
		||||
 | 
			
		||||
 ## Installation  (Manual)
 | 
			
		||||
 - Open your terminal and navigate to where you'd like to store the application  
 | 
			
		||||
 - Create a new python virtual environment using `python -m venv CDRM-Project`  
 | 
			
		||||
 - Change directory into the new `CDRM-Project` folder  
 | 
			
		||||
 - Activate the virtual environment  
 | 
			
		||||
## Installation  (Manual)
 | 
			
		||||
 | 
			
		||||
- Open your terminal and navigate to where you'd like to store the application
 | 
			
		||||
- Clone the project with `git clone https://cdm-project.com/tpd94/CDRM-Project.git`
 | 
			
		||||
- Navigate to the `CDRM-Project` folder
 | 
			
		||||
- Create a new python virtual environment using `python -m venv venv`
 | 
			
		||||
- Activate the virtual environment
 | 
			
		||||
 | 
			
		||||
  - Windows:
 | 
			
		||||
  
 | 
			
		||||
   > Windows - change directory into the `Scripts` directory then `activate.bat`  
 | 
			
		||||
    >   
 | 
			
		||||
    > Linux - `source bin/activate`  
 | 
			
		||||
  ```bash
 | 
			
		||||
  .\venv\Scripts\activate
 | 
			
		||||
  ```
 | 
			
		||||
 | 
			
		||||
  - Linux:
 | 
			
		||||
  
 | 
			
		||||
 - Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder  
 | 
			
		||||
 - Install python dependencies `pip install -r requirements.txt`  
 | 
			
		||||
 - (Optional) Create the folder structure `/configs/CDMs/WV` and place your .WVD file into `/configs/CDMs/WV`  
 | 
			
		||||
 - (Optional) Create the folder structure `/config/CDMs/PR` and place your .PRD file into `/configs/CDMs/PR`  
 | 
			
		||||
 - Run the application with `python main.py`
 | 
			
		||||
  ```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
 | 
			
		||||
							
								
								
									
										9
									
								
								cdrm-frontend/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								cdrm-frontend/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
    "trailingComma": "es5",
 | 
			
		||||
    "tabWidth": 4,
 | 
			
		||||
    "semi": true,
 | 
			
		||||
    "singleQuote": false,
 | 
			
		||||
    "useTabs": false,
 | 
			
		||||
    "printWidth": 100,
 | 
			
		||||
    "plugins": ["prettier-plugin-tailwindcss"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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 globals from 'globals'
 | 
			
		||||
import reactHooks from 'eslint-plugin-react-hooks'
 | 
			
		||||
import reactRefresh from 'eslint-plugin-react-refresh'
 | 
			
		||||
import js from "@eslint/js";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import reactHooks from "eslint-plugin-react-hooks";
 | 
			
		||||
import reactRefresh from "eslint-plugin-react-refresh";
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
  { ignores: ['dist'] },
 | 
			
		||||
  {
 | 
			
		||||
    files: ['**/*.{js,jsx}'],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      ecmaVersion: 2020,
 | 
			
		||||
      globals: globals.browser,
 | 
			
		||||
      parserOptions: {
 | 
			
		||||
        ecmaVersion: 'latest',
 | 
			
		||||
        ecmaFeatures: { jsx: true },
 | 
			
		||||
        sourceType: 'module',
 | 
			
		||||
      },
 | 
			
		||||
    { ignores: ["dist"] },
 | 
			
		||||
    {
 | 
			
		||||
        files: ["**/*.{js,jsx}"],
 | 
			
		||||
        languageOptions: {
 | 
			
		||||
            ecmaVersion: 2020,
 | 
			
		||||
            globals: globals.browser,
 | 
			
		||||
            parserOptions: {
 | 
			
		||||
                ecmaVersion: "latest",
 | 
			
		||||
                ecmaFeatures: { jsx: true },
 | 
			
		||||
                sourceType: "module",
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        plugins: {
 | 
			
		||||
            "react-hooks": reactHooks,
 | 
			
		||||
            "react-refresh": reactRefresh,
 | 
			
		||||
        },
 | 
			
		||||
        rules: {
 | 
			
		||||
            ...js.configs.recommended.rules,
 | 
			
		||||
            ...reactHooks.configs.recommended.rules,
 | 
			
		||||
            "no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
 | 
			
		||||
            "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    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>
 | 
			
		||||
<html lang="en" class="w-full h-full">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <link rel="icon" type="image/svg+xml" href="/favico.png" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <meta name="description" content="{{ data.description }}"/>
 | 
			
		||||
    <meta name="keywords" content="{{ data.keywords }}"/>
 | 
			
		||||
    <meta property='og:title' content="{{ data.opengraph_title }}" />
 | 
			
		||||
    <meta property='og:description' content="{{ data.opengraph_description }}" />
 | 
			
		||||
    <meta property='og:image' content="{{ data.opengraph_image }}" />
 | 
			
		||||
    <meta property='og:url' content="{{ data.opengraph_url }}" />
 | 
			
		||||
    <meta property='og:locale' content='en_US' />
 | 
			
		||||
    <title>{{ data.tab_title }}</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body class="w-full h-full">
 | 
			
		||||
    <div id="root" class="w-full h-full"></div>
 | 
			
		||||
    <script type="module" src="/src/main.jsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="UTF-8" />
 | 
			
		||||
        <link rel="icon" type="image/svg+xml" href="/favico.png" />
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
        <meta name="description" content="{{ data.description }}" />
 | 
			
		||||
        <meta name="keywords" content="{{ data.keywords }}" />
 | 
			
		||||
        <meta property="og:title" content="{{ data.opengraph_title }}" />
 | 
			
		||||
        <meta property="og:description" content="{{ data.opengraph_description }}" />
 | 
			
		||||
        <meta property="og:image" content="{{ data.opengraph_image }}" />
 | 
			
		||||
        <meta property="og:url" content="{{ data.opengraph_url }}" />
 | 
			
		||||
        <meta property="og:locale" content="en_US" />
 | 
			
		||||
        <title>{{ data.tab_title }}</title>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body class="w-full h-full">
 | 
			
		||||
        <div id="root" class="w-full h-full"></div>
 | 
			
		||||
        <script type="module" src="/src/main.jsx"></script>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7749
									
								
								cdrm-frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7749
									
								
								cdrm-frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,33 +1,37 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "cdrm-frontend",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "lint": "eslint .",
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@tailwindcss/vite": "^4.1.4",
 | 
			
		||||
    "axios": "^1.9.0",
 | 
			
		||||
    "react": "^19.0.0",
 | 
			
		||||
    "react-dom": "^19.0.0",
 | 
			
		||||
    "react-helmet": "^6.1.0",
 | 
			
		||||
    "react-router-dom": "^7.5.2",
 | 
			
		||||
    "shaka-player": "^4.14.9",
 | 
			
		||||
    "tailwindcss": "^4.1.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@eslint/js": "^9.22.0",
 | 
			
		||||
    "@types/react": "^19.0.10",
 | 
			
		||||
    "@types/react-dom": "^19.0.4",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.3.4",
 | 
			
		||||
    "eslint": "^9.22.0",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^5.2.0",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^0.4.19",
 | 
			
		||||
    "globals": "^16.0.0",
 | 
			
		||||
    "vite": "^6.3.1"
 | 
			
		||||
  }
 | 
			
		||||
    "name": "cdrm-frontend",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "0.0.0",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
        "build": "vite build",
 | 
			
		||||
        "lint": "eslint .",
 | 
			
		||||
        "preview": "vite preview"
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@tailwindcss/vite": "^4.1.11",
 | 
			
		||||
        "axios": "^1.10.0",
 | 
			
		||||
        "react": "^19.1.0",
 | 
			
		||||
        "react-dom": "^19.1.0",
 | 
			
		||||
        "react-icons": "^5.5.0",
 | 
			
		||||
        "react-router-dom": "^7.7.0",
 | 
			
		||||
        "shaka-player": "^4.15.8",
 | 
			
		||||
        "sonner": "^2.0.6",
 | 
			
		||||
        "tailwindcss": "^4.1.11"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@eslint/js": "^9.31.0",
 | 
			
		||||
        "@types/react": "^19.1.8",
 | 
			
		||||
        "@types/react-dom": "^19.1.6",
 | 
			
		||||
        "@vitejs/plugin-react": "^4.7.0",
 | 
			
		||||
        "@vitejs/plugin-react-swc": "^3.11.0",
 | 
			
		||||
        "daisyui": "^5.0.46",
 | 
			
		||||
        "eslint": "^9.31.0",
 | 
			
		||||
        "eslint-plugin-react-hooks": "^5.2.0",
 | 
			
		||||
        "eslint-plugin-react-refresh": "^0.4.20",
 | 
			
		||||
        "globals": "^16.3.0",
 | 
			
		||||
        "prettier-plugin-tailwindcss": "^0.6.14",
 | 
			
		||||
        "vite": "^7.0.5"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,43 +1,20 @@
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import Home from "./components/Pages/HomePage";
 | 
			
		||||
import Cache from "./components/Pages/Cache";
 | 
			
		||||
import API from "./components/Pages/API";
 | 
			
		||||
import TestPlayer from "./components/Pages/TestPlayer";
 | 
			
		||||
import NavBar from "./components/NavBar";
 | 
			
		||||
import NavBarMain from "./components/NavBarMain";
 | 
			
		||||
import SideMenu from "./components/SideMenu"; // Add this import
 | 
			
		||||
import { Route, Routes } from "react-router-dom";
 | 
			
		||||
import Account from "./components/Pages/Account";
 | 
			
		||||
import { Routes, Route } from "react-router-dom";
 | 
			
		||||
import API from "./components/Pages/API";
 | 
			
		||||
import Cache from "./components/Pages/Cache";
 | 
			
		||||
import Home from "./components/Pages/HomePage";
 | 
			
		||||
import TestPlayer from "./components/Pages/TestPlayer";
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  const [isMenuOpen, setIsMenuOpen] = useState(false); // Track if the menu is open
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div id="appcontainer" className="flex flex-row w-full h-full bg-black">
 | 
			
		||||
      {/* The SideMenu should be visible when isMenuOpen is true */}
 | 
			
		||||
      <SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
 | 
			
		||||
 | 
			
		||||
      <div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0">
 | 
			
		||||
        <NavBar />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
 | 
			
		||||
        <div id="navbarmaincontainer" className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5  sticky top-0 z-10">
 | 
			
		||||
          <NavBarMain setIsMenuOpen={setIsMenuOpen} />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div id="maincontentcontainer" className="w-full grow overflow-y-auto">
 | 
			
		||||
          <Routes>
 | 
			
		||||
    return (
 | 
			
		||||
        <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>
 | 
			
		||||
  );
 | 
			
		||||
        </Routes>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								cdrm-frontend/src/assets/fonts/InterVariable-Italic.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								cdrm-frontend/src/assets/fonts/InterVariable-Italic.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								cdrm-frontend/src/assets/fonts/InterVariable.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								cdrm-frontend/src/assets/fonts/InterVariable.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										15
									
								
								cdrm-frontend/src/assets/fonts/font-face.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								cdrm-frontend/src/assets/fonts/font-face.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: Inter;
 | 
			
		||||
    src: url("./InterVariable.woff2");
 | 
			
		||||
    font-style: normal;
 | 
			
		||||
    font-weight: 300 900;
 | 
			
		||||
    font-display: swap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: Inter;
 | 
			
		||||
    src: url("./InterVariable-Italic.woff2");
 | 
			
		||||
    font-style: italic;
 | 
			
		||||
    font-weight: 300 900;
 | 
			
		||||
    font-display: swap;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								cdrm-frontend/src/components/Container.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								cdrm-frontend/src/components/Container.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
const Container = ({ children, className = "", ...props }) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <main className={`container mx-auto p-4 mb-5 ${className}`} {...props}>
 | 
			
		||||
            {children}
 | 
			
		||||
        </main>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Container;
 | 
			
		||||
@ -4,11 +4,11 @@ import "./license_protocol.min.js";
 | 
			
		||||
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
 | 
			
		||||
 | 
			
		||||
function uint8ArrayToBase64(uint8Array) {
 | 
			
		||||
  const binaryString = Array.from(uint8Array)
 | 
			
		||||
    .map(b => String.fromCharCode(b))
 | 
			
		||||
    .join('');
 | 
			
		||||
    const binaryString = Array.from(uint8Array)
 | 
			
		||||
        .map((b) => String.fromCharCode(b))
 | 
			
		||||
        .join("");
 | 
			
		||||
 | 
			
		||||
  return btoa(binaryString);
 | 
			
		||||
    return btoa(binaryString);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parseFetch(fetchString) {
 | 
			
		||||
@ -17,10 +17,13 @@ function parseFetch(fetchString) {
 | 
			
		||||
 | 
			
		||||
    // Use a more lenient regex to capture the fetch pattern (including complex bodies)
 | 
			
		||||
    const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
 | 
			
		||||
    const lines = fetchString.split('\n').map(line => line.trim()).filter(Boolean);
 | 
			
		||||
    const lines = fetchString
 | 
			
		||||
        .split("\n")
 | 
			
		||||
        .map((line) => line.trim())
 | 
			
		||||
        .filter(Boolean);
 | 
			
		||||
    const result = {
 | 
			
		||||
        method: 'UNDEFINED',
 | 
			
		||||
        url: '',
 | 
			
		||||
        method: "UNDEFINED",
 | 
			
		||||
        url: "",
 | 
			
		||||
        headers: {},
 | 
			
		||||
        body: null,
 | 
			
		||||
    };
 | 
			
		||||
@ -47,9 +50,12 @@ function parseFetch(fetchString) {
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]);
 | 
			
		||||
const PLAYREADY_SYSTEM_ID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]);
 | 
			
		||||
const WIDEVINE_SYSTEM_ID = new Uint8Array([
 | 
			
		||||
    0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed,
 | 
			
		||||
]);
 | 
			
		||||
const PLAYREADY_SYSTEM_ID = new Uint8Array([
 | 
			
		||||
    0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
 | 
			
		||||
]);
 | 
			
		||||
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
 | 
			
		||||
 | 
			
		||||
function intToUint8Array(num, endian) {
 | 
			
		||||
@ -75,44 +81,44 @@ function psshDataToPsshBoxB64(pssh_data, system_id) {
 | 
			
		||||
        ...new Uint8Array(4),
 | 
			
		||||
        ...system_id,
 | 
			
		||||
        ...intToUint8Array(dataLength, false),
 | 
			
		||||
        ...pssh_data
 | 
			
		||||
        ...pssh_data,
 | 
			
		||||
    ]);
 | 
			
		||||
    return uint8ArrayToBase64(pssh);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function wrmHeaderToPlayReadyHeader(wrm_header){
 | 
			
		||||
function wrmHeaderToPlayReadyHeader(wrm_header) {
 | 
			
		||||
    const playready_object = new Uint8Array([
 | 
			
		||||
        ...shortToUint8Array(1, true),
 | 
			
		||||
        ...shortToUint8Array(wrm_header.length, true),
 | 
			
		||||
        ...wrm_header
 | 
			
		||||
        ...wrm_header,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return new Uint8Array([
 | 
			
		||||
        ...intToUint8Array(playready_object.length + 2 + 4, true),
 | 
			
		||||
        ...shortToUint8Array(1, true),
 | 
			
		||||
        ...playready_object
 | 
			
		||||
        ...playready_object,
 | 
			
		||||
    ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function encodeUtf16LE(str) {
 | 
			
		||||
  const buffer = new Uint8Array(str.length * 2);
 | 
			
		||||
  for (let i = 0; i < str.length; i++) {
 | 
			
		||||
    const code = str.charCodeAt(i);
 | 
			
		||||
    buffer[i * 2] = code & 0xff;
 | 
			
		||||
    buffer[i * 2 + 1] = code >> 8;
 | 
			
		||||
  }
 | 
			
		||||
  return buffer;
 | 
			
		||||
    const buffer = new Uint8Array(str.length * 2);
 | 
			
		||||
    for (let i = 0; i < str.length; i++) {
 | 
			
		||||
        const code = str.charCodeAt(i);
 | 
			
		||||
        buffer[i * 2] = code & 0xff;
 | 
			
		||||
        buffer[i * 2 + 1] = code >> 8;
 | 
			
		||||
    }
 | 
			
		||||
    return buffer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stringToUint8Array(string) {
 | 
			
		||||
    return Uint8Array.from(string.split("").map(x => x.charCodeAt()));
 | 
			
		||||
    return Uint8Array.from(string.split("").map((x) => x.charCodeAt()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function readTextFromClipboard() {
 | 
			
		||||
    try {
 | 
			
		||||
        // Request text from the clipboard
 | 
			
		||||
        const clipboardText = await navigator.clipboard.readText();
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        const result = parseFetch(clipboardText);
 | 
			
		||||
 | 
			
		||||
        let pssh_data_string;
 | 
			
		||||
@ -136,11 +142,15 @@ export async function readTextFromClipboard() {
 | 
			
		||||
                license_request = LicenseRequest.decode(signed_message.msg);
 | 
			
		||||
            } catch (decodeError) {
 | 
			
		||||
                // If error occurs during decoding, return an empty pssh
 | 
			
		||||
                console.error('Decoding failed, returning empty pssh', decodeError);
 | 
			
		||||
                pssh_data_string = '';  // Empty pssh if decoding fails
 | 
			
		||||
                console.error("Decoding failed, returning empty pssh", decodeError);
 | 
			
		||||
                pssh_data_string = ""; // Empty pssh if decoding fails
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (license_request && license_request.contentId && license_request.contentId.widevinePsshData) {
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                license_request &&
 | 
			
		||||
                license_request.contentId &&
 | 
			
		||||
                license_request.contentId.widevinePsshData
 | 
			
		||||
            ) {
 | 
			
		||||
                const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
 | 
			
		||||
                pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
 | 
			
		||||
            }
 | 
			
		||||
@ -160,14 +170,12 @@ export async function readTextFromClipboard() {
 | 
			
		||||
        document.getElementById("pssh").value = pssh_data_string;
 | 
			
		||||
        document.getElementById("data").value = payload_string;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('Failed to read clipboard contents:', error);
 | 
			
		||||
        console.error("Failed to read clipboard contents:", error);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to check if the data is binary
 | 
			
		||||
function isBinary(uint8Array) {
 | 
			
		||||
    // Check for non-text (non-ASCII) bytes (basic heuristic)
 | 
			
		||||
    return uint8Array.some(byte => byte > 127); // Non-ASCII byte indicates binary
 | 
			
		||||
    return uint8Array.some((byte) => byte > 127); // Non-ASCII byte indicates binary
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,162 +1,178 @@
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { NavLink } from 'react-router-dom';
 | 
			
		||||
import homeIcon from '../assets/icons/home.svg';
 | 
			
		||||
import cacheIcon from '../assets/icons/cache.svg';
 | 
			
		||||
import apiIcon from '../assets/icons/api.svg';
 | 
			
		||||
import testPlayerIcon from '../assets/icons/testplayer.svg';
 | 
			
		||||
import accountIcon from '../assets/icons/account.svg'; 
 | 
			
		||||
import discordIcon from '../assets/icons/discord.svg';
 | 
			
		||||
import telegramIcon from '../assets/icons/telegram.svg';
 | 
			
		||||
import giteaIcon from '../assets/icons/gitea.svg';
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { NavLink } from "react-router-dom";
 | 
			
		||||
import { FaDiscord } from "react-icons/fa";
 | 
			
		||||
import { FaTelegram } from "react-icons/fa";
 | 
			
		||||
import { SiGitea } from "react-icons/si";
 | 
			
		||||
import { FaHome } from "react-icons/fa";
 | 
			
		||||
import { FaDatabase } from "react-icons/fa";
 | 
			
		||||
import { IoCodeSlashSharp } from "react-icons/io5";
 | 
			
		||||
import { FaVideo } from "react-icons/fa";
 | 
			
		||||
import { RiAccountCircleFill } from "react-icons/ri";
 | 
			
		||||
 | 
			
		||||
function NavBar() {
 | 
			
		||||
    const [externalLinks, setExternalLinks] = useState({
 | 
			
		||||
        discord: '#',
 | 
			
		||||
        telegram: '#',
 | 
			
		||||
        gitea: '#',
 | 
			
		||||
        discord: "#",
 | 
			
		||||
        telegram: "#",
 | 
			
		||||
        gitea: "#",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        fetch('/api/links')
 | 
			
		||||
            .then(response => response.json())
 | 
			
		||||
            .then(data => setExternalLinks(data))
 | 
			
		||||
            .catch(error => console.error('Error fetching links:', error));
 | 
			
		||||
        fetch("/api/links")
 | 
			
		||||
            .then((response) => response.json())
 | 
			
		||||
            .then((data) => setExternalLinks(data))
 | 
			
		||||
            .catch((error) => console.error("Error fetching links:", error));
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const MenuItem = ({ to, children }) => {
 | 
			
		||||
        return (
 | 
			
		||||
            <li>
 | 
			
		||||
                <NavLink to={to} className={({ isActive }) => (isActive ? "menu-active" : "")}>
 | 
			
		||||
                    {children}
 | 
			
		||||
                </NavLink>
 | 
			
		||||
            </li>
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="flex flex-col w-full h-full bg-white/1">
 | 
			
		||||
            {/* Header */}
 | 
			
		||||
            <div>
 | 
			
		||||
                <p className="text-white text-2xl font-bold p-3 text-center mb-5">
 | 
			
		||||
                    <a href="/">CDRM-Project</a>
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        <>
 | 
			
		||||
            <div className="navbar sticky top-0 z-300 bg-slate-700 shadow-sm text-white">
 | 
			
		||||
                <div className="navbar-start">
 | 
			
		||||
                    <div className="dropdown">
 | 
			
		||||
                        <div tabIndex={0} role="button" className="btn btn-ghost lg:hidden">
 | 
			
		||||
                            <svg
 | 
			
		||||
                                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                                className="h-5 w-5"
 | 
			
		||||
                                fill="none"
 | 
			
		||||
                                viewBox="0 0 24 24"
 | 
			
		||||
                                stroke="currentColor"
 | 
			
		||||
                            >
 | 
			
		||||
                                {" "}
 | 
			
		||||
                                <path
 | 
			
		||||
                                    strokeLinecap="round"
 | 
			
		||||
                                    strokeLinejoin="round"
 | 
			
		||||
                                    strokeWidth="2"
 | 
			
		||||
                                    d="M4 6h16M4 12h8m-8 6h16"
 | 
			
		||||
                                />{" "}
 | 
			
		||||
                            </svg>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <ul
 | 
			
		||||
                            tabIndex={0}
 | 
			
		||||
                            className="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
 | 
			
		||||
                        >
 | 
			
		||||
                            <MenuItem to="/">
 | 
			
		||||
                                <FaHome alt="Home" width={20} height={20} />
 | 
			
		||||
                                Home
 | 
			
		||||
                            </MenuItem>
 | 
			
		||||
                            <MenuItem to="/cache">
 | 
			
		||||
                                <FaDatabase alt="Cache" width={20} height={20} />
 | 
			
		||||
                                Cache
 | 
			
		||||
                            </MenuItem>
 | 
			
		||||
                            <MenuItem to="/api">
 | 
			
		||||
                                <IoCodeSlashSharp alt="API" width={20} height={20} />
 | 
			
		||||
                                API
 | 
			
		||||
                            </MenuItem>
 | 
			
		||||
                            <MenuItem to="/testplayer">
 | 
			
		||||
                                <FaVideo alt="Test Player" width={20} height={20} />
 | 
			
		||||
                                Test Player
 | 
			
		||||
                            </MenuItem>
 | 
			
		||||
                            <MenuItem to="/account">
 | 
			
		||||
                                <RiAccountCircleFill alt="My Account" width={20} height={20} />
 | 
			
		||||
                                My Account
 | 
			
		||||
                            </MenuItem>
 | 
			
		||||
 | 
			
		||||
            {/* Scrollable navigation area */}
 | 
			
		||||
            <div className="overflow-y-auto grow flex flex-col">
 | 
			
		||||
                {/* Main NavLinks */}
 | 
			
		||||
                <NavLink
 | 
			
		||||
                    to="/"
 | 
			
		||||
                    className={({ isActive }) =>
 | 
			
		||||
                        `flex flex-row p-3 border-l-3 ${
 | 
			
		||||
                            isActive
 | 
			
		||||
                                ? 'border-l-sky-500/50 bg-black/50'
 | 
			
		||||
                                : 'hover:border-l-sky-500/50 hover:bg-white/5'
 | 
			
		||||
                        }`
 | 
			
		||||
                    }
 | 
			
		||||
                >
 | 
			
		||||
                    <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
 | 
			
		||||
                        <img src={homeIcon} alt="Home" className="w-1/2 cursor-pointer" />
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
 | 
			
		||||
                        Home
 | 
			
		||||
                    </p>
 | 
			
		||||
                </NavLink>
 | 
			
		||||
 | 
			
		||||
                <NavLink
 | 
			
		||||
                    to="/cache"
 | 
			
		||||
                    className={({ isActive }) =>
 | 
			
		||||
                        `flex flex-row p-3 border-l-3 ${
 | 
			
		||||
                            isActive
 | 
			
		||||
                                ? 'border-l-emerald-500/50 bg-black/50'
 | 
			
		||||
                                : 'hover:border-l-emerald-500/50 hover:bg-white/5'
 | 
			
		||||
                        }`
 | 
			
		||||
                    }
 | 
			
		||||
                >
 | 
			
		||||
                    <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
 | 
			
		||||
                        <img src={cacheIcon} alt="Cache" className="w-1/2 cursor-pointer" />
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
 | 
			
		||||
                        Cache
 | 
			
		||||
                    </p>
 | 
			
		||||
                </NavLink>
 | 
			
		||||
 | 
			
		||||
                <NavLink
 | 
			
		||||
                    to="/api"
 | 
			
		||||
                    className={({ isActive }) =>
 | 
			
		||||
                        `flex flex-row p-3 border-l-3 ${
 | 
			
		||||
                            isActive
 | 
			
		||||
                                ? 'border-l-indigo-500/50 bg-black/50'
 | 
			
		||||
                                : 'hover:border-l-indigo-500/50 hover:bg-white/5'
 | 
			
		||||
                        }`
 | 
			
		||||
                    }
 | 
			
		||||
                >
 | 
			
		||||
                    <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
 | 
			
		||||
                        <img src={apiIcon} alt="API" className="w-1/2 cursor-pointer" />
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
 | 
			
		||||
                        API
 | 
			
		||||
                    </p>
 | 
			
		||||
                </NavLink>
 | 
			
		||||
 | 
			
		||||
                <NavLink
 | 
			
		||||
                    to="/testplayer"
 | 
			
		||||
                    className={({ isActive }) =>
 | 
			
		||||
                        `flex flex-row p-3 border-l-3 ${
 | 
			
		||||
                            isActive
 | 
			
		||||
                                ? 'border-l-rose-500/50 bg-black/50'
 | 
			
		||||
                                : 'hover:border-l-rose-500/50 hover:bg-white/5'
 | 
			
		||||
                        }`
 | 
			
		||||
                    }
 | 
			
		||||
                >
 | 
			
		||||
                    <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" />
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
 | 
			
		||||
                        Test Player
 | 
			
		||||
                    </p>
 | 
			
		||||
                </NavLink>
 | 
			
		||||
 | 
			
		||||
                {/* Account link at bottom of scrollable area */}
 | 
			
		||||
                <div className="mt-auto">
 | 
			
		||||
                    <NavLink
 | 
			
		||||
                        to="/account"
 | 
			
		||||
                        className={({ isActive }) =>
 | 
			
		||||
                            `flex flex-row p-3 border-l-3 ${
 | 
			
		||||
                                isActive
 | 
			
		||||
                                    ? 'border-l-yellow-500/50 bg-black/50'
 | 
			
		||||
                                    : 'hover:border-l-yellow-500/50 hover:bg-white/5'
 | 
			
		||||
                            }`
 | 
			
		||||
                        }
 | 
			
		||||
                    >
 | 
			
		||||
                        <button className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer">
 | 
			
		||||
                            <img src={accountIcon} alt="Account" className="w-1/2 cursor-pointer" />
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <p className="grow text-white md:text-2xl font-bold flex items-center justify-start">
 | 
			
		||||
                            <div className="divider">Social links</div>
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <a
 | 
			
		||||
                                    href={externalLinks.discord}
 | 
			
		||||
                                    target="_blank"
 | 
			
		||||
                                    rel="noopener noreferrer"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <FaDiscord alt="Discord" width={20} height={20} />
 | 
			
		||||
                                    Discord
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <a
 | 
			
		||||
                                    href={externalLinks.telegram}
 | 
			
		||||
                                    target="_blank"
 | 
			
		||||
                                    rel="noopener noreferrer"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <FaTelegram alt="Telegram" width={20} height={20} />
 | 
			
		||||
                                    Telegram
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <a
 | 
			
		||||
                                    href={externalLinks.gitea}
 | 
			
		||||
                                    target="_blank"
 | 
			
		||||
                                    rel="noopener noreferrer"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <SiGitea alt="Gitea" width={20} height={20} />
 | 
			
		||||
                                    Gitea
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <a className="btn btn-ghost text-xl">CDRM-Project</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="navbar-center hidden lg:flex">
 | 
			
		||||
                    <ul className="menu menu-horizontal px-1">
 | 
			
		||||
                        <MenuItem to="/">
 | 
			
		||||
                            <FaHome alt="Home" width={20} height={20} />
 | 
			
		||||
                            Home
 | 
			
		||||
                        </MenuItem>
 | 
			
		||||
                        <MenuItem to="/cache">
 | 
			
		||||
                            <FaDatabase alt="Cache" width={20} height={20} />
 | 
			
		||||
                            Cache
 | 
			
		||||
                        </MenuItem>
 | 
			
		||||
                        <MenuItem to="/api">
 | 
			
		||||
                            <IoCodeSlashSharp alt="API" width={20} height={20} />
 | 
			
		||||
                            API
 | 
			
		||||
                        </MenuItem>
 | 
			
		||||
                        <MenuItem to="/testplayer">
 | 
			
		||||
                            <FaVideo alt="Test Player" width={20} height={20} />
 | 
			
		||||
                            Test Player
 | 
			
		||||
                        </MenuItem>
 | 
			
		||||
                        <MenuItem to="/account">
 | 
			
		||||
                            <RiAccountCircleFill alt="My Account" width={20} height={20} />
 | 
			
		||||
                            My Account
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </NavLink>
 | 
			
		||||
                        </MenuItem>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="navbar-end hidden lg:flex">
 | 
			
		||||
                    <a
 | 
			
		||||
                        className="btn btn-ghost hover:text-indigo-400"
 | 
			
		||||
                        href={externalLinks.discord}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        rel="noopener noreferrer"
 | 
			
		||||
                    >
 | 
			
		||||
                        <div className="tooltip tooltip-bottom" data-tip="CDRM Discord">
 | 
			
		||||
                            <FaDiscord className="h-6 w-6" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a
 | 
			
		||||
                        className="btn btn-ghost hover:text-sky-400"
 | 
			
		||||
                        href={externalLinks.telegram}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        rel="noopener noreferrer"
 | 
			
		||||
                    >
 | 
			
		||||
                        <div className="tooltip tooltip-bottom" data-tip="CDRM Telegram">
 | 
			
		||||
                            <FaTelegram className="h-6 w-6" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a
 | 
			
		||||
                        className="btn btn-ghost hover:text-lime-400"
 | 
			
		||||
                        href={externalLinks.gitea}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        rel="noopener noreferrer"
 | 
			
		||||
                    >
 | 
			
		||||
                        <div className="tooltip tooltip-left" data-tip="CDRM Gitea">
 | 
			
		||||
                            <SiGitea className="h-6 w-6" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {/* External links at very bottom */}
 | 
			
		||||
            <div className="flex flex-row w-full h-16 bg-black/25">
 | 
			
		||||
                <a
 | 
			
		||||
                    href={externalLinks.discord}
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                    rel="noopener noreferrer"
 | 
			
		||||
                    className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group"
 | 
			
		||||
                >
 | 
			
		||||
                    <img src={discordIcon} alt="Discord" className="w-1/2 group-hover:animate-bounce" />
 | 
			
		||||
                </a>
 | 
			
		||||
                <a
 | 
			
		||||
                    href={externalLinks.telegram}
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                    rel="noopener noreferrer"
 | 
			
		||||
                    className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group"
 | 
			
		||||
                >
 | 
			
		||||
                    <img src={telegramIcon} alt="Telegram" className="w-1/2 group-hover:animate-bounce" />
 | 
			
		||||
                </a>
 | 
			
		||||
                <a
 | 
			
		||||
                    href={externalLinks.gitea}
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                    rel="noopener noreferrer"
 | 
			
		||||
                    className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-green-700 group"
 | 
			
		||||
                >
 | 
			
		||||
                    <img src={giteaIcon} alt="Gitea" className="w-1/2 group-hover:animate-bounce" />
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,21 +2,21 @@ import { useState } from "react";
 | 
			
		||||
import hamburgerIcon from "../assets/icons/hamburger.svg";
 | 
			
		||||
 | 
			
		||||
function NavBarMain({ setIsMenuOpen }) {
 | 
			
		||||
  const handleMenuToggle = () => {
 | 
			
		||||
    setIsMenuOpen((prevState) => !prevState); // Toggle the menu state
 | 
			
		||||
  };
 | 
			
		||||
    const handleMenuToggle = () => {
 | 
			
		||||
        setIsMenuOpen((prevState) => !prevState); // Toggle the menu state
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-row w-full h-full bg-white/1">
 | 
			
		||||
      <button className="w-24 p-4" onClick={handleMenuToggle}>
 | 
			
		||||
        <img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" />
 | 
			
		||||
      </button>
 | 
			
		||||
      <p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
 | 
			
		||||
        CDRM-Project
 | 
			
		||||
      </p>
 | 
			
		||||
      <div className="w-24 p-4"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="flex flex-row w-full h-full bg-white/1">
 | 
			
		||||
            <button className="w-24 p-4" onClick={handleMenuToggle}>
 | 
			
		||||
                <img src={hamburgerIcon} alt="Menu" className="w-full h-full cursor-pointer" />
 | 
			
		||||
            </button>
 | 
			
		||||
            <p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
 | 
			
		||||
                CDRM-Project
 | 
			
		||||
            </p>
 | 
			
		||||
            <div className="w-24 p-4"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default NavBarMain;
 | 
			
		||||
 | 
			
		||||
@ -1,73 +1,76 @@
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import { Helmet } from 'react-helmet'; // Import Helmet
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import NavBar from "../NavBar";
 | 
			
		||||
import Container from "../Container";
 | 
			
		||||
import { FaCopy } from "react-icons/fa";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
 | 
			
		||||
const { protocol, hostname, port } = window.location;
 | 
			
		||||
 | 
			
		||||
let fullHost = `${protocol}//${hostname}`;
 | 
			
		||||
if (
 | 
			
		||||
  (protocol === 'http:' && port !== '80') ||
 | 
			
		||||
  (protocol === 'https:' && port !== '443' && port !== '')
 | 
			
		||||
    (protocol === "http:" && port !== "80") ||
 | 
			
		||||
    (protocol === "https:" && port !== "443" && port !== "")
 | 
			
		||||
) {
 | 
			
		||||
  fullHost += `:${port}`;
 | 
			
		||||
    fullHost += `:${port}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleCopy = (text) => {
 | 
			
		||||
    navigator.clipboard.writeText(text);
 | 
			
		||||
    toast.success("Copied to clipboard");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function API() {
 | 
			
		||||
  const [deviceInfo, setDeviceInfo] = useState({
 | 
			
		||||
    device_type: '',
 | 
			
		||||
    system_id: '',
 | 
			
		||||
    security_level: '',
 | 
			
		||||
    host: '',
 | 
			
		||||
    secret: '',
 | 
			
		||||
    device_name: ''
 | 
			
		||||
  });
 | 
			
		||||
    const [deviceInfo, setDeviceInfo] = useState({
 | 
			
		||||
        device_type: "",
 | 
			
		||||
        system_id: "",
 | 
			
		||||
        security_level: "",
 | 
			
		||||
        host: "",
 | 
			
		||||
        secret: "",
 | 
			
		||||
        device_name: "",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  const [prDeviceInfo, setPrDeviceInfo] = useState({
 | 
			
		||||
    security_level: '',
 | 
			
		||||
    host: '',
 | 
			
		||||
    secret: '',
 | 
			
		||||
    device_name: ''
 | 
			
		||||
  });
 | 
			
		||||
    const [prDeviceInfo, setPrDeviceInfo] = useState({
 | 
			
		||||
        security_level: "",
 | 
			
		||||
        host: "",
 | 
			
		||||
        secret: "",
 | 
			
		||||
        device_name: "",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Fetch Widevine info
 | 
			
		||||
    fetch('/remotecdm/widevine/deviceinfo')
 | 
			
		||||
      .then(response => response.json())
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        setDeviceInfo({
 | 
			
		||||
          device_type: data.device_type,
 | 
			
		||||
          system_id: data.system_id,
 | 
			
		||||
          security_level: data.security_level,
 | 
			
		||||
          host: data.host,
 | 
			
		||||
          secret: data.secret,
 | 
			
		||||
          device_name: data.device_name
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => console.error('Error fetching Widevine info:', error));
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // Fetch Widevine info
 | 
			
		||||
        fetch("/remotecdm/widevine/deviceinfo")
 | 
			
		||||
            .then((response) => response.json())
 | 
			
		||||
            .then((data) => {
 | 
			
		||||
                setDeviceInfo({
 | 
			
		||||
                    device_type: data.device_type,
 | 
			
		||||
                    system_id: data.system_id,
 | 
			
		||||
                    security_level: data.security_level,
 | 
			
		||||
                    host: data.host,
 | 
			
		||||
                    secret: data.secret,
 | 
			
		||||
                    device_name: data.device_name,
 | 
			
		||||
                });
 | 
			
		||||
            })
 | 
			
		||||
            .catch((error) => console.error("Error fetching Widevine info:", error));
 | 
			
		||||
 | 
			
		||||
    // Fetch PlayReady info
 | 
			
		||||
    fetch('/remotecdm/playready/deviceinfo')
 | 
			
		||||
      .then(response => response.json())
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        setPrDeviceInfo({
 | 
			
		||||
          security_level: data.security_level,
 | 
			
		||||
          host: data.host,
 | 
			
		||||
          secret: data.secret,
 | 
			
		||||
          device_name: data.device_name
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => console.error('Error fetching PlayReady info:', error));
 | 
			
		||||
  }, []);
 | 
			
		||||
        // Fetch PlayReady info
 | 
			
		||||
        fetch("/remotecdm/playready/deviceinfo")
 | 
			
		||||
            .then((response) => response.json())
 | 
			
		||||
            .then((data) => {
 | 
			
		||||
                setPrDeviceInfo({
 | 
			
		||||
                    security_level: data.security_level,
 | 
			
		||||
                    host: data.host,
 | 
			
		||||
                    secret: data.secret,
 | 
			
		||||
                    device_name: data.device_name,
 | 
			
		||||
                });
 | 
			
		||||
            })
 | 
			
		||||
            .catch((error) => console.error("Error fetching PlayReady info:", error));
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col w-full overflow-y-auto p-4 text-white">
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>API</title>
 | 
			
		||||
      </Helmet>
 | 
			
		||||
        <details open className='w-full list-none'>
 | 
			
		||||
            <summary className='text-2xl'>Sending a decryption request</summary>
 | 
			
		||||
            <div className='mt-5 p-5 rounded-lg border-2 border-indigo-500/50'>  
 | 
			
		||||
              <pre className='rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto'>
 | 
			
		||||
              {`import requests
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        document.title = "API | CDRM-Project";
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const decryptRequest = `import requests
 | 
			
		||||
 | 
			
		||||
print(requests.post(
 | 
			
		||||
    url='${fullHost}/api/decrypt',
 | 
			
		||||
@ -83,52 +86,155 @@ print(requests.post(
 | 
			
		||||
            'Accept-Language': 'en-US,en;q=0.5',
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
).json()['message'])`}
 | 
			
		||||
              </pre>
 | 
			
		||||
            </div>
 | 
			
		||||
        </details>
 | 
			
		||||
        <details open className='w-full list-none mt-5'>
 | 
			
		||||
            <summary className='text-2xl'>Sending a search request</summary>
 | 
			
		||||
            <div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg'>
 | 
			
		||||
            <pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
 | 
			
		||||
{`import requests
 | 
			
		||||
).json()['message'])`;
 | 
			
		||||
 | 
			
		||||
    const searchRequest = `import requests
 | 
			
		||||
 | 
			
		||||
print(requests.post(
 | 
			
		||||
    url='${fullHost}/api/cache/search',
 | 
			
		||||
    json={
 | 
			
		||||
        'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
 | 
			
		||||
    }
 | 
			
		||||
).json())`}
 | 
			
		||||
</pre>
 | 
			
		||||
            </div>
 | 
			
		||||
        </details>
 | 
			
		||||
        <details open className='w-full list-none mt-5'>
 | 
			
		||||
            <summary className='text-2xl'>PyWidevine RemoteCDM info</summary>
 | 
			
		||||
            <div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'>
 | 
			
		||||
                <p>
 | 
			
		||||
                    <strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
 | 
			
		||||
                    <strong>System ID:</strong> {deviceInfo.system_id}<br />
 | 
			
		||||
                    <strong>Security Level:</strong> {deviceInfo.security_level}<br />
 | 
			
		||||
                    <strong>Host:</strong> {fullHost}/remotecdm/widevine<br />
 | 
			
		||||
                    <strong>Secret:</strong> '{deviceInfo.secret}'<br />
 | 
			
		||||
                    <strong>Device Name:</strong> {deviceInfo.device_name}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </details>
 | 
			
		||||
        <details open className='w-full list-none mt-5'>
 | 
			
		||||
            <summary className='text-2xl'>PyPlayready RemoteCDM info</summary>
 | 
			
		||||
            <div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'>
 | 
			
		||||
                <p>
 | 
			
		||||
                    <strong>Security Level:</strong> {prDeviceInfo.security_level}<br />
 | 
			
		||||
                    <strong>Host:</strong> {fullHost}/remotecdm/playready<br />
 | 
			
		||||
                    <strong>Secret:</strong> '{prDeviceInfo.secret}'<br />
 | 
			
		||||
                    <strong>Device Name:</strong> {prDeviceInfo.device_name}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </details>
 | 
			
		||||
).json())`;
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <NavBar />
 | 
			
		||||
            <Container>
 | 
			
		||||
                <div className="mx-auto flex w-full max-w-2xl flex-col justify-center py-8">
 | 
			
		||||
                    <div className="join join-vertical w-full max-w-2xl">
 | 
			
		||||
                        <div
 | 
			
		||||
                            tabIndex={0}
 | 
			
		||||
                            className="collapse-arrow join-item collapse border border-gray-600"
 | 
			
		||||
                        >
 | 
			
		||||
                            <input type="checkbox" defaultChecked />
 | 
			
		||||
                            <div className="collapse-title text-lg font-semibold">
 | 
			
		||||
                                Sending a decryption request
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div className="collapse-content text-slate-300">
 | 
			
		||||
                                <pre className="my-4 overflow-auto rounded-lg font-mono break-all whitespace-pre-wrap">
 | 
			
		||||
                                    {decryptRequest}
 | 
			
		||||
                                </pre>
 | 
			
		||||
                                <div className="flex justify-end">
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        className="btn btn-primary"
 | 
			
		||||
                                        onClick={() => handleCopy(decryptRequest)}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <FaCopy /> Copy
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div
 | 
			
		||||
                            tabIndex={0}
 | 
			
		||||
                            className="collapse-arrow join-item collapse border border-gray-600"
 | 
			
		||||
                        >
 | 
			
		||||
                            <input type="checkbox" defaultChecked />
 | 
			
		||||
                            <div className="collapse-title text-lg font-semibold">
 | 
			
		||||
                                Sending a search request
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div className="collapse-content text-slate-300">
 | 
			
		||||
                                <pre className="my-4 overflow-auto rounded-lg font-mono break-all whitespace-pre-wrap">
 | 
			
		||||
                                    {searchRequest}
 | 
			
		||||
                                </pre>
 | 
			
		||||
                                <div className="flex justify-end">
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        className="btn btn-primary"
 | 
			
		||||
                                        onClick={() => handleCopy(searchRequest)}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <FaCopy /> Copy
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div
 | 
			
		||||
                            tabIndex={0}
 | 
			
		||||
                            className="collapse-arrow join-item collapse border border-gray-600"
 | 
			
		||||
                        >
 | 
			
		||||
                            <input type="checkbox" defaultChecked />
 | 
			
		||||
                            <div className="collapse-title text-lg font-semibold">
 | 
			
		||||
                                PyWidevine RemoteCDM info
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div className="collapse-content text-slate-300">
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Device Type:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">
 | 
			
		||||
                                        {deviceInfo.device_type || "N/A"}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>System ID:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">
 | 
			
		||||
                                        {deviceInfo.system_id || "N/A"}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Security Level:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">
 | 
			
		||||
                                        {deviceInfo.security_level || "N/A"}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Host:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">{fullHost}/remotecdm/widevine</span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Secret:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">{deviceInfo.secret || "N/A"}</span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Device Name:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">
 | 
			
		||||
                                        {deviceInfo.device_name || "N/A"}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div
 | 
			
		||||
                            tabIndex={0}
 | 
			
		||||
                            className="collapse-arrow join-item collapse border border-gray-600"
 | 
			
		||||
                        >
 | 
			
		||||
                            <input type="checkbox" defaultChecked />
 | 
			
		||||
                            <div className="collapse-title text-lg font-semibold">
 | 
			
		||||
                                PyPlayready RemoteCDM info
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div className="collapse-content text-slate-300">
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Security Level:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">
 | 
			
		||||
                                        {prDeviceInfo.security_level || "N/A"}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Host:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">
 | 
			
		||||
                                        {fullHost}/remotecdm/playready
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Secret:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">
 | 
			
		||||
                                        {prDeviceInfo.secret || "N/A"}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    <strong>Device Name:</strong>{" "}
 | 
			
		||||
                                    <span className="font-mono">
 | 
			
		||||
                                        {prDeviceInfo.device_name || "N/A"}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </Container>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default API;
 | 
			
		||||
 | 
			
		||||
@ -1,38 +1,47 @@
 | 
			
		||||
import React, { useEffect, useState } from "react";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import Container from "../Container";
 | 
			
		||||
import NavBar from "../NavBar";
 | 
			
		||||
import MyAccount from "./MyAccount";
 | 
			
		||||
import Register from "./Register";
 | 
			
		||||
import MyAccount from "./MyAccount"; // <-- Import the MyAccount component
 | 
			
		||||
 | 
			
		||||
function Account() {
 | 
			
		||||
  const [isLoggedIn, setIsLoggedIn] = useState(null); // null = loading state
 | 
			
		||||
    const [isLoggedIn, setIsLoggedIn] = useState(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetch('/login/status', {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      credentials: 'include', // Sends cookies with request
 | 
			
		||||
    })
 | 
			
		||||
    .then(res => res.json())
 | 
			
		||||
    .then(data => {
 | 
			
		||||
      if (data.message === 'True') {
 | 
			
		||||
        setIsLoggedIn(true);
 | 
			
		||||
      } else {
 | 
			
		||||
        setIsLoggedIn(false);
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(err => {
 | 
			
		||||
      console.error("Error checking login status:", err);
 | 
			
		||||
      setIsLoggedIn(false); // Assume not logged in on error
 | 
			
		||||
    });
 | 
			
		||||
  }, []);
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        fetch("/login/status", {
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            credentials: "include",
 | 
			
		||||
        })
 | 
			
		||||
            .then((res) => res.json())
 | 
			
		||||
            .then((data) => {
 | 
			
		||||
                if (data.message === "True") {
 | 
			
		||||
                    setIsLoggedIn(true);
 | 
			
		||||
                } else {
 | 
			
		||||
                    setIsLoggedIn(false);
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .catch((err) => {
 | 
			
		||||
                toast.error(`Error checking login status. Reason: ${err.message}`);
 | 
			
		||||
                console.error("Error checking login status:", err);
 | 
			
		||||
                setIsLoggedIn(false);
 | 
			
		||||
            });
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
  if (isLoggedIn === null) {
 | 
			
		||||
    return <div>Loading...</div>; // Optional loading UI
 | 
			
		||||
  }
 | 
			
		||||
    if (isLoggedIn === null) {
 | 
			
		||||
        return <div>Loading...</div>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div id="accountpage" className="w-full h-full flex">
 | 
			
		||||
      {isLoggedIn ? <MyAccount /> : <Register />}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <NavBar />
 | 
			
		||||
            <Container>
 | 
			
		||||
                <div id="accountpage" className="flex h-full w-full">
 | 
			
		||||
                    {isLoggedIn ? <MyAccount /> : <Register />}
 | 
			
		||||
                </div>
 | 
			
		||||
            </Container>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Account;
 | 
			
		||||
 | 
			
		||||
@ -1,21 +1,27 @@
 | 
			
		||||
import { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import { Helmet } from 'react-helmet'; // Import Helmet
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import { FaDownload } from "react-icons/fa";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import Container from "../Container";
 | 
			
		||||
import NavBar from "../NavBar";
 | 
			
		||||
 | 
			
		||||
function Cache() {
 | 
			
		||||
    const [searchQuery, setSearchQuery] = useState('');
 | 
			
		||||
    const [searchQuery, setSearchQuery] = useState("");
 | 
			
		||||
    const [cacheData, setCacheData] = useState([]);
 | 
			
		||||
    const [keyCount, setKeyCount] = useState(0); // New state to store the key count
 | 
			
		||||
    const [keyCount, setKeyCount] = useState(0);
 | 
			
		||||
    const [loading, setLoading] = useState(false);
 | 
			
		||||
    const [hasSearched, setHasSearched] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const debounceTimeout = useRef(null);
 | 
			
		||||
 | 
			
		||||
    // Fetch the key count when the component mounts
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const fetchKeyCount = async () => {
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch('/api/cache/keycount');
 | 
			
		||||
                const response = await fetch("/api/cache/keycount");
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
                setKeyCount(data.count); // Update key count
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error fetching key count:', error);
 | 
			
		||||
                console.error("Error fetching key count:", error);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -24,83 +30,112 @@ function Cache() {
 | 
			
		||||
 | 
			
		||||
    const handleInputChange = (event) => {
 | 
			
		||||
        const query = event.target.value;
 | 
			
		||||
        setSearchQuery(query); // Update the search query
 | 
			
		||||
    
 | 
			
		||||
        // Clear the previous timeout
 | 
			
		||||
        setSearchQuery(query);
 | 
			
		||||
 | 
			
		||||
        if (debounceTimeout.current) {
 | 
			
		||||
            clearTimeout(debounceTimeout.current);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set a new timeout to send the API call after 1 second of no typing
 | 
			
		||||
        debounceTimeout.current = setTimeout(() => {
 | 
			
		||||
            if (query.trim() !== '') {
 | 
			
		||||
                sendApiCall(query); // Only call the API if the query is not empty
 | 
			
		||||
            } else {
 | 
			
		||||
                setCacheData([]); // Clear results if query is empty
 | 
			
		||||
            }
 | 
			
		||||
        }, 1000); // 1 second delay
 | 
			
		||||
        if (query.trim() !== "") {
 | 
			
		||||
            setLoading(true); // Show spinner immediately
 | 
			
		||||
            debounceTimeout.current = setTimeout(() => {
 | 
			
		||||
                sendApiCall(query);
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        } else {
 | 
			
		||||
            setHasSearched(false); // Reset state when input is cleared
 | 
			
		||||
            setCacheData([]);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const sendApiCall = (text) => {
 | 
			
		||||
        fetch('/api/cache/search', {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            headers: { 'Content-Type': 'application/json' },
 | 
			
		||||
        setLoading(true);
 | 
			
		||||
        fetch("/api/cache/search", {
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            headers: { "Content-Type": "application/json" },
 | 
			
		||||
            body: JSON.stringify({ input: text }),
 | 
			
		||||
        })
 | 
			
		||||
            .then((response) => response.json())
 | 
			
		||||
            .then((data) => setCacheData(data)) // Update cache data with the results
 | 
			
		||||
            .catch((error) => console.error('Error:', error));
 | 
			
		||||
            .then((data) => {
 | 
			
		||||
                setCacheData(data);
 | 
			
		||||
                setHasSearched(true);
 | 
			
		||||
            })
 | 
			
		||||
            .catch((error) => {
 | 
			
		||||
                toast.error(`Error: ${error.message}`);
 | 
			
		||||
                console.error("Error:", error);
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => setLoading(false));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        document.title = "Cache | CDRM-Project";
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="flex flex-col w-full h-full overflow-y-auto p-4">
 | 
			
		||||
            <Helmet>
 | 
			
		||||
                <title>Cache</title>
 | 
			
		||||
            </Helmet>
 | 
			
		||||
            <div className="flex flex-col lg:flex-row w-full lg:h-12 items-center">
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={searchQuery}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    placeholder={`Search ${keyCount} keys...`} // Dynamic placeholder
 | 
			
		||||
                    className="lg:grow w-full border-2 border-emerald-500/25 rounded-xl h-10 self-center m-2 text-white p-1 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 transition-all duration-200 ease-in-out"
 | 
			
		||||
                />
 | 
			
		||||
                <a
 | 
			
		||||
                    href="/api/cache/download"
 | 
			
		||||
                    className="bg-emerald-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-10 truncate w-full text-center flex items-center justify-center m-2"
 | 
			
		||||
                >
 | 
			
		||||
                    Download Cache
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="w-full grow p-4 border-2 border-emerald-500/50 rounded-2xl mt-5 overflow-y-auto">
 | 
			
		||||
                <table className="min-w-full text-white">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th className="p-2 border border-black">PSSH</th>
 | 
			
		||||
                            <th className="p-2 border border-black">KID</th>
 | 
			
		||||
                            <th className="p-2 border border-black">Key</th>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                        {cacheData.length > 0 ? (
 | 
			
		||||
                            cacheData.map((item, index) => (
 | 
			
		||||
                                <tr key={index}>
 | 
			
		||||
                                    <td className="p-2 border border-black">{item.PSSH}</td>
 | 
			
		||||
                                    <td className="p-2 border border-black">{item.KID}</td>
 | 
			
		||||
                                    <td className="p-2 border border-black">{item.Key}</td>
 | 
			
		||||
                                </tr>
 | 
			
		||||
                            ))
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <td colSpan="3" className="p-2 border border-black text-center">
 | 
			
		||||
                                    No data found
 | 
			
		||||
                                </td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        )}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                </table>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <>
 | 
			
		||||
            <NavBar />
 | 
			
		||||
            <Container>
 | 
			
		||||
                <div className="my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row">
 | 
			
		||||
                    <fieldset className="fieldset w-full max-w-2xl">
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            value={searchQuery}
 | 
			
		||||
                            onChange={handleInputChange}
 | 
			
		||||
                            placeholder={`Search ${keyCount} keys...`}
 | 
			
		||||
                            className="input w-full max-w-2xl font-mono"
 | 
			
		||||
                        />
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
                    <button
 | 
			
		||||
                        className="btn btn-success"
 | 
			
		||||
                        onClick={() => {
 | 
			
		||||
                            window.location.href = "/api/cache/download";
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        <FaDownload />
 | 
			
		||||
                        Download keys cache
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {loading ? (
 | 
			
		||||
                    <div className="flex justify-center py-16">
 | 
			
		||||
                        <span className="loading loading-spinner loading-md me-2"></span>
 | 
			
		||||
                        Searching...
 | 
			
		||||
                    </div>
 | 
			
		||||
                ) : cacheData.length > 0 ? (
 | 
			
		||||
                    <div className="my-4 flex justify-center">
 | 
			
		||||
                        <div className="overflow-x-auto">
 | 
			
		||||
                            <table className="table">
 | 
			
		||||
                                <thead>
 | 
			
		||||
                                    <tr>
 | 
			
		||||
                                        <th></th>
 | 
			
		||||
                                        <th className="text-center">PSSH</th>
 | 
			
		||||
                                        <th className="text-center">key ID:key pair</th>
 | 
			
		||||
                                    </tr>
 | 
			
		||||
                                </thead>
 | 
			
		||||
                                <tbody>
 | 
			
		||||
                                    {cacheData.map((item, index) => (
 | 
			
		||||
                                        <tr key={index}>
 | 
			
		||||
                                            <th>{index + 1}</th>
 | 
			
		||||
                                            <td className="font-mono">{item.PSSH}</td>
 | 
			
		||||
                                            <td className="font-mono">
 | 
			
		||||
                                                {item.KID}:{item.Key}
 | 
			
		||||
                                            </td>
 | 
			
		||||
                                        </tr>
 | 
			
		||||
                                    ))}
 | 
			
		||||
                                </tbody>
 | 
			
		||||
                            </table>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                ) : hasSearched ? (
 | 
			
		||||
                    <div className="flex justify-center py-16">
 | 
			
		||||
                        <div className="text-center">No data found in the database</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <div className="flex justify-center py-16">
 | 
			
		||||
                        <div className="text-center">Enter a search query to see results</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                )}
 | 
			
		||||
            </Container>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,248 +1,290 @@
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import { readTextFromClipboard } from '../Functions/ParseChallenge';
 | 
			
		||||
import { Helmet } from 'react-helmet'; // Import Helmet
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import { readTextFromClipboard } from "../Functions/ParseChallenge";
 | 
			
		||||
import NavBar from "../NavBar";
 | 
			
		||||
import Container from "../Container";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import { IoInformationCircleOutline } from "react-icons/io5";
 | 
			
		||||
 | 
			
		||||
function HomePage() {
 | 
			
		||||
  const [pssh, setPssh] = useState('');
 | 
			
		||||
  const [licurl, setLicurl] = useState('');
 | 
			
		||||
  const [proxy, setProxy] = useState('');
 | 
			
		||||
  const [headers, setHeaders] = useState('');
 | 
			
		||||
  const [cookies, setCookies] = useState('');
 | 
			
		||||
  const [data, setData] = useState('');
 | 
			
		||||
  const [message, setMessage] = useState('');
 | 
			
		||||
  const [isVisible, setIsVisible] = useState(false);
 | 
			
		||||
  const [devices, setDevices] = useState([]);
 | 
			
		||||
  const [selectedDevice, setSelectedDevice] = useState('default');
 | 
			
		||||
    const [pssh, setPssh] = useState("");
 | 
			
		||||
    const [licurl, setLicurl] = useState("");
 | 
			
		||||
    const [proxy, setProxy] = useState("");
 | 
			
		||||
    const [headers, setHeaders] = useState("");
 | 
			
		||||
    const [cookies, setCookies] = useState("");
 | 
			
		||||
    const [data, setData] = useState("");
 | 
			
		||||
    const [message, setMessage] = useState("");
 | 
			
		||||
    const [isVisible, setIsVisible] = useState(false);
 | 
			
		||||
    const [devices, setDevices] = useState([]);
 | 
			
		||||
    const [selectedDevice, setSelectedDevice] = useState("default");
 | 
			
		||||
 | 
			
		||||
  const bottomRef = useRef(null);
 | 
			
		||||
  const messageRef = useRef(null); // Reference to result container
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        document.title = "Home | CDRM-Project";
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
  const handleReset = () => {
 | 
			
		||||
    if (isVisible) {
 | 
			
		||||
      setIsVisible(false);
 | 
			
		||||
    }
 | 
			
		||||
    setPssh('');
 | 
			
		||||
    setLicurl('');
 | 
			
		||||
    setProxy('');
 | 
			
		||||
    setHeaders('');
 | 
			
		||||
    setCookies('');
 | 
			
		||||
    setData('');
 | 
			
		||||
  };
 | 
			
		||||
    const bottomRef = useRef(null);
 | 
			
		||||
    const messageRef = useRef(null); // Reference to result container
 | 
			
		||||
 | 
			
		||||
  const handleSubmitButton = (event) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
 | 
			
		||||
    fetch('/api/decrypt', {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Content-Type': 'application/json',
 | 
			
		||||
      },
 | 
			
		||||
      body: JSON.stringify({
 | 
			
		||||
        pssh: pssh,
 | 
			
		||||
        licurl: licurl,
 | 
			
		||||
        proxy: proxy,
 | 
			
		||||
        headers: headers,
 | 
			
		||||
        cookies: cookies,
 | 
			
		||||
        data: data,
 | 
			
		||||
        device: selectedDevice, // Include selected device in the request
 | 
			
		||||
      }),
 | 
			
		||||
    })
 | 
			
		||||
      .then(response => response.json())
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        const resultMessage = data['message'].replace(/\n/g, '<br />');
 | 
			
		||||
        setMessage(resultMessage);
 | 
			
		||||
        setIsVisible(true);
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        console.error('Error during decryption request:', error);
 | 
			
		||||
        setMessage('Error: Unable to process request.');
 | 
			
		||||
        setIsVisible(true);
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleCopy = (event) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    if (messageRef.current) {
 | 
			
		||||
      const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
 | 
			
		||||
      navigator.clipboard.writeText(textToCopy).catch(err => {
 | 
			
		||||
        alert('Failed to copy!');
 | 
			
		||||
        console.error(err);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleFetchPaste = () => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    readTextFromClipboard().then(() => {
 | 
			
		||||
      setPssh(document.getElementById("pssh").value);
 | 
			
		||||
      setLicurl(document.getElementById("licurl").value);
 | 
			
		||||
      setHeaders(document.getElementById("headers").value);
 | 
			
		||||
      setData(document.getElementById("data").value);
 | 
			
		||||
    }).catch(err => {
 | 
			
		||||
      alert('Failed to paste from fetch!');
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isVisible && bottomRef.current) {
 | 
			
		||||
      bottomRef.current.scrollIntoView({ behavior: 'smooth' });
 | 
			
		||||
    }
 | 
			
		||||
  }, [message, isVisible]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetch('/login/status', {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
    })
 | 
			
		||||
      .then(res => res.json())
 | 
			
		||||
      .then(statusData => {
 | 
			
		||||
        if (statusData.message === 'True') {
 | 
			
		||||
          return fetch('/userinfo', { method: 'POST' });
 | 
			
		||||
        } else {
 | 
			
		||||
          throw new Error('Not logged in');
 | 
			
		||||
    const handleReset = () => {
 | 
			
		||||
        if (isVisible) {
 | 
			
		||||
            setIsVisible(false);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .then(res => res.json())
 | 
			
		||||
      .then(deviceData => {
 | 
			
		||||
        const combinedDevices = [
 | 
			
		||||
          ...deviceData.Widevine_Devices,
 | 
			
		||||
          ...deviceData.Playready_Devices,
 | 
			
		||||
        ];
 | 
			
		||||
        setPssh("");
 | 
			
		||||
        setLicurl("");
 | 
			
		||||
        setProxy("");
 | 
			
		||||
        setHeaders("");
 | 
			
		||||
        setCookies("");
 | 
			
		||||
        setData("");
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
        // Add default devices if logged in
 | 
			
		||||
        const allDevices = [
 | 
			
		||||
          "CDRM-Project Public Widevine CDM", 
 | 
			
		||||
          "CDRM-Project Public PlayReady CDM",
 | 
			
		||||
          ...combinedDevices,
 | 
			
		||||
        ];
 | 
			
		||||
    const handleSubmitButton = (event) => {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
 | 
			
		||||
        // 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');
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
        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. Reason: ${error.message}`);
 | 
			
		||||
                setIsVisible(true);
 | 
			
		||||
            });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
 | 
			
		||||
        <Helmet>
 | 
			
		||||
          <title>CDRM-Project</title>
 | 
			
		||||
        </Helmet>
 | 
			
		||||
        <form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
 | 
			
		||||
          <label htmlFor="pssh" className="text-white w-8/10 self-center">PSSH: </label>
 | 
			
		||||
          <input
 | 
			
		||||
            type="text"
 | 
			
		||||
            id="pssh"
 | 
			
		||||
            className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
 | 
			
		||||
            value={pssh}
 | 
			
		||||
            onChange={(e) => setPssh(e.target.value)}
 | 
			
		||||
          />
 | 
			
		||||
          <label htmlFor="licurl" className="text-white w-8/10 self-center">License URL: </label>
 | 
			
		||||
          <input
 | 
			
		||||
            type="text"
 | 
			
		||||
            id="licurl"
 | 
			
		||||
            className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
 | 
			
		||||
            value={licurl}
 | 
			
		||||
            onChange={(e) => setLicurl(e.target.value)}
 | 
			
		||||
          />
 | 
			
		||||
          <label htmlFor="proxy" className="text-white w-8/10 self-center">Proxy: </label>
 | 
			
		||||
          <input
 | 
			
		||||
            type="text"
 | 
			
		||||
            id="proxy"
 | 
			
		||||
            className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
 | 
			
		||||
            value={proxy}
 | 
			
		||||
            onChange={(e) => setProxy(e.target.value)}
 | 
			
		||||
          />
 | 
			
		||||
          <label htmlFor="headers" className="text-white w-8/10 self-center">Headers: </label>
 | 
			
		||||
          <textarea
 | 
			
		||||
            id="headers"
 | 
			
		||||
            className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
 | 
			
		||||
            value={headers}
 | 
			
		||||
            onChange={(e) => setHeaders(e.target.value)}
 | 
			
		||||
          />
 | 
			
		||||
          <label htmlFor="cookies" className="text-white w-8/10 self-center">Cookies: </label>
 | 
			
		||||
          <textarea
 | 
			
		||||
            id="cookies"
 | 
			
		||||
            className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
 | 
			
		||||
            value={cookies}
 | 
			
		||||
            onChange={(e) => setCookies(e.target.value)}
 | 
			
		||||
          />
 | 
			
		||||
          <label htmlFor="data" className="text-white w-8/10 self-center">Data: </label>
 | 
			
		||||
          <textarea
 | 
			
		||||
            id="data"
 | 
			
		||||
            className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
 | 
			
		||||
            value={data}
 | 
			
		||||
            onChange={(e) => setData(e.target.value)}
 | 
			
		||||
          />
 | 
			
		||||
    const handleCopy = (event) => {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        if (messageRef.current) {
 | 
			
		||||
            const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
 | 
			
		||||
            toast.success("Copied to clipboard");
 | 
			
		||||
            navigator.clipboard.writeText(textToCopy).catch((err) => {
 | 
			
		||||
                toast.error(`Failed to copy. Reason: ${err.message}`);
 | 
			
		||||
                console.error(err);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
          {/* 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>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
    const handleFetchPaste = (event) => {
 | 
			
		||||
        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) => {
 | 
			
		||||
                toast.error(`Failed to paste from fetch. Reason: ${err.message}`);
 | 
			
		||||
                console.error("Failed to paste from fetch:", err);
 | 
			
		||||
            });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
          <div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
 | 
			
		||||
              onClick={handleSubmitButton}
 | 
			
		||||
            >
 | 
			
		||||
              Submit
 | 
			
		||||
            </button>
 | 
			
		||||
            <button onClick={handleFetchPaste} className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0">
 | 
			
		||||
              Paste from fetch
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
 | 
			
		||||
              onClick={handleReset}
 | 
			
		||||
            >
 | 
			
		||||
              Reset
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </form>
 | 
			
		||||
      </div>
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (isVisible && bottomRef.current) {
 | 
			
		||||
            bottomRef.current.scrollIntoView({ behavior: "smooth" });
 | 
			
		||||
        }
 | 
			
		||||
    }, [message, isVisible]);
 | 
			
		||||
 | 
			
		||||
      {isVisible && (
 | 
			
		||||
        <div id="main_content" className="flex-col w-full h-full p-10 items-center justify-center self-center">
 | 
			
		||||
          <div className="flex flex-col w-full h-full overflow-y-auto items-center">
 | 
			
		||||
            <div className='w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5'>
 | 
			
		||||
              <p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
 | 
			
		||||
              <p
 | 
			
		||||
                className="w-full grow pt-10 break-words overflow-y-auto"
 | 
			
		||||
                ref={messageRef}
 | 
			
		||||
                dangerouslySetInnerHTML={{ __html: message }}
 | 
			
		||||
              />
 | 
			
		||||
              <div ref={bottomRef} />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
 | 
			
		||||
            <button
 | 
			
		||||
              className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
 | 
			
		||||
              onClick={handleCopy}
 | 
			
		||||
            >
 | 
			
		||||
              Copy Results
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        fetch("/login/status", {
 | 
			
		||||
            method: "POST",
 | 
			
		||||
        })
 | 
			
		||||
            .then((res) => res.json())
 | 
			
		||||
            .then((statusData) => {
 | 
			
		||||
                if (statusData.message === "True") {
 | 
			
		||||
                    return fetch("/userinfo", { method: "POST" });
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw new Error("Not logged in");
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .then((res) => res.json())
 | 
			
		||||
            .then((deviceData) => {
 | 
			
		||||
                const combinedDevices = [
 | 
			
		||||
                    ...deviceData.Widevine_Devices,
 | 
			
		||||
                    ...deviceData.Playready_Devices,
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
                // Add default devices if logged in
 | 
			
		||||
                const allDevices = [
 | 
			
		||||
                    "CDRM-Project Public Widevine CDM",
 | 
			
		||||
                    "CDRM-Project Public PlayReady CDM",
 | 
			
		||||
                    ...combinedDevices,
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
                // Set devices and select a device if logged in
 | 
			
		||||
                setDevices(allDevices.length > 0 ? allDevices : []);
 | 
			
		||||
                setSelectedDevice(allDevices.length > 0 ? allDevices[0] : "default");
 | 
			
		||||
            })
 | 
			
		||||
            .catch(() => {
 | 
			
		||||
                // User isn't logged in, set default device to 'default'
 | 
			
		||||
                setDevices([]); // Don't display devices list
 | 
			
		||||
                setSelectedDevice("default");
 | 
			
		||||
            });
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <NavBar />
 | 
			
		||||
            <Container>
 | 
			
		||||
                <div className="mx-auto flex w-full max-w-2xl flex-col justify-center">
 | 
			
		||||
                    <fieldset className="fieldset">
 | 
			
		||||
                        <legend className="fieldset-legend text-base">PSSH*</legend>
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            className="input w-full font-mono"
 | 
			
		||||
                            placeholder="Enter PSSH here"
 | 
			
		||||
                            value={pssh}
 | 
			
		||||
                            onChange={(e) => setPssh(e.target.value)}
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                        <p className="label text-red-500">* Required</p>
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
                    <fieldset className="fieldset">
 | 
			
		||||
                        <legend className="fieldset-legend text-base">License URL*</legend>
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            className="input w-full font-mono"
 | 
			
		||||
                            placeholder="Enter License URL here"
 | 
			
		||||
                            value={licurl}
 | 
			
		||||
                            onChange={(e) => setLicurl(e.target.value)}
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                        <p className="label text-red-500">* Required</p>
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
                    <fieldset className="fieldset">
 | 
			
		||||
                        <legend className="fieldset-legend text-base">Proxy</legend>
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            className="input w-full font-mono"
 | 
			
		||||
                            placeholder="Enter Proxy here (https://example.com:8080)"
 | 
			
		||||
                            value={proxy}
 | 
			
		||||
                            onChange={(e) => setProxy(e.target.value)}
 | 
			
		||||
                        />
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
                    <fieldset className="fieldset">
 | 
			
		||||
                        <legend className="fieldset-legend text-base">
 | 
			
		||||
                            Headers*
 | 
			
		||||
                            <div
 | 
			
		||||
                                className="tooltip"
 | 
			
		||||
                                data-tip="You can use https://curlconverter.com/python/ to paste the header values here"
 | 
			
		||||
                            >
 | 
			
		||||
                                <IoInformationCircleOutline className="h-5 w-5" />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </legend>
 | 
			
		||||
                        <textarea
 | 
			
		||||
                            className="textarea h-48 w-full font-mono"
 | 
			
		||||
                            placeholder="Enter headers here (JSON format). E.g. {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'}"
 | 
			
		||||
                            value={headers}
 | 
			
		||||
                            onChange={(e) => setHeaders(e.target.value)}
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                        <p className="label text-red-500">* Required</p>
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
                    <fieldset className="fieldset">
 | 
			
		||||
                        <legend className="fieldset-legend text-base">Cookies</legend>
 | 
			
		||||
                        <textarea
 | 
			
		||||
                            className="textarea h-48 w-full font-mono"
 | 
			
		||||
                            placeholder="Enter cookies here (JSON format)"
 | 
			
		||||
                            value={cookies}
 | 
			
		||||
                            onChange={(e) => setCookies(e.target.value)}
 | 
			
		||||
                        />
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
                    <fieldset className="fieldset">
 | 
			
		||||
                        <legend className="fieldset-legend text-base">Data</legend>
 | 
			
		||||
                        <textarea
 | 
			
		||||
                            className="textarea h-48 w-full font-mono"
 | 
			
		||||
                            placeholder="Enter data here (JSON format)"
 | 
			
		||||
                            value={data}
 | 
			
		||||
                            onChange={(e) => setData(e.target.value)}
 | 
			
		||||
                        />
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
                    {/* Device Selection Dropdown, only show if logged in */}
 | 
			
		||||
                    {devices.length > 0 && (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <fieldset className="fieldset">
 | 
			
		||||
                                <legend className="fieldset-legend text-base">Select device</legend>
 | 
			
		||||
                                <select
 | 
			
		||||
                                    className="select w-full"
 | 
			
		||||
                                    value={selectedDevice}
 | 
			
		||||
                                    onChange={(e) => setSelectedDevice(e.target.value)}
 | 
			
		||||
                                >
 | 
			
		||||
                                    {devices.map((device, index) => (
 | 
			
		||||
                                        <option key={index} value={device}>
 | 
			
		||||
                                            {device}
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                    ))}
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </fieldset>
 | 
			
		||||
                        </>
 | 
			
		||||
                    )}
 | 
			
		||||
                    <div className="mx-auto my-4 flex w-full flex-col items-center justify-center gap-2 lg:flex-row">
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            className="btn btn-primary btn-wide"
 | 
			
		||||
                            onClick={handleSubmitButton}
 | 
			
		||||
                            disabled={pssh === "" || licurl === "" || headers === ""}
 | 
			
		||||
                        >
 | 
			
		||||
                            Submit
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            className="btn btn-info btn-wide"
 | 
			
		||||
                            onClick={handleFetchPaste}
 | 
			
		||||
                        >
 | 
			
		||||
                            Paste from fetch
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            className="btn btn-error btn-wide"
 | 
			
		||||
                            onClick={handleReset}
 | 
			
		||||
                        >
 | 
			
		||||
                            Reset
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {isVisible && (
 | 
			
		||||
                    <>
 | 
			
		||||
                        <div className="mx-auto my-4 flex w-full max-w-2xl flex-col justify-center">
 | 
			
		||||
                            <div className="card bg-base-100 card-lg border border-gray-500 shadow-sm">
 | 
			
		||||
                                <div className="card-body">
 | 
			
		||||
                                    <h2 className="card-title">Result</h2>
 | 
			
		||||
                                    <div className="divider"></div>
 | 
			
		||||
                                    <p
 | 
			
		||||
                                        className="w-full grow overflow-y-auto font-mono break-words"
 | 
			
		||||
                                        ref={messageRef}
 | 
			
		||||
                                        dangerouslySetInnerHTML={{ __html: message }}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <div ref={bottomRef} />
 | 
			
		||||
                                    <div
 | 
			
		||||
                                        className="card-actions mt-4 justify-end"
 | 
			
		||||
                                        onClick={handleCopy}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <button className="btn btn-success">Copy results</button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </>
 | 
			
		||||
                )}
 | 
			
		||||
            </Container>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default HomePage;
 | 
			
		||||
 | 
			
		||||
@ -1,262 +1,318 @@
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import axios from "axios";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import Container from "../Container";
 | 
			
		||||
 | 
			
		||||
function MyAccount() {
 | 
			
		||||
  const [wvList, setWvList] = useState([]);
 | 
			
		||||
  const [prList, setPrList] = useState([]);
 | 
			
		||||
  const [uploading, setUploading] = useState(false);
 | 
			
		||||
  const [username, setUsername] = useState('');
 | 
			
		||||
  const [apiKey, setApiKey] = useState('');
 | 
			
		||||
  const [password, setPassword] = useState('');
 | 
			
		||||
  const [passwordError, setPasswordError] = useState('');
 | 
			
		||||
  const [newApiKey, setNewApiKey] = useState('');
 | 
			
		||||
  const [apiKeyError, setApiKeyError] = useState('');
 | 
			
		||||
    const [wvList, setWvList] = useState([]);
 | 
			
		||||
    const [prList, setPrList] = useState([]);
 | 
			
		||||
    const [uploading, setUploading] = useState(false);
 | 
			
		||||
    const [username, setUsername] = useState("");
 | 
			
		||||
    const [apiKey, setApiKey] = useState("");
 | 
			
		||||
    const [password, setPassword] = useState("");
 | 
			
		||||
    const [passwordError, setPasswordError] = useState("");
 | 
			
		||||
    const [newApiKey, setNewApiKey] = useState("");
 | 
			
		||||
    const [apiKeyError, setApiKeyError] = useState("");
 | 
			
		||||
 | 
			
		||||
  // Fetch user info
 | 
			
		||||
  const fetchUserInfo = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.post('/userinfo');
 | 
			
		||||
      setWvList(response.data.Widevine_Devices || []);
 | 
			
		||||
      setPrList(response.data.Playready_Devices || []);
 | 
			
		||||
      setUsername(response.data.Styled_Username || '');
 | 
			
		||||
      setApiKey(response.data.API_Key || '');
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error('Failed to fetch user info', err);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
    // Fetch user info
 | 
			
		||||
    const fetchUserInfo = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await axios.post("/userinfo");
 | 
			
		||||
            setWvList(response.data.Widevine_Devices || []);
 | 
			
		||||
            setPrList(response.data.Playready_Devices || []);
 | 
			
		||||
            setUsername(response.data.Styled_Username || "");
 | 
			
		||||
            setApiKey(response.data.API_Key || "");
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            toast.error(`Failed to fetch user info. Reason: ${err.message}`);
 | 
			
		||||
            console.error("Failed to fetch user info", err);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchUserInfo();
 | 
			
		||||
  }, []);
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        fetchUserInfo();
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
  // Handle file upload
 | 
			
		||||
  const handleUpload = async (event, cdmType) => {
 | 
			
		||||
    const file = event.target.files[0];
 | 
			
		||||
    if (!file) return;
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        document.title = "My account | CDRM-Project";
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const extension = file.name.split('.').pop();
 | 
			
		||||
    if ((cdmType === 'PR' && extension !== 'prd') || (cdmType === 'WV' && extension !== 'wvd')) {
 | 
			
		||||
      alert(`Please upload a .${cdmType === 'PR' ? 'prd' : 'wvd'} file.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // Handle file upload
 | 
			
		||||
    const handleUpload = async (event, cdmType) => {
 | 
			
		||||
        const file = event.target.files[0];
 | 
			
		||||
        if (!file) return;
 | 
			
		||||
 | 
			
		||||
    const formData = new FormData();
 | 
			
		||||
    formData.append('file', file);
 | 
			
		||||
        const extension = file.name.split(".").pop();
 | 
			
		||||
        if (
 | 
			
		||||
            (cdmType === "PR" && extension !== "prd") ||
 | 
			
		||||
            (cdmType === "WV" && extension !== "wvd")
 | 
			
		||||
        ) {
 | 
			
		||||
            toast.error(`Please upload a .${cdmType === "PR" ? "prd" : "wvd"} file.`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    setUploading(true);
 | 
			
		||||
    try {
 | 
			
		||||
      await axios.post(`/upload/${cdmType}`, formData);
 | 
			
		||||
      await fetchUserInfo(); // Refresh list after upload
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error('Upload failed', err);
 | 
			
		||||
      alert('Upload failed');
 | 
			
		||||
    } finally {
 | 
			
		||||
      setUploading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
        const formData = new FormData();
 | 
			
		||||
        formData.append("file", file);
 | 
			
		||||
 | 
			
		||||
  // Handle logout
 | 
			
		||||
  const handleLogout = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await axios.post('/logout');
 | 
			
		||||
      window.location.reload();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Logout failed:', error);
 | 
			
		||||
      alert('Logout failed!');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
        setUploading(true);
 | 
			
		||||
        try {
 | 
			
		||||
            await axios.post(`/upload/${cdmType}`, formData);
 | 
			
		||||
            await fetchUserInfo(); // Refresh list after upload
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            toast.error(`Upload failed. Reason: ${err.message}`);
 | 
			
		||||
            console.error("Upload failed", err);
 | 
			
		||||
        } finally {
 | 
			
		||||
            toast.success(`${cdmType} CDM uploaded successfully`);
 | 
			
		||||
            setUploading(false);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  // Handle change password
 | 
			
		||||
  const handleChangePassword = async () => {
 | 
			
		||||
    if (passwordError || password === '') {
 | 
			
		||||
      alert('Please enter a valid password.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // Handle logout
 | 
			
		||||
    const handleLogout = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            await axios.post("/logout");
 | 
			
		||||
            toast.success("Logged out successfully. Reloading page...");
 | 
			
		||||
            window.location.reload();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            toast.error(`Logout failed. Reason: ${error.message}`);
 | 
			
		||||
            console.error("Logout failed:", error);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.post('/user/change_password', {
 | 
			
		||||
        new_password: password
 | 
			
		||||
      });
 | 
			
		||||
    // Handle change password
 | 
			
		||||
    const handleChangePassword = async () => {
 | 
			
		||||
        if (passwordError || password === "") {
 | 
			
		||||
            toast.error("Please enter a valid password");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      if (response.data.message === 'True') {
 | 
			
		||||
        alert('Password changed successfully.');
 | 
			
		||||
        setPassword('');
 | 
			
		||||
      } else {
 | 
			
		||||
        alert('Failed to change password.');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (error.response && error.response.data?.message === 'Invalid password format') {
 | 
			
		||||
        alert('Password format is invalid. Please try again.');
 | 
			
		||||
      } else {
 | 
			
		||||
        alert('Error occurred while changing password.');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await axios.post("/user/change_password", {
 | 
			
		||||
                new_password: password,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
  // Handle change API key
 | 
			
		||||
  const handleChangeApiKey = async () => {
 | 
			
		||||
    if (apiKeyError || newApiKey === '') {
 | 
			
		||||
      alert('Please enter a valid API key.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
            if (response.data.message === "True") {
 | 
			
		||||
                toast.success("Password changed successfully");
 | 
			
		||||
                setPassword("");
 | 
			
		||||
            } else {
 | 
			
		||||
                toast.error("Failed to change password");
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (error.response && error.response.data?.message === "Invalid password format") {
 | 
			
		||||
                toast.error("Password format is invalid. Please try again.");
 | 
			
		||||
            } else {
 | 
			
		||||
                toast.error("Error occurred while changing password");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.post('/user/change_api_key', {
 | 
			
		||||
        new_api_key: newApiKey,
 | 
			
		||||
      });
 | 
			
		||||
      if (response.data.message === 'True') {
 | 
			
		||||
        alert('API key changed successfully.');
 | 
			
		||||
        setApiKey(newApiKey);
 | 
			
		||||
        setNewApiKey('');
 | 
			
		||||
      } else {
 | 
			
		||||
        alert('Failed to change API key.');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      alert('Error occurred while changing API key.');
 | 
			
		||||
      console.error(error);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
    // Handle change API key
 | 
			
		||||
    const handleChangeApiKey = async () => {
 | 
			
		||||
        if (apiKeyError || newApiKey === "") {
 | 
			
		||||
            toast.error("Please enter a valid API key");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div id="myaccount" className="flex flex-col lg:flex-row gap-4 w-full min-h-full overflow-y-auto p-4">
 | 
			
		||||
      <div className="flex-col w-full min-h-164 lg:h-full lg:w-96 border-2 border-yellow-500/50 rounded-2xl p-4 flex items-center overflow-y-auto">
 | 
			
		||||
        <h1 className="text-2xl font-bold text-white border-b-2 border-white p-2 w-full text-center mb-2">
 | 
			
		||||
          {username ? `${username}` : 'My Account'}
 | 
			
		||||
        </h1>
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await axios.post("/user/change_api_key", {
 | 
			
		||||
                new_api_key: newApiKey,
 | 
			
		||||
            });
 | 
			
		||||
            if (response.data.message === "True") {
 | 
			
		||||
                toast.success("API key changed successfully");
 | 
			
		||||
                setApiKey(newApiKey);
 | 
			
		||||
                setNewApiKey("");
 | 
			
		||||
            } else {
 | 
			
		||||
                toast.error("Failed to change API key");
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            toast.error("Error occurred while changing API key");
 | 
			
		||||
            console.error(error);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
        {/* API Key Section */}
 | 
			
		||||
        <div className="w-full flex flex-col items-center">
 | 
			
		||||
          <label htmlFor="apiKey" className="text-white font-semibold mb-1">API Key</label>
 | 
			
		||||
          <input
 | 
			
		||||
            id="apiKey"
 | 
			
		||||
            type="text"
 | 
			
		||||
            value={apiKey}
 | 
			
		||||
            readOnly
 | 
			
		||||
            className="w-full p-2 mb-4 rounded bg-gray-800 text-white border border-gray-600 text-center"
 | 
			
		||||
          />
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <Container>
 | 
			
		||||
                <div className="flex flex-col gap-4 p-4 lg:flex-row">
 | 
			
		||||
                    {/* Left Panel - Account Settings */}
 | 
			
		||||
                    <div className="w-full lg:w-96">
 | 
			
		||||
                        <div className="card bg-base-200 shadow-xl">
 | 
			
		||||
                            <div className="card-body">
 | 
			
		||||
                                <p className="text-center text-sm">Username:</p>
 | 
			
		||||
                                <h2 className="card-title justify-center text-center font-bold">
 | 
			
		||||
                                    {username}
 | 
			
		||||
                                </h2>
 | 
			
		||||
 | 
			
		||||
          {/* 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>
 | 
			
		||||
                                <div className="divider"></div>
 | 
			
		||||
 | 
			
		||||
          {/* 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>
 | 
			
		||||
                                <fieldset className="fieldset">
 | 
			
		||||
                                    <legend className="fieldset-legend text-base" htmlFor="apiKey">
 | 
			
		||||
                                        API Key
 | 
			
		||||
                                    </legend>
 | 
			
		||||
                                    <input
 | 
			
		||||
                                        name="apiKey"
 | 
			
		||||
                                        type="text"
 | 
			
		||||
                                        value={apiKey}
 | 
			
		||||
                                        readOnly
 | 
			
		||||
                                        className="input input-bordered text-center"
 | 
			
		||||
                                    />
 | 
			
		||||
 | 
			
		||||
        <button
 | 
			
		||||
          onClick={handleLogout}
 | 
			
		||||
          className="mt-auto w-full h-12 bg-yellow-500/50 rounded-2xl text-2xl text-white"
 | 
			
		||||
        >
 | 
			
		||||
          Log out
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
                                    <legend
 | 
			
		||||
                                        className="fieldset-legend text-base"
 | 
			
		||||
                                        htmlFor="newApiKey"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        New API Key
 | 
			
		||||
                                    </legend>
 | 
			
		||||
                                    <input
 | 
			
		||||
                                        name="newApiKey"
 | 
			
		||||
                                        type="text"
 | 
			
		||||
                                        value={newApiKey}
 | 
			
		||||
                                        onChange={(e) => {
 | 
			
		||||
                                            const value = e.target.value;
 | 
			
		||||
                                            const isValid = /^[^\s]+$/.test(value);
 | 
			
		||||
                                            if (!isValid) {
 | 
			
		||||
                                                setApiKeyError("API key must not contain spaces");
 | 
			
		||||
                                            } else {
 | 
			
		||||
                                                setApiKeyError("");
 | 
			
		||||
                                            }
 | 
			
		||||
                                            setNewApiKey(value);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        placeholder="Enter new API key"
 | 
			
		||||
                                        className="input input-bordered"
 | 
			
		||||
                                    />
 | 
			
		||||
                                    {apiKeyError && (
 | 
			
		||||
                                        <p className="label text-error">{apiKeyError}</p>
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        className="btn btn-primary btn-block mt-2"
 | 
			
		||||
                                        onClick={handleChangeApiKey}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        Change API key
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </fieldset>
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-col w-full lg:ml-2 mt-2 lg:mt-0">
 | 
			
		||||
        {/* Widevine Section */}
 | 
			
		||||
        <div className="border-2 border-yellow-500/50 flex flex-col w-full min-h-1/2 text-center rounded-2xl lg:p-4 p-2 overflow-y-auto">
 | 
			
		||||
          <h1 className="bg-black text-2xl font-bold text-white border-b-2 border-white p-2">Widevine CDMs</h1>
 | 
			
		||||
          <div className="flex flex-col w-full grow p-2 bg-white/5 rounded-2xl mt-2 text-white text-left">
 | 
			
		||||
            {wvList.length === 0 ? (
 | 
			
		||||
              <div className="text-white text-center font-bold">No Widevine CDMs uploaded.</div>
 | 
			
		||||
            ) : (
 | 
			
		||||
              wvList.map((filename, i) => (
 | 
			
		||||
                <div
 | 
			
		||||
                  key={i}
 | 
			
		||||
                  className={`text-center font-bold text-white p-2 rounded ${i % 2 === 0 ? 'bg-black/30' : 'bg-black/60'}`}
 | 
			
		||||
                >
 | 
			
		||||
                  {filename}
 | 
			
		||||
                                <fieldset className="fieldset">
 | 
			
		||||
                                    <legend
 | 
			
		||||
                                        className="fieldset-legend text-base"
 | 
			
		||||
                                        htmlFor="passwordChange"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        Change password
 | 
			
		||||
                                    </legend>
 | 
			
		||||
                                    <input
 | 
			
		||||
                                        name="passwordChange"
 | 
			
		||||
                                        type="password"
 | 
			
		||||
                                        value={password}
 | 
			
		||||
                                        onChange={(e) => {
 | 
			
		||||
                                            const value = e.target.value;
 | 
			
		||||
                                            const isValid =
 | 
			
		||||
                                                /^[A-Za-z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]*$/.test(
 | 
			
		||||
                                                    value
 | 
			
		||||
                                                );
 | 
			
		||||
                                            if (!isValid) {
 | 
			
		||||
                                                setPasswordError("Invalid password characters");
 | 
			
		||||
                                            } else {
 | 
			
		||||
                                                setPasswordError("");
 | 
			
		||||
                                            }
 | 
			
		||||
                                            setPassword(value);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        placeholder="New password"
 | 
			
		||||
                                        className="input input-bordered"
 | 
			
		||||
                                    />
 | 
			
		||||
                                    {passwordError && (
 | 
			
		||||
                                        <p className="label text-error">{passwordError}</p>
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        className="btn btn-secondary btn-block mt-2"
 | 
			
		||||
                                        onClick={handleChangePassword}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        Change password
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </fieldset>
 | 
			
		||||
 | 
			
		||||
                                <div className="divider"></div>
 | 
			
		||||
 | 
			
		||||
                                <button className="btn btn-error mt-auto" onClick={handleLogout}>
 | 
			
		||||
                                    Log out
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Right Panel - CDM Uploads */}
 | 
			
		||||
                    <div className="flex w-full flex-col gap-4">
 | 
			
		||||
                        {/* Widevine CDM */}
 | 
			
		||||
                        <div className="card bg-base-200 shadow-xl">
 | 
			
		||||
                            <div className="card-body">
 | 
			
		||||
                                <h2 className="card-title">Widevine CDMs</h2>
 | 
			
		||||
                                <div className="divider"></div>
 | 
			
		||||
                                <div className="max-h-60 space-y-2 overflow-y-auto">
 | 
			
		||||
                                    {wvList.length === 0 ? (
 | 
			
		||||
                                        <div className="text-center text-sm">
 | 
			
		||||
                                            No Widevine CDMs uploaded.
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    ) : (
 | 
			
		||||
                                        wvList.map((filename, i) => (
 | 
			
		||||
                                            <div
 | 
			
		||||
                                                key={i}
 | 
			
		||||
                                                className={`rounded px-2 py-1 text-sm ${
 | 
			
		||||
                                                    i % 2 === 0 ? "bg-base-100" : "bg-base-300"
 | 
			
		||||
                                                }`}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {filename}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        ))
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <label className="btn btn-accent mt-4">
 | 
			
		||||
                                    {uploading ? "Uploading..." : "Upload CDM"}
 | 
			
		||||
                                    <input
 | 
			
		||||
                                        type="file"
 | 
			
		||||
                                        accept=".wvd"
 | 
			
		||||
                                        hidden
 | 
			
		||||
                                        onChange={(e) => handleUpload(e, "WV")}
 | 
			
		||||
                                    />
 | 
			
		||||
                                </label>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        {/* PlayReady CDM */}
 | 
			
		||||
                        <div className="card bg-base-200 shadow-xl">
 | 
			
		||||
                            <div className="card-body">
 | 
			
		||||
                                <h2 className="card-title">PlayReady CDMs</h2>
 | 
			
		||||
                                <div className="divider"></div>
 | 
			
		||||
                                <div className="max-h-60 space-y-2 overflow-y-auto">
 | 
			
		||||
                                    {prList.length === 0 ? (
 | 
			
		||||
                                        <div className="text-center text-sm">
 | 
			
		||||
                                            No PlayReady CDMs uploaded.
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    ) : (
 | 
			
		||||
                                        prList.map((filename, i) => (
 | 
			
		||||
                                            <div
 | 
			
		||||
                                                key={i}
 | 
			
		||||
                                                className={`rounded px-2 py-1 text-sm ${
 | 
			
		||||
                                                    i % 2 === 0 ? "bg-base-100" : "bg-base-300"
 | 
			
		||||
                                                }`}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {filename}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        ))
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <label className="btn btn-accent mt-4">
 | 
			
		||||
                                    {uploading ? "Uploading..." : "Upload CDM"}
 | 
			
		||||
                                    <input
 | 
			
		||||
                                        type="file"
 | 
			
		||||
                                        accept=".prd"
 | 
			
		||||
                                        hidden
 | 
			
		||||
                                        onChange={(e) => handleUpload(e, "PR")}
 | 
			
		||||
                                    />
 | 
			
		||||
                                </label>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </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>
 | 
			
		||||
  );
 | 
			
		||||
            </Container>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MyAccount;
 | 
			
		||||
 | 
			
		||||
@ -1,117 +1,149 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import { IoIosLogIn } from "react-icons/io";
 | 
			
		||||
import { PiUserCirclePlus } from "react-icons/pi";
 | 
			
		||||
 | 
			
		||||
function Register() {
 | 
			
		||||
  const [username, setUsername] = useState('');
 | 
			
		||||
  const [password, setPassword] = useState('');
 | 
			
		||||
  const [status, setStatus] = useState('');
 | 
			
		||||
    const [username, setUsername] = useState("");
 | 
			
		||||
    const [password, setPassword] = useState("");
 | 
			
		||||
    const [confirmPassword, setConfirmPassword] = useState("");
 | 
			
		||||
    const [tab, setTab] = useState("login"); // 'login' or 'register'
 | 
			
		||||
 | 
			
		||||
  // Validation functions
 | 
			
		||||
  const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
 | 
			
		||||
  const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        document.title = "Register | CDRM-Project";
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
  const handleRegister = async () => {
 | 
			
		||||
    if (!validateUsername(username)) {
 | 
			
		||||
      setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!validatePassword(password)) {
 | 
			
		||||
      setStatus("Invalid password. Spaces are not allowed.");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const validateUsername = (name) => /^[A-Za-z0-9_-]+$/.test(name);
 | 
			
		||||
    const validatePassword = (pass) => /^\S+$/.test(pass); // No spaces
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await fetch('/register', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        },
 | 
			
		||||
        body: JSON.stringify({ username, password })
 | 
			
		||||
      });
 | 
			
		||||
      const data = await response.json();
 | 
			
		||||
      if (data.message) {
 | 
			
		||||
        setStatus(data.message);
 | 
			
		||||
      } else if (data.error) {
 | 
			
		||||
        setStatus(data.error);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      setStatus('An error occurred while registering.');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
    const handleSubmit = async (e) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
  const handleLogin = async () => {
 | 
			
		||||
    if (!validateUsername(username)) {
 | 
			
		||||
      setStatus("Invalid username. Use only letters, numbers, hyphens, or underscores.");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!validatePassword(password)) {
 | 
			
		||||
      setStatus("Invalid password. Spaces are not allowed.");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
        if (!validateUsername(username)) {
 | 
			
		||||
            toast.error("Invalid username. Use only letters, numbers, hyphens, or underscores.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!validatePassword(password)) {
 | 
			
		||||
            toast.error("Invalid password. Spaces are not allowed.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await fetch('/login', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        },
 | 
			
		||||
        credentials: 'include', // Important to send cookies
 | 
			
		||||
        body: JSON.stringify({ username, password })
 | 
			
		||||
      });
 | 
			
		||||
      const data = await response.json();
 | 
			
		||||
      if (data.message) {
 | 
			
		||||
        // Successful login - reload the page to trigger Account check
 | 
			
		||||
        window.location.reload();
 | 
			
		||||
      } else if (data.error) {
 | 
			
		||||
        setStatus(data.error);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      setStatus('An error occurred while logging in.');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
        if (tab === "register") {
 | 
			
		||||
            if (password !== confirmPassword) {
 | 
			
		||||
                toast.error("Passwords do not match.");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            try {
 | 
			
		||||
                const res = await fetch("/register", {
 | 
			
		||||
                    method: "POST",
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        "Content-Type": "application/json",
 | 
			
		||||
                    },
 | 
			
		||||
                    body: JSON.stringify({ username, password }),
 | 
			
		||||
                });
 | 
			
		||||
                const data = await res.json();
 | 
			
		||||
                if (data.message) {
 | 
			
		||||
                    toast.success(data.message);
 | 
			
		||||
                } else {
 | 
			
		||||
                    toast.error(data.error || "Unknown error");
 | 
			
		||||
                }
 | 
			
		||||
            } catch (err) {
 | 
			
		||||
                toast.error(`Register error: ${err.message}`);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            try {
 | 
			
		||||
                const res = await fetch("/login", {
 | 
			
		||||
                    method: "POST",
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        "Content-Type": "application/json",
 | 
			
		||||
                    },
 | 
			
		||||
                    credentials: "include",
 | 
			
		||||
                    body: JSON.stringify({ username, password }),
 | 
			
		||||
                });
 | 
			
		||||
                const data = await res.json();
 | 
			
		||||
                if (data.message) {
 | 
			
		||||
                    window.location.reload();
 | 
			
		||||
                } else {
 | 
			
		||||
                    toast.error(data.error || "Login failed");
 | 
			
		||||
                }
 | 
			
		||||
            } catch (err) {
 | 
			
		||||
                toast.error(`Login error: ${err.message}`);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  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"
 | 
			
		||||
          />
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="mx-auto flex min-h-full w-full max-w-xl flex-col justify-center px-6 py-12 lg:px-8">
 | 
			
		||||
            <div className="mx-auto">
 | 
			
		||||
                {/* Tabs */}
 | 
			
		||||
                <div className="tabs tabs-box justify-center">
 | 
			
		||||
                    <button
 | 
			
		||||
                        className={`tab ${tab === "login" ? "tab-active" : ""}`}
 | 
			
		||||
                        onClick={() => setTab("login")}
 | 
			
		||||
                    >
 | 
			
		||||
                        <IoIosLogIn className="h-6 w-6 me-1"/>
 | 
			
		||||
                        Sign in
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <button
 | 
			
		||||
                        className={`tab ${tab === "register" ? "tab-active" : ""}`}
 | 
			
		||||
                        onClick={() => setTab("register")}
 | 
			
		||||
                    >
 | 
			
		||||
                        <PiUserCirclePlus className="h-6 w-6 me-1" />
 | 
			
		||||
                        Register
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <h2 className="mt-10 text-center text-2xl font-bold tracking-tight">
 | 
			
		||||
                    {tab === "login" ? "Sign in" : "Register"}
 | 
			
		||||
                </h2>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className="mx-auto mt-10 w-full max-w-xl">
 | 
			
		||||
                <form className="space-y-6" onSubmit={handleSubmit}>
 | 
			
		||||
                    <fieldset className="fieldset">
 | 
			
		||||
                        <legend className="fieldset-legend text-base">Username</legend>
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            value={username}
 | 
			
		||||
                            onChange={(e) => setUsername(e.target.value)}
 | 
			
		||||
                            className="input w-full"
 | 
			
		||||
                            placeholder="Enter your username"
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
 | 
			
		||||
                    <fieldset className="fieldset">
 | 
			
		||||
                        <legend className="fieldset-legend text-base">Password</legend>
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="password"
 | 
			
		||||
                            value={password}
 | 
			
		||||
                            onChange={(e) => setPassword(e.target.value)}
 | 
			
		||||
                            className="input w-full"
 | 
			
		||||
                            placeholder="Enter your password"
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                    </fieldset>
 | 
			
		||||
 | 
			
		||||
                    {tab === "register" && (
 | 
			
		||||
                        <fieldset className="fieldset">
 | 
			
		||||
                            <legend className="fieldset-legend text-base">Confirm password</legend>
 | 
			
		||||
                            <input
 | 
			
		||||
                                type="password"
 | 
			
		||||
                                value={confirmPassword}
 | 
			
		||||
                                onChange={(e) => setConfirmPassword(e.target.value)}
 | 
			
		||||
                                className="input w-full"
 | 
			
		||||
                                placeholder="Confirm your password"
 | 
			
		||||
                                required
 | 
			
		||||
                            />
 | 
			
		||||
                        </fieldset>
 | 
			
		||||
                    )}
 | 
			
		||||
 | 
			
		||||
                    <button type="submit" className="btn btn-primary btn-block">
 | 
			
		||||
                        {tab === "login" ? "Sign in" : "Register"}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </form>
 | 
			
		||||
            </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;
 | 
			
		||||
 | 
			
		||||
@ -1,158 +1,178 @@
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react';
 | 
			
		||||
import shaka from 'shaka-player';
 | 
			
		||||
import { Helmet } from 'react-helmet'; // Import Helmet
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import shaka from "shaka-player";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import Container from "../Container";
 | 
			
		||||
import NavBar from "../NavBar";
 | 
			
		||||
 | 
			
		||||
function TestPlayer() {
 | 
			
		||||
  const [mpdUrl, setMpdUrl] = useState(''); // State to hold the MPD URL
 | 
			
		||||
  const [kids, setKids] = useState(''); // State to hold KIDs (separated by line breaks)
 | 
			
		||||
  const [keys, setKeys] = useState(''); // State to hold Keys (separated by line breaks)
 | 
			
		||||
  const [headers, setHeaders] = useState(''); // State to hold request headers
 | 
			
		||||
    const [mpdUrl, setMpdUrl] = useState("");
 | 
			
		||||
    const [headers, setHeaders] = useState("");
 | 
			
		||||
    const [keyPairs, setKeyPairs] = useState(""); // "kid:key" per line
 | 
			
		||||
 | 
			
		||||
  const videoRef = useRef(null); // Ref for the video element
 | 
			
		||||
  const playerRef = useRef(null); // Ref for Shaka Player instance
 | 
			
		||||
    const videoRef = useRef(null);
 | 
			
		||||
    const playerRef = useRef(null);
 | 
			
		||||
 | 
			
		||||
  // Function to update the MPD URL state
 | 
			
		||||
  const handleInputChange = (event) => {
 | 
			
		||||
    setMpdUrl(event.target.value);
 | 
			
		||||
  };
 | 
			
		||||
    const handleInputChange = (event) => {
 | 
			
		||||
        setMpdUrl(event.target.value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  // Function to update KIDs and Keys
 | 
			
		||||
  const handleKidsChange = (event) => {
 | 
			
		||||
    setKids(event.target.value);
 | 
			
		||||
  };
 | 
			
		||||
    const handleKeyPairsChange = (event) => {
 | 
			
		||||
        setKeyPairs(event.target.value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  const handleKeysChange = (event) => {
 | 
			
		||||
    setKeys(event.target.value);
 | 
			
		||||
  };
 | 
			
		||||
    const handleHeadersChange = (event) => {
 | 
			
		||||
        setHeaders(event.target.value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  const handleHeadersChange = (event) => {
 | 
			
		||||
    setHeaders(event.target.value);
 | 
			
		||||
  };
 | 
			
		||||
    // Function to initialize Shaka Player
 | 
			
		||||
    const initializePlayer = async () => {
 | 
			
		||||
        if (videoRef.current && !playerRef.current) {
 | 
			
		||||
            const player = new shaka.Player(); // no mediaElement
 | 
			
		||||
            await player.attach(videoRef.current); // attach later
 | 
			
		||||
            playerRef.current = player;
 | 
			
		||||
 | 
			
		||||
  // Function to initialize Shaka Player
 | 
			
		||||
  const initializePlayer = () => {
 | 
			
		||||
    if (videoRef.current) {
 | 
			
		||||
      // Initialize Shaka Player only if it's not already initialized
 | 
			
		||||
      if (!playerRef.current) {
 | 
			
		||||
        const player = new shaka.Player(videoRef.current);
 | 
			
		||||
        playerRef.current = player;
 | 
			
		||||
            player.addEventListener("error", (event) => {
 | 
			
		||||
                toast.error(`Error code ${event.detail.code}: ${event.detail.message}`);
 | 
			
		||||
                console.error("Error code", event.detail.code, "object", event.detail);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
        // Add error listener
 | 
			
		||||
        player.addEventListener('error', (event) => {
 | 
			
		||||
          console.error('Error code', event.detail.code, 'object', event.detail);
 | 
			
		||||
    // Function to handle submit and configure player with DRM keys and headers
 | 
			
		||||
    const handleSubmit = () => {
 | 
			
		||||
        if (mpdUrl && keyPairs) {
 | 
			
		||||
            // Parse KID:KEY pairs
 | 
			
		||||
            const lines = keyPairs
 | 
			
		||||
                .split("\n")
 | 
			
		||||
                .map((line) => line.trim())
 | 
			
		||||
                .filter(Boolean);
 | 
			
		||||
            const clearKeys = {};
 | 
			
		||||
 | 
			
		||||
            for (const line of lines) {
 | 
			
		||||
                const [kid, key] = line.split(":").map((part) => part.trim());
 | 
			
		||||
                if (!kid || !key) {
 | 
			
		||||
                    toast.error(`Invalid line (expected format keyId:key) at line "${line}"`);
 | 
			
		||||
                    console.error(`Invalid line (expected format keyId:key) at line "${line}"`);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                clearKeys[kid] = key;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 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: clearKeys,
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            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");
 | 
			
		||||
                    toast.success("Video successfully loaded");
 | 
			
		||||
                })
 | 
			
		||||
                .catch((error) => {
 | 
			
		||||
                    toast.error(`Error loading the video. Reason: ${error.message}`);
 | 
			
		||||
                    console.error("Error loading the video", error);
 | 
			
		||||
                });
 | 
			
		||||
        } else {
 | 
			
		||||
            toast.error("Manifest URL and key pairs are required");
 | 
			
		||||
            console.error("Manifest URL and key pairs 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
 | 
			
		||||
  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());
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        document.title = "Test player | CDRM-Project";
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
      if (kidsArray.length !== keysArray.length) {
 | 
			
		||||
        console.error("The number of KIDs and Keys must be the same.");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <NavBar />
 | 
			
		||||
            <Container>
 | 
			
		||||
                <div className="flex w-full flex-col items-center justify-center py-8">
 | 
			
		||||
                    <div className="flex w-full flex-col items-center lg:flex-row lg:items-start lg:gap-4">
 | 
			
		||||
                        {/* Video Section */}
 | 
			
		||||
                        <div className="w-full lg:w-1/2">
 | 
			
		||||
                            <video
 | 
			
		||||
                                ref={videoRef}
 | 
			
		||||
                                width="100%"
 | 
			
		||||
                                height="auto"
 | 
			
		||||
                                controls
 | 
			
		||||
                                className="aspect-video max-h-96 w-full"
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
      // Initialize Shaka Player only when the submit button is pressed
 | 
			
		||||
      const player = new shaka.Player(videoRef.current);
 | 
			
		||||
 | 
			
		||||
      // Widevine DRM configuration with the provided KIDs and Keys
 | 
			
		||||
      const config = {
 | 
			
		||||
        drm: {
 | 
			
		||||
          clearKeys: {},
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      // Map KIDs to Keys
 | 
			
		||||
      kidsArray.forEach((kid, index) => {
 | 
			
		||||
        config.drm.clearKeys[kid] = keysArray[index];
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log("Configuring player with the following DRM config and headers:", config);
 | 
			
		||||
 | 
			
		||||
      // Configure the player with ClearKey DRM and custom headers
 | 
			
		||||
      player.configure(config);
 | 
			
		||||
 | 
			
		||||
      // Load the video stream with MPD URL
 | 
			
		||||
      player.load(mpdUrl).then(() => {
 | 
			
		||||
        console.log('Video loaded');
 | 
			
		||||
      }).catch((error) => {
 | 
			
		||||
        console.error('Error loading the video', error);
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error('MPD URL, KIDs, and Keys are required.');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Load the video stream whenever the MPD URL changes
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    initializePlayer(); // Initialize the player if it's not initialized already
 | 
			
		||||
  }, []); // This effect runs only once on mount
 | 
			
		||||
 | 
			
		||||
  // Helper function to parse headers from the textarea input
 | 
			
		||||
  const parseHeaders = (headersText) => {
 | 
			
		||||
    const headersArr = headersText.split('\n');
 | 
			
		||||
    const headersObj = {};
 | 
			
		||||
    headersArr.forEach((line) => {
 | 
			
		||||
      const [key, value] = line.split(':');
 | 
			
		||||
      if (key && value) {
 | 
			
		||||
        headersObj[key.trim()] = value.trim();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return headersObj;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col items-center w-full p-4">
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>Test Player</title>
 | 
			
		||||
      </Helmet>
 | 
			
		||||
      <div className="w-full flex flex-col">
 | 
			
		||||
        <video
 | 
			
		||||
          ref={videoRef}
 | 
			
		||||
          width="100%"
 | 
			
		||||
          height="auto"
 | 
			
		||||
          controls
 | 
			
		||||
          className="h-96"
 | 
			
		||||
        />
 | 
			
		||||
        <input
 | 
			
		||||
          type="text"
 | 
			
		||||
          value={mpdUrl}
 | 
			
		||||
          onChange={handleInputChange}
 | 
			
		||||
          placeholder="MPD URL"
 | 
			
		||||
          className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
 | 
			
		||||
        />
 | 
			
		||||
        <textarea
 | 
			
		||||
          placeholder="KIDs (one per line)"
 | 
			
		||||
          value={kids}
 | 
			
		||||
          onChange={handleKidsChange}
 | 
			
		||||
          className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
 | 
			
		||||
        />
 | 
			
		||||
        <textarea
 | 
			
		||||
          placeholder="Keys (one per line)"
 | 
			
		||||
          value={keys}
 | 
			
		||||
          onChange={handleKeysChange}
 | 
			
		||||
          className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
 | 
			
		||||
        />
 | 
			
		||||
        <textarea
 | 
			
		||||
          placeholder="Headers (one per line)"
 | 
			
		||||
          value={headers}
 | 
			
		||||
          onChange={handleHeadersChange}
 | 
			
		||||
          className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
 | 
			
		||||
        />
 | 
			
		||||
        <button
 | 
			
		||||
          onClick={handleSubmit}
 | 
			
		||||
          className="mt-4 p-2 bg-blue-500 text-white rounded"
 | 
			
		||||
        >
 | 
			
		||||
          Submit
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
                        {/* Inputs Section */}
 | 
			
		||||
                        <div className="mt-4 flex w-full flex-col items-center lg:mt-0 lg:w-1/2">
 | 
			
		||||
                            <fieldset className="fieldset w-full">
 | 
			
		||||
                                <legend className="fieldset-legend text-base">Manifest URL*</legend>
 | 
			
		||||
                                <input
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    value={mpdUrl}
 | 
			
		||||
                                    onChange={handleInputChange}
 | 
			
		||||
                                    placeholder="Enter manifest URL here"
 | 
			
		||||
                                    className="input w-full font-mono"
 | 
			
		||||
                                />
 | 
			
		||||
                                <p className="label text-red-500">* Required</p>
 | 
			
		||||
                            </fieldset>
 | 
			
		||||
                            <fieldset className="fieldset w-full">
 | 
			
		||||
                                <legend className="fieldset-legend text-base">Key pairs*</legend>
 | 
			
		||||
                                <textarea
 | 
			
		||||
                                    placeholder="keyId:key pair (one per line)"
 | 
			
		||||
                                    value={keyPairs}
 | 
			
		||||
                                    onChange={handleKeyPairsChange}
 | 
			
		||||
                                    className="textarea w-full font-mono"
 | 
			
		||||
                                />
 | 
			
		||||
                                <p className="label text-red-500">* Required</p>
 | 
			
		||||
                            </fieldset>
 | 
			
		||||
                            <fieldset className="fieldset w-full">
 | 
			
		||||
                                <legend className="fieldset-legend text-base">Headers</legend>
 | 
			
		||||
                                <textarea
 | 
			
		||||
                                    placeholder="Headers (one per line)"
 | 
			
		||||
                                    value={headers}
 | 
			
		||||
                                    onChange={handleHeadersChange}
 | 
			
		||||
                                    className="textarea w-full font-mono"
 | 
			
		||||
                                />
 | 
			
		||||
                            </fieldset>
 | 
			
		||||
                            <button
 | 
			
		||||
                                onClick={handleSubmit}
 | 
			
		||||
                                className="btn btn-primary btn-wide my-4"
 | 
			
		||||
                            >
 | 
			
		||||
                                Submit
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </Container>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default TestPlayer;
 | 
			
		||||
 | 
			
		||||
@ -1,178 +1,182 @@
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { NavLink } from 'react-router-dom';
 | 
			
		||||
import closeIcon from '../assets/icons/close.svg';
 | 
			
		||||
import homeIcon from '../assets/icons/home.svg';
 | 
			
		||||
import cacheIcon from '../assets/icons/cache.svg';
 | 
			
		||||
import apiIcon from '../assets/icons/api.svg';
 | 
			
		||||
import testPlayerIcon from '../assets/icons/testplayer.svg'; 
 | 
			
		||||
import accountIcon from '../assets/icons/account.svg'; 
 | 
			
		||||
import discordIcon from '../assets/icons/discord.svg';
 | 
			
		||||
import telegramIcon from '../assets/icons/telegram.svg';
 | 
			
		||||
import giteaIcon from '../assets/icons/gitea.svg';
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { NavLink } from "react-router-dom";
 | 
			
		||||
import closeIcon from "../assets/icons/close.svg";
 | 
			
		||||
import homeIcon from "../assets/icons/home.svg";
 | 
			
		||||
import cacheIcon from "../assets/icons/cache.svg";
 | 
			
		||||
import apiIcon from "../assets/icons/api.svg";
 | 
			
		||||
import testPlayerIcon from "../assets/icons/testplayer.svg";
 | 
			
		||||
import accountIcon from "../assets/icons/account.svg";
 | 
			
		||||
import discordIcon from "../assets/icons/discord.svg";
 | 
			
		||||
import telegramIcon from "../assets/icons/telegram.svg";
 | 
			
		||||
import giteaIcon from "../assets/icons/gitea.svg";
 | 
			
		||||
 | 
			
		||||
function SideMenu({ isMenuOpen, setIsMenuOpen }) {
 | 
			
		||||
  const [externalLinks, setExternalLinks] = useState({
 | 
			
		||||
    discord: '#',
 | 
			
		||||
    telegram: '#',
 | 
			
		||||
    gitea: '#',
 | 
			
		||||
  });
 | 
			
		||||
    const [externalLinks, setExternalLinks] = useState({
 | 
			
		||||
        discord: "#",
 | 
			
		||||
        telegram: "#",
 | 
			
		||||
        gitea: "#",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetch('/api/links')
 | 
			
		||||
      .then((res) => res.json())
 | 
			
		||||
      .then((data) => setExternalLinks(data))
 | 
			
		||||
      .catch((err) => console.error('Failed to fetch links:', err));
 | 
			
		||||
  }, []);
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        fetch("/api/links")
 | 
			
		||||
            .then((res) => res.json())
 | 
			
		||||
            .then((data) => setExternalLinks(data))
 | 
			
		||||
            .catch((err) => console.error("Failed to fetch links:", err));
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${
 | 
			
		||||
        isMenuOpen ? 'translate-x-0' : '-translate-x-full'
 | 
			
		||||
      } z-50`}
 | 
			
		||||
      style={{ transitionDuration: '0.3s' }}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="flex flex-col bg-gray-950/55 h-full">
 | 
			
		||||
        {/* Header */}
 | 
			
		||||
        <div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
 | 
			
		||||
          <div className="w-1/4 h-full"></div>
 | 
			
		||||
          <p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
 | 
			
		||||
            CDRM-Project
 | 
			
		||||
          </p>
 | 
			
		||||
          <div className="w-1/4 h-full">
 | 
			
		||||
            <button
 | 
			
		||||
              className="w-full h-full flex items-center justify-center"
 | 
			
		||||
              onClick={() => setIsMenuOpen(false)}
 | 
			
		||||
            >
 | 
			
		||||
              <img src={closeIcon} alt="Close" className="w-1/2 h-1/2 cursor-pointer" />
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
    return (
 | 
			
		||||
        <div
 | 
			
		||||
            className={`flex flex-col fixed top-0 left-0 w-full h-full bg-black transition-transform transform ${
 | 
			
		||||
                isMenuOpen ? "translate-x-0" : "-translate-x-full"
 | 
			
		||||
            } z-50`}
 | 
			
		||||
            style={{ transitionDuration: "0.3s" }}
 | 
			
		||||
        >
 | 
			
		||||
            <div className="flex flex-col bg-gray-950/55 h-full">
 | 
			
		||||
                {/* Header */}
 | 
			
		||||
                <div className="h-16 w-full border-b-2 border-white/5 flex flex-row">
 | 
			
		||||
                    <div className="w-1/4 h-full"></div>
 | 
			
		||||
                    <p className="grow text-white md:text-2xl font-bold text-center flex items-center justify-center p-4">
 | 
			
		||||
                        CDRM-Project
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <div className="w-1/4 h-full">
 | 
			
		||||
                        <button
 | 
			
		||||
                            className="w-full h-full flex items-center justify-center"
 | 
			
		||||
                            onClick={() => setIsMenuOpen(false)}
 | 
			
		||||
                        >
 | 
			
		||||
                            <img
 | 
			
		||||
                                src={closeIcon}
 | 
			
		||||
                                alt="Close"
 | 
			
		||||
                                className="w-1/2 h-1/2 cursor-pointer"
 | 
			
		||||
                            />
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {/* Scrollable Navigation Links */}
 | 
			
		||||
                <div className="overflow-y-auto flex flex-col p-5 w-full flex-grow">
 | 
			
		||||
                    <div className="flex flex-col space-y-2">
 | 
			
		||||
                        <NavLink
 | 
			
		||||
                            to="/"
 | 
			
		||||
                            className={({ isActive }) =>
 | 
			
		||||
                                `flex flex-row items-center gap-3 p-3 border-l-4 ${
 | 
			
		||||
                                    isActive
 | 
			
		||||
                                        ? "border-l-sky-500/50 bg-black/50 text-white"
 | 
			
		||||
                                        : "border-transparent hover:border-l-sky-500/50 hover:bg-white/5 text-white/80"
 | 
			
		||||
                                }`
 | 
			
		||||
                            }
 | 
			
		||||
                            onClick={() => setIsMenuOpen(false)}
 | 
			
		||||
                        >
 | 
			
		||||
                            <img src={homeIcon} alt="Home" className="w-5 h-5" />
 | 
			
		||||
                            <span className="text-lg">Home</span>
 | 
			
		||||
                        </NavLink>
 | 
			
		||||
 | 
			
		||||
                        <NavLink
 | 
			
		||||
                            to="/cache"
 | 
			
		||||
                            className={({ isActive }) =>
 | 
			
		||||
                                `flex flex-row items-center gap-3 p-3 border-l-4 ${
 | 
			
		||||
                                    isActive
 | 
			
		||||
                                        ? "border-l-emerald-500/50 bg-black/50 text-white"
 | 
			
		||||
                                        : "border-transparent hover:border-l-emerald-500/50 hover:bg-white/5 text-white/80"
 | 
			
		||||
                                }`
 | 
			
		||||
                            }
 | 
			
		||||
                            onClick={() => setIsMenuOpen(false)}
 | 
			
		||||
                        >
 | 
			
		||||
                            <img src={cacheIcon} alt="Cache" className="w-5 h-5" />
 | 
			
		||||
                            <span className="text-lg">Cache</span>
 | 
			
		||||
                        </NavLink>
 | 
			
		||||
 | 
			
		||||
                        <NavLink
 | 
			
		||||
                            to="/api"
 | 
			
		||||
                            className={({ isActive }) =>
 | 
			
		||||
                                `flex flex-row items-center gap-3 p-3 border-l-4 ${
 | 
			
		||||
                                    isActive
 | 
			
		||||
                                        ? "border-l-indigo-500/50 bg-black/50 text-white"
 | 
			
		||||
                                        : "border-transparent hover:border-l-indigo-500/50 hover:bg-white/5 text-white/80"
 | 
			
		||||
                                }`
 | 
			
		||||
                            }
 | 
			
		||||
                            onClick={() => setIsMenuOpen(false)}
 | 
			
		||||
                        >
 | 
			
		||||
                            <img src={apiIcon} alt="API" className="w-5 h-5" />
 | 
			
		||||
                            <span className="text-lg">API</span>
 | 
			
		||||
                        </NavLink>
 | 
			
		||||
 | 
			
		||||
                        <NavLink
 | 
			
		||||
                            to="/testplayer"
 | 
			
		||||
                            className={({ isActive }) =>
 | 
			
		||||
                                `flex flex-row items-center gap-3 p-3 border-l-4 ${
 | 
			
		||||
                                    isActive
 | 
			
		||||
                                        ? "border-l-rose-700/50 bg-black/50 text-white"
 | 
			
		||||
                                        : "border-transparent hover:border-l-rose-700/50 hover:bg-white/5 text-white/80"
 | 
			
		||||
                                }`
 | 
			
		||||
                            }
 | 
			
		||||
                            onClick={() => setIsMenuOpen(false)}
 | 
			
		||||
                        >
 | 
			
		||||
                            <img src={testPlayerIcon} alt="Test Player" className="w-5 h-5" />
 | 
			
		||||
                            <span className="text-lg">Test Player</span>
 | 
			
		||||
                        </NavLink>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* My Account Link at the Bottom of Scrollable Area */}
 | 
			
		||||
                    <div className="mt-auto pt-4">
 | 
			
		||||
                        <NavLink
 | 
			
		||||
                            to="/account"
 | 
			
		||||
                            className={({ isActive }) =>
 | 
			
		||||
                                `flex flex-row items-center gap-3 p-3 border-l-4 ${
 | 
			
		||||
                                    isActive
 | 
			
		||||
                                        ? "border-l-yellow-500/50 bg-black/50 text-white"
 | 
			
		||||
                                        : "border-transparent hover:border-l-yellow-500/50 hover:bg-white/5 text-white/80"
 | 
			
		||||
                                }`
 | 
			
		||||
                            }
 | 
			
		||||
                            onClick={() => setIsMenuOpen(false)}
 | 
			
		||||
                        >
 | 
			
		||||
                            <img src={accountIcon} alt="My Account" className="w-5 h-5" />
 | 
			
		||||
                            <span className="text-lg">My Account</span>
 | 
			
		||||
                        </NavLink>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {/* External Links */}
 | 
			
		||||
                <div className="h-16 w-full flex flex-row bg-black/5">
 | 
			
		||||
                    <a
 | 
			
		||||
                        href={externalLinks.discord}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        rel="noopener noreferrer"
 | 
			
		||||
                        className="w-1/3 h-full flex items-center justify-center hover:bg-blue-950 group"
 | 
			
		||||
                    >
 | 
			
		||||
                        <img
 | 
			
		||||
                            src={discordIcon}
 | 
			
		||||
                            alt="Discord"
 | 
			
		||||
                            className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
 | 
			
		||||
                        />
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a
 | 
			
		||||
                        href={externalLinks.telegram}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        rel="noopener noreferrer"
 | 
			
		||||
                        className="w-1/3 h-full flex items-center justify-center hover:bg-blue-400 group"
 | 
			
		||||
                    >
 | 
			
		||||
                        <img
 | 
			
		||||
                            src={telegramIcon}
 | 
			
		||||
                            alt="Telegram"
 | 
			
		||||
                            className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
 | 
			
		||||
                        />
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a
 | 
			
		||||
                        href={externalLinks.gitea}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        rel="noopener noreferrer"
 | 
			
		||||
                        className="w-1/3 h-full flex items-center justify-center hover:bg-green-700 group"
 | 
			
		||||
                    >
 | 
			
		||||
                        <img
 | 
			
		||||
                            src={giteaIcon}
 | 
			
		||||
                            alt="Gitea"
 | 
			
		||||
                            className="w-full h-2/3 p-1 cursor-pointer group-hover:animate-bounce"
 | 
			
		||||
                        />
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </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;
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,37 @@
 | 
			
		||||
@import "tailwindcss";
 | 
			
		||||
@plugin "daisyui";
 | 
			
		||||
 | 
			
		||||
@plugin "daisyui/theme" {
 | 
			
		||||
    name: "dim";
 | 
			
		||||
    default: true;
 | 
			
		||||
    --radius-selector: 0.5rem;
 | 
			
		||||
    --radius-field: 0.5rem;
 | 
			
		||||
    --radius-box: 0.5rem;
 | 
			
		||||
    --size-selector: 0.25rem;
 | 
			
		||||
    --size-field: 0.25rem;
 | 
			
		||||
    --depth: 0;
 | 
			
		||||
    --noise: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    --font-sans:
 | 
			
		||||
        "Inter", system-ui, -apple-system, Roboto, "Segoe UI", "Helvetica Neue", "Noto Sans",
 | 
			
		||||
        Oxygen, Ubuntu, Cantarell, "Open Sans", Arial, sans-serif;
 | 
			
		||||
    font-family: var(--font-sans);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Force Sonner toast to use Inter first */
 | 
			
		||||
[data-sonner-toast],
 | 
			
		||||
.sonner-toast,
 | 
			
		||||
:where([data-sonner-toast]) :where([data-title]) :where([data-description]) {
 | 
			
		||||
    font-family: var(--font-sans) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
details summary::-webkit-details-marker {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  details summary {
 | 
			
		||||
    list-style: none; 
 | 
			
		||||
    cursor: pointer; 
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
details summary {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,22 @@
 | 
			
		||||
import { StrictMode } from 'react'
 | 
			
		||||
import { createRoot } from 'react-dom/client'
 | 
			
		||||
import { BrowserRouter } from 'react-router-dom'
 | 
			
		||||
import './index.css'
 | 
			
		||||
import App from './App.jsx'
 | 
			
		||||
import { StrictMode } from "react";
 | 
			
		||||
import { createRoot } from "react-dom/client";
 | 
			
		||||
import { BrowserRouter } from "react-router-dom";
 | 
			
		||||
import { Toaster } from "sonner";
 | 
			
		||||
import App from "./App.jsx";
 | 
			
		||||
import "./assets/fonts/font-face.css";
 | 
			
		||||
import "./index.css";
 | 
			
		||||
 | 
			
		||||
createRoot(document.getElementById('root')).render(
 | 
			
		||||
  <StrictMode>
 | 
			
		||||
    <BrowserRouter>
 | 
			
		||||
      <App />
 | 
			
		||||
    </BrowserRouter>
 | 
			
		||||
  </StrictMode>
 | 
			
		||||
)
 | 
			
		||||
createRoot(document.getElementById("root")).render(
 | 
			
		||||
    <StrictMode>
 | 
			
		||||
        <BrowserRouter>
 | 
			
		||||
            <App />
 | 
			
		||||
            <Toaster
 | 
			
		||||
                richColors
 | 
			
		||||
                className="flex justify-center"
 | 
			
		||||
                position="bottom-center"
 | 
			
		||||
                duration="7000"
 | 
			
		||||
                theme="dark"
 | 
			
		||||
            />
 | 
			
		||||
        </BrowserRouter>
 | 
			
		||||
    </StrictMode>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import react from '@vitejs/plugin-react'
 | 
			
		||||
import tailwindcss from '@tailwindcss/vite'
 | 
			
		||||
import { defineConfig } from "vite";
 | 
			
		||||
import react from "@vitejs/plugin-react-swc";
 | 
			
		||||
import tailwindcss from "@tailwindcss/vite";
 | 
			
		||||
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [react(), tailwindcss()],
 | 
			
		||||
})
 | 
			
		||||
    plugins: [react(), tailwindcss()],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
"""Icon links module for social media links."""
 | 
			
		||||
 | 
			
		||||
data = {
 | 
			
		||||
    'discord': 'https://discord.cdrm-project.com/',
 | 
			
		||||
    'telegram': 'https://telegram.cdrm-project.com/',
 | 
			
		||||
    'gitea': 'https://cdm-project.com/tpd94/cdrm-project'
 | 
			
		||||
}
 | 
			
		||||
    "discord": "https://discord.cdrm-project.com/",
 | 
			
		||||
    "telegram": "https://telegram.cdrm-project.com/",
 | 
			
		||||
    "gitea": "https://cdm-project.com/tpd94/cdrm-project",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,47 +1,49 @@
 | 
			
		||||
"""Index tags module for the index page."""
 | 
			
		||||
 | 
			
		||||
tags = {
 | 
			
		||||
    'index': {
 | 
			
		||||
        'description': 'Decrypt Widevine and PlayReady protected content',
 | 
			
		||||
        'keywords': 'CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption',
 | 
			
		||||
        'opengraph_title': 'CDRM-Project',
 | 
			
		||||
        'opengraph_description': 'Self Hosted web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library to decrypt Widevine & Playready content',
 | 
			
		||||
        'opengraph_image': 'https://cdrm-project.com/og-home.jpg',
 | 
			
		||||
        'opengraph_url': 'https://cdm-project.com/tpd94/cdrm-project',
 | 
			
		||||
        'tab_title': 'CDRM-Project',
 | 
			
		||||
    "index": {
 | 
			
		||||
        "description": "Decrypt Widevine and PlayReady protected content",
 | 
			
		||||
        "keywords": "CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption",
 | 
			
		||||
        "opengraph_title": "CDRM-Project",
 | 
			
		||||
        "opengraph_description": "Self Hosted web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library to decrypt Widevine & Playready content",
 | 
			
		||||
        "opengraph_image": "https://cdrm-project.com/og-home.jpg",
 | 
			
		||||
        "opengraph_url": "https://cdm-project.com/tpd94/cdrm-project",
 | 
			
		||||
        "tab_title": "CDRM-Project",
 | 
			
		||||
    },
 | 
			
		||||
    'cache': {
 | 
			
		||||
        'description': 'Search the cache by KID or PSSH for decryption keys',
 | 
			
		||||
        'keywords': 'Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption',
 | 
			
		||||
        'opengraph_title': 'Search the Cache',
 | 
			
		||||
        'opengraph_description': 'Search the cache by KID or PSSH for decryption keys',
 | 
			
		||||
        'opengraph_image': 'https://cdrm-project.com/og-cache.jpg',
 | 
			
		||||
        'opengraph_url': 'https://cdrm-project.com/cache',
 | 
			
		||||
        'tab_title': 'Cache',
 | 
			
		||||
    "cache": {
 | 
			
		||||
        "description": "Search the cache by KID or PSSH for decryption keys",
 | 
			
		||||
        "keywords": "Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption",
 | 
			
		||||
        "opengraph_title": "Search the Cache",
 | 
			
		||||
        "opengraph_description": "Search the cache by KID or PSSH for decryption keys",
 | 
			
		||||
        "opengraph_image": "https://cdrm-project.com/og-cache.jpg",
 | 
			
		||||
        "opengraph_url": "https://cdrm-project.com/cache",
 | 
			
		||||
        "tab_title": "Cache",
 | 
			
		||||
    },
 | 
			
		||||
    'testplayer': {
 | 
			
		||||
        'description': 'Shaka Player for testing decryption keys',
 | 
			
		||||
        'keywords': 'Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY',
 | 
			
		||||
        'opengraph_title': 'Test Player',
 | 
			
		||||
        'opengraph_description': 'Shaka Player for testing decryption keys',
 | 
			
		||||
        'opengraph_image': 'https://cdrm-project.com/og-testplayer.jpg',
 | 
			
		||||
        'opengraph_url': 'https://cdrm-project.com/testplayer',
 | 
			
		||||
        'tab_title': 'Test Player',
 | 
			
		||||
    "testplayer": {
 | 
			
		||||
        "description": "Shaka Player for testing decryption keys",
 | 
			
		||||
        "keywords": "Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY",
 | 
			
		||||
        "opengraph_title": "Test Player",
 | 
			
		||||
        "opengraph_description": "Shaka Player for testing decryption keys",
 | 
			
		||||
        "opengraph_image": "https://cdrm-project.com/og-testplayer.jpg",
 | 
			
		||||
        "opengraph_url": "https://cdrm-project.com/testplayer",
 | 
			
		||||
        "tab_title": "Test Player",
 | 
			
		||||
    },
 | 
			
		||||
    'api': {
 | 
			
		||||
        'description': 'API documentation for the program "CDRM-Project"',
 | 
			
		||||
        'keywords': 'API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault',
 | 
			
		||||
        'opengraph_title': 'API',
 | 
			
		||||
        'opengraph_description': 'Documentation for the program "CDRM-Project"',
 | 
			
		||||
        'opengraph_image': 'https://cdrm-project.com/og-api.jpg',
 | 
			
		||||
        'opengraph_url': 'https://cdrm-project.com/api',
 | 
			
		||||
        'tab_title': 'API',
 | 
			
		||||
    "api": {
 | 
			
		||||
        "description": 'API documentation for the program "CDRM-Project"',
 | 
			
		||||
        "keywords": "API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault",
 | 
			
		||||
        "opengraph_title": "API",
 | 
			
		||||
        "opengraph_description": 'Documentation for the program "CDRM-Project"',
 | 
			
		||||
        "opengraph_image": "https://cdrm-project.com/og-api.jpg",
 | 
			
		||||
        "opengraph_url": "https://cdrm-project.com/api",
 | 
			
		||||
        "tab_title": "API",
 | 
			
		||||
    },
 | 
			
		||||
    'account': {
 | 
			
		||||
        'description': 'Account for CDRM-Project',
 | 
			
		||||
        'keywords': 'Login, CDRM, CDM, CDRM-Project, register, account',
 | 
			
		||||
        'opengraph_title': 'My account',
 | 
			
		||||
        'opengraph_description': 'Account for CDRM-Project',
 | 
			
		||||
        'opengraph_image': 'https://cdrm-project.com/og-home.jpg',
 | 
			
		||||
        'opengraph_url': 'https://cdrm-project.com/account',
 | 
			
		||||
        'tab_title': 'My account',
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
    "account": {
 | 
			
		||||
        "description": "Account for CDRM-Project",
 | 
			
		||||
        "keywords": "Login, CDRM, CDM, CDRM-Project, register, account",
 | 
			
		||||
        "opengraph_title": "My account",
 | 
			
		||||
        "opengraph_description": "Account for CDRM-Project",
 | 
			
		||||
        "opengraph_image": "https://cdrm-project.com/og-home.jpg",
 | 
			
		||||
        "opengraph_url": "https://cdrm-project.com/account",
 | 
			
		||||
        "tab_title": "My account",
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,28 +1,33 @@
 | 
			
		||||
"""Module to cache data to MariaDB."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import yaml
 | 
			
		||||
import mysql.connector
 | 
			
		||||
from mysql.connector import Error
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_db_config():
 | 
			
		||||
    # Configure your MariaDB connection
 | 
			
		||||
    with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
 | 
			
		||||
    """Get the database configuration for MariaDB."""
 | 
			
		||||
    with open(
 | 
			
		||||
        os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
 | 
			
		||||
    ) as file:
 | 
			
		||||
        config = yaml.safe_load(file)
 | 
			
		||||
    db_config = {
 | 
			
		||||
        'host': f'{config["mariadb"]["host"]}',
 | 
			
		||||
        'user': f'{config["mariadb"]["user"]}',
 | 
			
		||||
        'password': f'{config["mariadb"]["password"]}',
 | 
			
		||||
        'database': f'{config["mariadb"]["database"]}'
 | 
			
		||||
        "host": f'{config["mariadb"]["host"]}',
 | 
			
		||||
        "user": f'{config["mariadb"]["user"]}',
 | 
			
		||||
        "password": f'{config["mariadb"]["password"]}',
 | 
			
		||||
        "database": f'{config["mariadb"]["database"]}',
 | 
			
		||||
    }
 | 
			
		||||
    return db_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_database():
 | 
			
		||||
    """Create the database for MariaDB."""
 | 
			
		||||
    try:
 | 
			
		||||
        with mysql.connector.connect(**get_db_config()) as conn:
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            cursor.execute('''
 | 
			
		||||
            cursor.execute(
 | 
			
		||||
                """
 | 
			
		||||
            CREATE TABLE IF NOT EXISTS licenses (
 | 
			
		||||
                SERVICE VARCHAR(255),
 | 
			
		||||
                PSSH TEXT,
 | 
			
		||||
@ -33,20 +38,33 @@ def create_database():
 | 
			
		||||
                Cookies TEXT,
 | 
			
		||||
                Data BLOB
 | 
			
		||||
            )
 | 
			
		||||
            ''')
 | 
			
		||||
            """
 | 
			
		||||
            )
 | 
			
		||||
            conn.commit()
 | 
			
		||||
    except Error as 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:
 | 
			
		||||
        with mysql.connector.connect(**get_db_config()) as conn:
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
            cursor.execute('SELECT 1 FROM licenses WHERE KID = %s', (kid,))
 | 
			
		||||
            cursor.execute("SELECT 1 FROM licenses WHERE KID = %s", (kid,))
 | 
			
		||||
            existing_record = cursor.fetchone()
 | 
			
		||||
 | 
			
		||||
            cursor.execute('''
 | 
			
		||||
            cursor.execute(
 | 
			
		||||
                """
 | 
			
		||||
            INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data)
 | 
			
		||||
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
 | 
			
		||||
            ON DUPLICATE KEY UPDATE
 | 
			
		||||
@ -57,7 +75,9 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h
 | 
			
		||||
                Headers = VALUES(Headers),
 | 
			
		||||
                Cookies = VALUES(Cookies),
 | 
			
		||||
                Data = VALUES(Data)
 | 
			
		||||
            ''', (service, pssh, kid, key, license_url, headers, cookies, data))
 | 
			
		||||
            """,
 | 
			
		||||
                (service, pssh, kid, key, license_url, headers, cookies, data),
 | 
			
		||||
            )
 | 
			
		||||
            conn.commit()
 | 
			
		||||
 | 
			
		||||
            return True if existing_record else False
 | 
			
		||||
@ -65,61 +85,84 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h
 | 
			
		||||
        print(f"Error: {e}")
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def search_by_pssh_or_kid(search_filter):
 | 
			
		||||
    """Search the database by PSSH or KID for MariaDB."""
 | 
			
		||||
    results = set()
 | 
			
		||||
    try:
 | 
			
		||||
        with mysql.connector.connect(**get_db_config()) as conn:
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            like_filter = f"%{search_filter}%"
 | 
			
		||||
 | 
			
		||||
            cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s', (like_filter,))
 | 
			
		||||
            cursor.execute(
 | 
			
		||||
                "SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s",
 | 
			
		||||
                (like_filter,),
 | 
			
		||||
            )
 | 
			
		||||
            results.update(cursor.fetchall())
 | 
			
		||||
 | 
			
		||||
            cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s', (like_filter,))
 | 
			
		||||
            cursor.execute(
 | 
			
		||||
                "SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s",
 | 
			
		||||
                (like_filter,),
 | 
			
		||||
            )
 | 
			
		||||
            results.update(cursor.fetchall())
 | 
			
		||||
 | 
			
		||||
        final_results = [{'PSSH': row[0], 'KID': row[1], 'Key': row[2]} for row in results]
 | 
			
		||||
        final_results = [
 | 
			
		||||
            {"PSSH": row[0], "KID": row[1], "Key": row[2]} for row in results
 | 
			
		||||
        ]
 | 
			
		||||
        return final_results[:20]
 | 
			
		||||
    except Error as e:
 | 
			
		||||
        print(f"Error: {e}")
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_key_by_kid_and_service(kid, service):
 | 
			
		||||
    """Get the key by KID and service for MariaDB."""
 | 
			
		||||
    try:
 | 
			
		||||
        with mysql.connector.connect(**get_db_config()) as conn:
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            cursor.execute('SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s', (kid, service))
 | 
			
		||||
            cursor.execute(
 | 
			
		||||
                "SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s",
 | 
			
		||||
                (kid, service),
 | 
			
		||||
            )
 | 
			
		||||
            result = cursor.fetchone()
 | 
			
		||||
            return result[0] if result else None
 | 
			
		||||
    except Error as e:
 | 
			
		||||
        print(f"Error: {e}")
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_kid_key_dict(service_name):
 | 
			
		||||
    """Get the KID and key dictionary for MariaDB."""
 | 
			
		||||
    try:
 | 
			
		||||
        with mysql.connector.connect(**get_db_config()) as conn:
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            cursor.execute('SELECT KID, `Key` FROM licenses WHERE SERVICE = %s', (service_name,))
 | 
			
		||||
            cursor.execute(
 | 
			
		||||
                "SELECT KID, `Key` FROM licenses WHERE SERVICE = %s", (service_name,)
 | 
			
		||||
            )
 | 
			
		||||
            return {row[0]: row[1] for row in cursor.fetchall()}
 | 
			
		||||
    except Error as e:
 | 
			
		||||
        print(f"Error: {e}")
 | 
			
		||||
        return {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_unique_services():
 | 
			
		||||
    """Get the unique services for MariaDB."""
 | 
			
		||||
    try:
 | 
			
		||||
        with mysql.connector.connect(**get_db_config()) as conn:
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
 | 
			
		||||
            cursor.execute("SELECT DISTINCT SERVICE FROM licenses")
 | 
			
		||||
            return [row[0] for row in cursor.fetchall()]
 | 
			
		||||
    except Error as e:
 | 
			
		||||
        print(f"Error: {e}")
 | 
			
		||||
        return []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def key_count():
 | 
			
		||||
    """Get the key count for MariaDB."""
 | 
			
		||||
    try:
 | 
			
		||||
        with mysql.connector.connect(**get_db_config()) as conn:
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            cursor.execute('SELECT COUNT(KID) FROM licenses')
 | 
			
		||||
            cursor.execute("SELECT COUNT(KID) FROM licenses")
 | 
			
		||||
            return cursor.fetchone()[0]
 | 
			
		||||
    except Error as e:
 | 
			
		||||
        print(f"Error: {e}")
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,17 @@
 | 
			
		||||
"""Module to cache data to SQLite."""
 | 
			
		||||
 | 
			
		||||
import sqlite3
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_database():
 | 
			
		||||
    # Using with statement to manage the connection and cursor
 | 
			
		||||
    with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
 | 
			
		||||
    """Create the database for SQLite."""
 | 
			
		||||
    with sqlite3.connect(
 | 
			
		||||
        os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
 | 
			
		||||
    ) as conn:
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
        cursor.execute(
 | 
			
		||||
            """
 | 
			
		||||
        CREATE TABLE IF NOT EXISTS licenses (
 | 
			
		||||
            SERVICE TEXT,
 | 
			
		||||
            PSSH TEXT,
 | 
			
		||||
@ -16,92 +22,138 @@ def create_database():
 | 
			
		||||
            Cookies 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()
 | 
			
		||||
 | 
			
		||||
        # Check if the record with the given KID already exists
 | 
			
		||||
        cursor.execute('''SELECT 1 FROM licenses WHERE KID = ?''', (kid,))
 | 
			
		||||
        cursor.execute("""SELECT 1 FROM licenses WHERE KID = ?""", (kid,))
 | 
			
		||||
        existing_record = cursor.fetchone()
 | 
			
		||||
 | 
			
		||||
        # Insert or replace the record
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
        cursor.execute(
 | 
			
		||||
            """
 | 
			
		||||
        INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data)
 | 
			
		||||
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
        ''', (service, pssh, kid, key, license_url, headers, cookies, data))
 | 
			
		||||
        """,
 | 
			
		||||
            (service, pssh, kid, key, license_url, headers, cookies, data),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # If the record was existing and updated, return True (updated), else return False (added)
 | 
			
		||||
        return True if existing_record else False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def search_by_pssh_or_kid(search_filter):
 | 
			
		||||
    # Using with statement to automatically close the connection
 | 
			
		||||
    with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
 | 
			
		||||
    """Search the database by PSSH or KID for SQLite."""
 | 
			
		||||
    with sqlite3.connect(
 | 
			
		||||
        os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
 | 
			
		||||
    ) as conn:
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
        # Initialize a set to store unique matching records
 | 
			
		||||
        results = set()
 | 
			
		||||
 | 
			
		||||
        # Search for records where PSSH contains the search_filter
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
        cursor.execute(
 | 
			
		||||
            """
 | 
			
		||||
        SELECT * FROM licenses WHERE PSSH LIKE ?
 | 
			
		||||
        ''', ('%' + search_filter + '%',))
 | 
			
		||||
        """,
 | 
			
		||||
            ("%" + search_filter + "%",),
 | 
			
		||||
        )
 | 
			
		||||
        rows = cursor.fetchall()
 | 
			
		||||
        for row in rows:
 | 
			
		||||
            results.add((row[1], row[2], row[3]))  # (PSSH, KID, Key)
 | 
			
		||||
 | 
			
		||||
        # Search for records where KID contains the search_filter
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
        cursor.execute(
 | 
			
		||||
            """
 | 
			
		||||
        SELECT * FROM licenses WHERE KID LIKE ?
 | 
			
		||||
        ''', ('%' + search_filter + '%',))
 | 
			
		||||
        """,
 | 
			
		||||
            ("%" + search_filter + "%",),
 | 
			
		||||
        )
 | 
			
		||||
        rows = cursor.fetchall()
 | 
			
		||||
        for row in rows:
 | 
			
		||||
            results.add((row[1], row[2], row[3]))  # (PSSH, KID, Key)
 | 
			
		||||
 | 
			
		||||
        # Convert the set of results to a list of dictionaries for output
 | 
			
		||||
        final_results = [{'PSSH': result[0], 'KID': result[1], 'Key': result[2]} for result in results]
 | 
			
		||||
        final_results = [
 | 
			
		||||
            {"PSSH": result[0], "KID": result[1], "Key": result[2]}
 | 
			
		||||
            for result in results
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    return final_results[:20]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_key_by_kid_and_service(kid, service):
 | 
			
		||||
    # Using 'with' to automatically close the connection when done
 | 
			
		||||
    with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
 | 
			
		||||
    """Get the key by KID and service for SQLite."""
 | 
			
		||||
    with sqlite3.connect(
 | 
			
		||||
        os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
 | 
			
		||||
    ) as conn:
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
        # Query to search by KID and SERVICE
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
        cursor.execute(
 | 
			
		||||
            """
 | 
			
		||||
        SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ?
 | 
			
		||||
        ''', (kid, service))
 | 
			
		||||
        """,
 | 
			
		||||
            (kid, service),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Fetch the result
 | 
			
		||||
        result = cursor.fetchone()
 | 
			
		||||
 | 
			
		||||
        # Check if a result was found
 | 
			
		||||
        return result[0] if result else None  # The 'Key' is the first (and only) column returned in the result
 | 
			
		||||
        return (
 | 
			
		||||
            result[0] if result else None
 | 
			
		||||
        )  # The 'Key' is the first (and only) column returned in the result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_kid_key_dict(service_name):
 | 
			
		||||
    # Using with statement to automatically manage the connection and cursor
 | 
			
		||||
    with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
 | 
			
		||||
    """Get the KID and key dictionary for SQLite."""
 | 
			
		||||
    with sqlite3.connect(
 | 
			
		||||
        os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
 | 
			
		||||
    ) as conn:
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
        # Query to fetch KID and Key for the selected service
 | 
			
		||||
        cursor.execute('''
 | 
			
		||||
        cursor.execute(
 | 
			
		||||
            """
 | 
			
		||||
        SELECT KID, Key FROM licenses WHERE SERVICE = ?
 | 
			
		||||
        ''', (service_name,))
 | 
			
		||||
        """,
 | 
			
		||||
            (service_name,),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Fetch all results and create the dictionary
 | 
			
		||||
        kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()}
 | 
			
		||||
 | 
			
		||||
    return kid_key_dict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_unique_services():
 | 
			
		||||
    # Using with statement to automatically manage the connection and cursor
 | 
			
		||||
    with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
 | 
			
		||||
    """Get the unique services for SQLite."""
 | 
			
		||||
    with sqlite3.connect(
 | 
			
		||||
        os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
 | 
			
		||||
    ) as conn:
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
        # Query to get distinct services from the 'licenses' table
 | 
			
		||||
        cursor.execute('SELECT DISTINCT SERVICE FROM licenses')
 | 
			
		||||
        cursor.execute("SELECT DISTINCT SERVICE FROM licenses")
 | 
			
		||||
 | 
			
		||||
        # Fetch all results and extract the unique services
 | 
			
		||||
        services = cursor.fetchall()
 | 
			
		||||
@ -111,13 +163,16 @@ def get_unique_services():
 | 
			
		||||
 | 
			
		||||
    return unique_services
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def key_count():
 | 
			
		||||
    # Using with statement to automatically manage the connection and cursor
 | 
			
		||||
    with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn:
 | 
			
		||||
    """Get the key count for SQLite."""
 | 
			
		||||
    with sqlite3.connect(
 | 
			
		||||
        os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
 | 
			
		||||
    ) as conn:
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
        # Count the number of KID entries in the licenses table
 | 
			
		||||
        cursor.execute('SELECT COUNT(KID) FROM licenses')
 | 
			
		||||
        cursor.execute("SELECT COUNT(KID) FROM licenses")
 | 
			
		||||
        count = cursor.fetchone()[0]  # Fetch the result and get the count
 | 
			
		||||
 | 
			
		||||
    return count
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 os
 | 
			
		||||
import bcrypt
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.execute('''
 | 
			
		||||
        cursor.execute(
 | 
			
		||||
            """
 | 
			
		||||
        CREATE TABLE IF NOT EXISTS user_info (
 | 
			
		||||
            Username TEXT PRIMARY KEY,
 | 
			
		||||
            Password TEXT,
 | 
			
		||||
            Styled_Username TEXT,
 | 
			
		||||
            API_Key TEXT
 | 
			
		||||
        )
 | 
			
		||||
        ''')
 | 
			
		||||
        """
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
        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()
 | 
			
		||||
            return True
 | 
			
		||||
        except sqlite3.IntegrityError:
 | 
			
		||||
@ -32,69 +45,100 @@ def add_user(username, password, api_key):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),))
 | 
			
		||||
        cursor.execute(
 | 
			
		||||
            "SELECT Password FROM user_info WHERE Username = ?", (username.lower(),)
 | 
			
		||||
        )
 | 
			
		||||
        result = cursor.fetchone()
 | 
			
		||||
 | 
			
		||||
        if result:
 | 
			
		||||
            stored_hash = result[0]
 | 
			
		||||
            # Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT)
 | 
			
		||||
            if isinstance(stored_hash, str):
 | 
			
		||||
                stored_hash = stored_hash.encode('utf-8')
 | 
			
		||||
            return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
 | 
			
		||||
        else:
 | 
			
		||||
            return False
 | 
			
		||||
                stored_hash = stored_hash.encode("utf-8")
 | 
			
		||||
            return bcrypt.checkpw(password.encode("utf-8"), stored_hash)
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.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()
 | 
			
		||||
 | 
			
		||||
        if result:
 | 
			
		||||
            return result[0]
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def change_password(username, new_password):
 | 
			
		||||
 | 
			
		||||
    """Change the password for a user."""
 | 
			
		||||
    # Hash the new password
 | 
			
		||||
    new_hashed_pw = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
 | 
			
		||||
    new_hashed_pw = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt())
 | 
			
		||||
 | 
			
		||||
    # 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.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()
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def change_api_key(username, new_api_key):
 | 
			
		||||
    """Change the API key for a user."""
 | 
			
		||||
    # Update the API key in the database
 | 
			
		||||
    with sqlite3.connect(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.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()
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.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()
 | 
			
		||||
 | 
			
		||||
        if result:
 | 
			
		||||
            return result[0]
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.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()
 | 
			
		||||
 | 
			
		||||
        if result:
 | 
			
		||||
            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,46 @@
 | 
			
		||||
"""Module to decrypt the license using the API."""
 | 
			
		||||
 | 
			
		||||
import base64
 | 
			
		||||
import ast
 | 
			
		||||
import glob
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
import binascii
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from requests.exceptions import Timeout, RequestException
 | 
			
		||||
import yaml
 | 
			
		||||
 | 
			
		||||
from pywidevine.cdm import Cdm as widevineCdm
 | 
			
		||||
from pywidevine.device import Device as widevineDevice
 | 
			
		||||
from pywidevine.pssh import PSSH as widevinePSSH
 | 
			
		||||
from pyplayready.cdm import Cdm as playreadyCdm
 | 
			
		||||
from pyplayready.device import Device as playreadyDevice
 | 
			
		||||
from pyplayready.system.pssh import PSSH as playreadyPSSH
 | 
			
		||||
import requests
 | 
			
		||||
import base64
 | 
			
		||||
import ast
 | 
			
		||||
import glob
 | 
			
		||||
import os
 | 
			
		||||
import yaml
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from custom_functions.database.unified_db_ops import cache_to_db
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def find_license_key(data, keywords=None):
 | 
			
		||||
    """Find the license key in the data."""
 | 
			
		||||
    if keywords is None:
 | 
			
		||||
        keywords = ["license", "licenseData", "widevine2License"]  # Default list of keywords to search for
 | 
			
		||||
        keywords = [
 | 
			
		||||
            "license",
 | 
			
		||||
            "licenseData",
 | 
			
		||||
            "widevine2License",
 | 
			
		||||
        ]  # Default list of keywords to search for
 | 
			
		||||
 | 
			
		||||
    # If the data is a dictionary, check each key
 | 
			
		||||
    if isinstance(data, dict):
 | 
			
		||||
        for key, value in data.items():
 | 
			
		||||
            if any(keyword in key.lower() for keyword in
 | 
			
		||||
                   keywords):  # Check if any keyword is in the key (case-insensitive)
 | 
			
		||||
                return value.replace("-", "+").replace("_", "/")  # Return the value immediately when found
 | 
			
		||||
            if any(
 | 
			
		||||
                keyword in key.lower() for keyword in keywords
 | 
			
		||||
            ):  # Check if any keyword is in the key (case-insensitive)
 | 
			
		||||
                return value.replace("-", "+").replace(
 | 
			
		||||
                    "_", "/"
 | 
			
		||||
                )  # Return the value immediately when found
 | 
			
		||||
            # Recursively check if the value is a dictionary or list
 | 
			
		||||
            if isinstance(value, (dict, list)):
 | 
			
		||||
                result = find_license_key(value, keywords)  # Recursively search
 | 
			
		||||
@ -43,37 +58,52 @@ def find_license_key(data, keywords=None):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def find_license_challenge(data, keywords=None, new_value=None):
 | 
			
		||||
    """Find the license challenge in the data."""
 | 
			
		||||
    if keywords is None:
 | 
			
		||||
        keywords = ["license", "licenseData", "widevine2License", "licenseRequest"]  # Default list of keywords to search for
 | 
			
		||||
        keywords = [
 | 
			
		||||
            "license",
 | 
			
		||||
            "licenseData",
 | 
			
		||||
            "widevine2License",
 | 
			
		||||
            "licenseRequest",
 | 
			
		||||
        ]  # Default list of keywords to search for
 | 
			
		||||
 | 
			
		||||
    # If the data is a dictionary, check each key
 | 
			
		||||
    if isinstance(data, dict):
 | 
			
		||||
        for key, value in data.items():
 | 
			
		||||
            if any(keyword in key.lower() for keyword in keywords):  # Check if any keyword is in the key (case-insensitive)
 | 
			
		||||
            if any(
 | 
			
		||||
                keyword in key.lower() for keyword in keywords
 | 
			
		||||
            ):  # Check if any keyword is in the key (case-insensitive)
 | 
			
		||||
                data[key] = new_value  # Modify the value in-place
 | 
			
		||||
            # Recursively check if the value is a dictionary or list
 | 
			
		||||
            elif isinstance(value, (dict, list)):
 | 
			
		||||
                find_license_challenge(value, keywords, new_value)  # Recursively modify in place
 | 
			
		||||
                find_license_challenge(
 | 
			
		||||
                    value, keywords, new_value
 | 
			
		||||
                )  # Recursively modify in place
 | 
			
		||||
 | 
			
		||||
    # If the data is a list, iterate through each item
 | 
			
		||||
    elif isinstance(data, list):
 | 
			
		||||
        for i, item in enumerate(data):
 | 
			
		||||
            result = find_license_challenge(item, keywords, new_value)  # Recursively modify in place
 | 
			
		||||
            result = find_license_challenge(
 | 
			
		||||
                item, keywords, new_value
 | 
			
		||||
            )  # Recursively modify in place
 | 
			
		||||
 | 
			
		||||
    return data  # Return the modified original data (no new structure is created)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_base64(string):
 | 
			
		||||
    """Check if the string is base64 encoded."""
 | 
			
		||||
    try:
 | 
			
		||||
        # Try decoding the string
 | 
			
		||||
        decoded_data = base64.b64decode(string)
 | 
			
		||||
        # Check if the decoded data, when re-encoded, matches the original string
 | 
			
		||||
        return base64.b64encode(decoded_data).decode('utf-8') == string
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return base64.b64encode(decoded_data).decode("utf-8") == string
 | 
			
		||||
    except (binascii.Error, TypeError):
 | 
			
		||||
        # If decoding or encoding fails, it's not Base64
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_url_and_split(input_str):
 | 
			
		||||
    """Check if the string is a URL and split it into protocol and FQDN."""
 | 
			
		||||
    parsed = urlparse(input_str)
 | 
			
		||||
 | 
			
		||||
    # Check if it's a valid URL with scheme and netloc
 | 
			
		||||
@ -81,364 +111,300 @@ def is_url_and_split(input_str):
 | 
			
		||||
        protocol = parsed.scheme
 | 
			
		||||
        fqdn = parsed.netloc
 | 
			
		||||
        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}')
 | 
			
		||||
    with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
 | 
			
		||||
        config = yaml.safe_load(file)
 | 
			
		||||
    if config['database_type'].lower() == 'sqlite':
 | 
			
		||||
        from custom_functions.database.cache_to_db_sqlite import cache_to_db
 | 
			
		||||
    elif config['database_type'].lower() == 'mariadb':
 | 
			
		||||
        from custom_functions.database.cache_to_db_mariadb import cache_to_db
 | 
			
		||||
    if pssh is None:
 | 
			
		||||
        return {
 | 
			
		||||
            'status': 'error',
 | 
			
		||||
            'message': '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}'
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
def sanitize_username(username):
 | 
			
		||||
    """Sanitize the username."""
 | 
			
		||||
    return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def load_device(device_type, device, username, config):
 | 
			
		||||
    """Load the appropriate device file for PlayReady or Widevine."""
 | 
			
		||||
    if device_type == "PR":
 | 
			
		||||
        ext, config_key, class_loader = ".prd", "default_pr_cdm", playreadyDevice.load
 | 
			
		||||
        base_dir = "PR"
 | 
			
		||||
    else:
 | 
			
		||||
        ext, config_key, class_loader = ".wvd", "default_wv_cdm", widevineDevice.load
 | 
			
		||||
        base_dir = "WV"
 | 
			
		||||
 | 
			
		||||
    if device == "public":
 | 
			
		||||
        base_name = config[config_key]
 | 
			
		||||
        if not base_name.endswith(ext):
 | 
			
		||||
            base_name += ext
 | 
			
		||||
        search_path = os.path.join(os.getcwd(), "configs", "CDMs", base_dir, base_name)
 | 
			
		||||
    else:
 | 
			
		||||
        base_name = device
 | 
			
		||||
        if not base_name.endswith(ext):
 | 
			
		||||
            base_name += ext
 | 
			
		||||
        safe_username = sanitize_username(username)
 | 
			
		||||
        search_path = os.path.join(
 | 
			
		||||
            os.getcwd(),
 | 
			
		||||
            "configs",
 | 
			
		||||
            "CDMs",
 | 
			
		||||
            "users_uploaded",
 | 
			
		||||
            safe_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:
 | 
			
		||||
            wv_pssh = widevinePSSH(pssh)
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred processing PSSH\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            if device == 'public':
 | 
			
		||||
                base_name = config["default_wv_cdm"]
 | 
			
		||||
                if not base_name.endswith(".wvd"):
 | 
			
		||||
                    base_name += ".wvd"
 | 
			
		||||
                    wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
 | 
			
		||||
            format_json_data = ast.literal_eval(json_data)
 | 
			
		||||
            if is_widevine:
 | 
			
		||||
                format_json_data = find_license_challenge(
 | 
			
		||||
                    data=format_json_data,
 | 
			
		||||
                    new_value=base64.b64encode(challenge).decode(),
 | 
			
		||||
                )
 | 
			
		||||
        except (ValueError, SyntaxError) as e:
 | 
			
		||||
            raise ValueError(f"Invalid json_data format: {e}") from e
 | 
			
		||||
        except (TypeError, AttributeError) as e:
 | 
			
		||||
            raise ValueError(f"Error processing json_data: {e}") from e
 | 
			
		||||
 | 
			
		||||
    return format_headers, format_cookies, format_json_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_license_request(license_url, headers, cookies, json_data, challenge, proxies):
 | 
			
		||||
    """Send the license request and return the response."""
 | 
			
		||||
    try:
 | 
			
		||||
        response = requests.post(
 | 
			
		||||
            url=license_url,
 | 
			
		||||
            headers=headers,
 | 
			
		||||
            proxies=proxies,
 | 
			
		||||
            cookies=cookies,
 | 
			
		||||
            json=json_data if json_data is not None else None,
 | 
			
		||||
            data=challenge if json_data is None else None,
 | 
			
		||||
            timeout=10,
 | 
			
		||||
        )
 | 
			
		||||
        return response, None
 | 
			
		||||
    except ConnectionError as error:
 | 
			
		||||
        return None, f"Connection error: {error}"
 | 
			
		||||
    except Timeout as error:
 | 
			
		||||
        return None, f"Request timeout: {error}"
 | 
			
		||||
    except RequestException as error:
 | 
			
		||||
        return None, f"Request error: {error}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def extract_and_cache_keys(
 | 
			
		||||
    cdm,
 | 
			
		||||
    session_id,
 | 
			
		||||
    cache_to_db,
 | 
			
		||||
    pssh,
 | 
			
		||||
    license_url,
 | 
			
		||||
    headers,
 | 
			
		||||
    cookies,
 | 
			
		||||
    challenge,
 | 
			
		||||
    json_data,
 | 
			
		||||
    is_widevine,
 | 
			
		||||
):
 | 
			
		||||
    """Extract keys from the session and cache them."""
 | 
			
		||||
    returned_keys = ""
 | 
			
		||||
    try:
 | 
			
		||||
        keys = list(cdm.get_keys(session_id))
 | 
			
		||||
        for index, key in enumerate(keys):
 | 
			
		||||
            # Widevine: key.type, PlayReady: key.key_type
 | 
			
		||||
            key_type = getattr(key, "type", getattr(key, "key_type", None))
 | 
			
		||||
            kid = getattr(key, "kid", getattr(key, "key_id", None))
 | 
			
		||||
            if key_type != "SIGNING" and kid is not None:
 | 
			
		||||
                cache_to_db(
 | 
			
		||||
                    pssh=pssh,
 | 
			
		||||
                    license_url=license_url,
 | 
			
		||||
                    headers=headers,
 | 
			
		||||
                    cookies=cookies,
 | 
			
		||||
                    data=challenge if json_data is None else json_data,
 | 
			
		||||
                    kid=kid.hex,
 | 
			
		||||
                    key=key.key.hex(),
 | 
			
		||||
                )
 | 
			
		||||
                if index != len(keys) - 1:
 | 
			
		||||
                    returned_keys += f"{kid.hex}:{key.key.hex()}\n"
 | 
			
		||||
                else:
 | 
			
		||||
                    wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
 | 
			
		||||
                if wvd_files:
 | 
			
		||||
                    wv_device = widevineDevice.load(wvd_files[0])
 | 
			
		||||
                else:
 | 
			
		||||
                    return {
 | 
			
		||||
                        'status': 'error',
 | 
			
		||||
                        'message': 'No default .wvd file found'
 | 
			
		||||
                    }
 | 
			
		||||
            else:
 | 
			
		||||
                base_name = device
 | 
			
		||||
                if not base_name.endswith(".wvd"):
 | 
			
		||||
                    base_name += ".wvd"
 | 
			
		||||
                    wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
 | 
			
		||||
                else:
 | 
			
		||||
                    wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
 | 
			
		||||
                if wvd_files:
 | 
			
		||||
                    wv_device = widevineDevice.load(wvd_files[0])
 | 
			
		||||
                else:
 | 
			
		||||
                    return {
 | 
			
		||||
                        'status': 'error',
 | 
			
		||||
                        'message': f'{base_name} does not exist'
 | 
			
		||||
                    }
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
                    returned_keys += f"{kid.hex}:{key.key.hex()}"
 | 
			
		||||
        return returned_keys, None
 | 
			
		||||
    except AttributeError as error:
 | 
			
		||||
        return None, f"Error accessing CDM keys: {error}"
 | 
			
		||||
    except (TypeError, ValueError) as error:
 | 
			
		||||
        return None, f"Error processing keys: {error}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def api_decrypt(
 | 
			
		||||
    pssh: str = "",
 | 
			
		||||
    license_url: str = "",
 | 
			
		||||
    proxy: str = "",
 | 
			
		||||
    headers: str = "",
 | 
			
		||||
    cookies: str = "",
 | 
			
		||||
    json_data: str = "",
 | 
			
		||||
    device: str = "public",
 | 
			
		||||
    username: str = "",
 | 
			
		||||
):
 | 
			
		||||
    """Decrypt the license using the API."""
 | 
			
		||||
    print(f"Using device {device} for user {username}")
 | 
			
		||||
    with open(f"{os.getcwd()}/configs/config.yaml", "r", encoding="utf-8") as file:
 | 
			
		||||
        config = yaml.safe_load(file)
 | 
			
		||||
 | 
			
		||||
    if pssh == "":
 | 
			
		||||
        return {"status": "error", "message": "No PSSH provided"}
 | 
			
		||||
 | 
			
		||||
    # Detect PlayReady or Widevine
 | 
			
		||||
    try:
 | 
			
		||||
        is_pr = "</WRMHEADER>".encode("utf-16-le") in base64.b64decode(pssh)
 | 
			
		||||
    except (binascii.Error, TypeError) as error:
 | 
			
		||||
        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 {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred location Widevine CDM file\n\n{error}'
 | 
			
		||||
                "status": "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)
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred loading Widevine CDM\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            wv_session_id = wv_cdm.open()
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred opening a CDM session\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh)
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred getting license challenge\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            if headers:
 | 
			
		||||
                format_headers = ast.literal_eval(headers)
 | 
			
		||||
            else:
 | 
			
		||||
                format_headers = None
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred getting headers\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            if cookies:
 | 
			
		||||
                format_cookies = ast.literal_eval(cookies)
 | 
			
		||||
            else:
 | 
			
		||||
                format_cookies = None
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred getting cookies\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            if json_data and not is_base64(json_data):
 | 
			
		||||
                format_json_data = ast.literal_eval(json_data)
 | 
			
		||||
                format_json_data = find_license_challenge(data=format_json_data, new_value=base64.b64encode(wv_challenge).decode())
 | 
			
		||||
            else:
 | 
			
		||||
                format_json_data = None
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred getting json_data\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        licence = None
 | 
			
		||||
        proxies = None
 | 
			
		||||
        if proxy is not None:
 | 
			
		||||
            is_url, protocol, fqdn = is_url_and_split(proxy)
 | 
			
		||||
            if is_url:
 | 
			
		||||
                proxies = {'http': proxy, 'https': proxy}
 | 
			
		||||
        try:
 | 
			
		||||
            licence = requests.post(
 | 
			
		||||
                url=license_url,
 | 
			
		||||
                headers=format_headers,
 | 
			
		||||
                proxies=proxies,
 | 
			
		||||
                cookies=format_cookies,
 | 
			
		||||
                json=format_json_data if format_json_data is not None else None,
 | 
			
		||||
                data=wv_challenge if format_json_data is None else None
 | 
			
		||||
            )
 | 
			
		||||
        except requests.exceptions.ConnectionError as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred sending license challenge through your proxy\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            wv_cdm.parse_license(wv_session_id, licence.content)
 | 
			
		||||
        except:
 | 
			
		||||
 | 
			
		||||
    # Send license request
 | 
			
		||||
    licence, req_err = send_license_request(
 | 
			
		||||
        license_url,
 | 
			
		||||
        format_headers,
 | 
			
		||||
        format_cookies,
 | 
			
		||||
        format_json_data,
 | 
			
		||||
        challenge,
 | 
			
		||||
        proxies,
 | 
			
		||||
    )
 | 
			
		||||
    if licence is None:
 | 
			
		||||
        return {"status": "error", "message": req_err}
 | 
			
		||||
 | 
			
		||||
    # Parse license
 | 
			
		||||
    try:
 | 
			
		||||
        if is_pr:
 | 
			
		||||
            cdm.parse_license(session_id, licence.text)
 | 
			
		||||
        else:
 | 
			
		||||
            try:
 | 
			
		||||
                license_json = licence.json()
 | 
			
		||||
                license_value = find_license_key(license_json)
 | 
			
		||||
                wv_cdm.parse_license(wv_session_id, license_value)
 | 
			
		||||
            except Exception as error:
 | 
			
		||||
                return {
 | 
			
		||||
                    'status': 'error',
 | 
			
		||||
                    'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}'
 | 
			
		||||
                }
 | 
			
		||||
        returned_keys = ""
 | 
			
		||||
        try:
 | 
			
		||||
            keys = list(wv_cdm.get_keys(wv_session_id))
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred getting keys\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            for index, key in enumerate(keys):
 | 
			
		||||
                if key.type != 'SIGNING':
 | 
			
		||||
                    cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, data=wv_challenge if json_data is None else json_data, kid=key.kid.hex, key=key.key.hex())
 | 
			
		||||
                    if index != len(keys) - 1:
 | 
			
		||||
                        returned_keys += f"{key.kid.hex}:{key.key.hex()}\n"
 | 
			
		||||
                cdm.parse_license(session_id, licence.content)  # type: ignore[arg-type]
 | 
			
		||||
            except (ValueError, TypeError):
 | 
			
		||||
                # Try to extract license from JSON
 | 
			
		||||
                try:
 | 
			
		||||
                    license_json = licence.json()
 | 
			
		||||
                    license_value = find_license_key(license_json)
 | 
			
		||||
                    if license_value is not None:
 | 
			
		||||
                        cdm.parse_license(session_id, license_value)
 | 
			
		||||
                    else:
 | 
			
		||||
                        returned_keys += f"{key.kid.hex}:{key.key.hex()}"
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred formatting keys\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
           wv_cdm.close(wv_session_id)
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred closing session\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
        try:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'success',
 | 
			
		||||
                'message': returned_keys
 | 
			
		||||
            }
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
            return {
 | 
			
		||||
                'status': 'error',
 | 
			
		||||
                'message': f'An error occurred getting returned_keys\n\n{error}'
 | 
			
		||||
            }
 | 
			
		||||
                        return {
 | 
			
		||||
                            "status": "error",
 | 
			
		||||
                            "message": f"Could not extract license from JSON: {license_json}",
 | 
			
		||||
                        }
 | 
			
		||||
                except (ValueError, json.JSONDecodeError, AttributeError) as error:
 | 
			
		||||
                    return {
 | 
			
		||||
                        "status": "error",
 | 
			
		||||
                        "message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
 | 
			
		||||
                    }
 | 
			
		||||
    except (ValueError, TypeError, AttributeError) as error:
 | 
			
		||||
        return {
 | 
			
		||||
            "status": "error",
 | 
			
		||||
            "message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    # Extract and cache keys
 | 
			
		||||
    returned_keys, key_err = extract_and_cache_keys(
 | 
			
		||||
        cdm,
 | 
			
		||||
        session_id,
 | 
			
		||||
        cache_to_db,
 | 
			
		||||
        pssh,
 | 
			
		||||
        license_url,
 | 
			
		||||
        headers,
 | 
			
		||||
        cookies,
 | 
			
		||||
        challenge,
 | 
			
		||||
        json_data,
 | 
			
		||||
        is_widevine=(not is_pr),
 | 
			
		||||
    )
 | 
			
		||||
    if returned_keys == "":
 | 
			
		||||
        return {"status": "error", "message": key_err}
 | 
			
		||||
 | 
			
		||||
    # Close session
 | 
			
		||||
    try:
 | 
			
		||||
        cdm.close(session_id)
 | 
			
		||||
    except (IOError, ValueError, AttributeError) as error:
 | 
			
		||||
        return {
 | 
			
		||||
            "status": "error",
 | 
			
		||||
            "message": f"An error occurred closing session\n\n{error}",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    return {"status": "success", "message": returned_keys}
 | 
			
		||||
 | 
			
		||||
@ -1,68 +1,106 @@
 | 
			
		||||
"""Module to check for and download CDM files."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import yaml
 | 
			
		||||
import requests
 | 
			
		||||
 | 
			
		||||
CONFIG_PATH = os.path.join(os.getcwd(), "configs", "config.yaml")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_wvd_cdm():
 | 
			
		||||
    with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
 | 
			
		||||
        config = yaml.safe_load(file)
 | 
			
		||||
    if config['default_wv_cdm'] == '':
 | 
			
		||||
        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: ')
 | 
			
		||||
        if answer[0].upper() == 'Y':
 | 
			
		||||
            response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd')
 | 
			
		||||
def load_config():
 | 
			
		||||
    """Load the config file."""
 | 
			
		||||
    with open(CONFIG_PATH, "r", encoding="utf-8") as file:
 | 
			
		||||
        return yaml.safe_load(file)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def save_config(config):
 | 
			
		||||
    """Save the config file."""
 | 
			
		||||
    with open(CONFIG_PATH, "w", encoding="utf-8") as file:
 | 
			
		||||
        yaml.dump(config, file)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def prompt_yes_no(message):
 | 
			
		||||
    """Prompt the user for a yes or no answer."""
 | 
			
		||||
    answer = " "
 | 
			
		||||
    while answer[0].upper() not in ["Y", "N"]:
 | 
			
		||||
        answer = input(message)
 | 
			
		||||
    return answer[0].upper() == "Y"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_cdm(config_key, file_ext, download_url, cdm_dir, cdm_name):
 | 
			
		||||
    """Check for a CDM file."""
 | 
			
		||||
    config = load_config()
 | 
			
		||||
    cdm_value = config.get(config_key, "")
 | 
			
		||||
    cdm_dir_path = os.path.join(os.getcwd(), "configs", "CDMs", cdm_dir)
 | 
			
		||||
    os.makedirs(cdm_dir_path, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
    if not cdm_value:
 | 
			
		||||
        if prompt_yes_no(
 | 
			
		||||
            f"No default {cdm_name} CDM specified, would you like to download one "
 | 
			
		||||
            "from The CDM Project? (Y)es / (N)o: "
 | 
			
		||||
        ):
 | 
			
		||||
            response = requests.get(download_url, timeout=10)
 | 
			
		||||
            if response.status_code == 200:
 | 
			
		||||
                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)
 | 
			
		||||
                config['default_wv_cdm'] = 'public'
 | 
			
		||||
                with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
 | 
			
		||||
                    yaml.dump(config, file)
 | 
			
		||||
                print("Successfully downloaded Widevine CDM")
 | 
			
		||||
                config[config_key] = "public"
 | 
			
		||||
                save_config(config)
 | 
			
		||||
                print(f"Successfully downloaded {cdm_name} CDM")
 | 
			
		||||
            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")
 | 
			
		||||
        if answer[0].upper() == 'N':
 | 
			
		||||
            exit(f"Place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml")
 | 
			
		||||
    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
 | 
			
		||||
                sys.exit(
 | 
			
		||||
                    f"Download failed, please try again, or place a .{file_ext} file "
 | 
			
		||||
                    f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV")
 | 
			
		||||
 | 
			
		||||
def check_for_prd_cdm():
 | 
			
		||||
    with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
 | 
			
		||||
        config = yaml.safe_load(file)
 | 
			
		||||
    if config['default_pr_cdm'] == '':
 | 
			
		||||
        answer = ' '
 | 
			
		||||
        while answer[0].upper() != 'Y' and answer[0].upper() != 'N':
 | 
			
		||||
            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':
 | 
			
		||||
            response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd')
 | 
			
		||||
            sys.exit(
 | 
			
		||||
                f"Place a .{file_ext} file in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
 | 
			
		||||
            )
 | 
			
		||||
    else:
 | 
			
		||||
        base_name = (
 | 
			
		||||
            cdm_value
 | 
			
		||||
            if cdm_value.endswith(f".{file_ext}")
 | 
			
		||||
            else f"{cdm_value}.{file_ext}"
 | 
			
		||||
        )
 | 
			
		||||
        file_path = os.path.join(cdm_dir_path, base_name)
 | 
			
		||||
        if os.path.exists(file_path):
 | 
			
		||||
            return
 | 
			
		||||
        # Prompt to download if file is missing, even if config has a value
 | 
			
		||||
        if prompt_yes_no(
 | 
			
		||||
            f"{cdm_name} CDM {base_name} does not exist in {cdm_dir_path}. Would you like to download it from The CDM Project? (Y)es/(N)o: "
 | 
			
		||||
        ):
 | 
			
		||||
            response = requests.get(download_url, timeout=10)
 | 
			
		||||
            if response.status_code == 200:
 | 
			
		||||
                with open(f'{os.getcwd()}/configs/CDMs/PR/public.prd', 'wb') as file:
 | 
			
		||||
                with open(file_path, "wb") as file:
 | 
			
		||||
                    file.write(response.content)
 | 
			
		||||
                config['default_pr_cdm'] = 'public'
 | 
			
		||||
                with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file:
 | 
			
		||||
                    yaml.dump(config, file)
 | 
			
		||||
                print("Successfully downloaded PlayReady CDM")
 | 
			
		||||
                config[config_key] = base_name.replace(f".{file_ext}", "")
 | 
			
		||||
                save_config(config)
 | 
			
		||||
                print(f"Successfully downloaded {cdm_name} CDM")
 | 
			
		||||
            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")
 | 
			
		||||
        if answer[0].upper() == 'N':
 | 
			
		||||
            exit(f"Place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml")
 | 
			
		||||
    else:
 | 
			
		||||
        base_name = config["default_pr_cdm"]
 | 
			
		||||
        if not base_name.endswith(".prd"):
 | 
			
		||||
            base_name += ".prd"
 | 
			
		||||
        if os.path.exists(f'{os.getcwd()}/configs/CDMs/PR/{base_name}'):
 | 
			
		||||
            return
 | 
			
		||||
                sys.exit(
 | 
			
		||||
                    f"Download failed, please try again, or place a .{file_ext} file "
 | 
			
		||||
                    f"in {cdm_dir_path} and specify the name in {CONFIG_PATH}"
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            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():
 | 
			
		||||
    check_for_wvd_cdm()
 | 
			
		||||
    check_for_prd_cdm()
 | 
			
		||||
    """Check for CDM files."""
 | 
			
		||||
    check_for_cdm(
 | 
			
		||||
        config_key="default_wv_cdm",
 | 
			
		||||
        file_ext="wvd",
 | 
			
		||||
        download_url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd",
 | 
			
		||||
        cdm_dir="WV",
 | 
			
		||||
        cdm_name="Widevine",
 | 
			
		||||
    )
 | 
			
		||||
    check_for_cdm(
 | 
			
		||||
        config_key="default_pr_cdm",
 | 
			
		||||
        file_ext="prd",
 | 
			
		||||
        download_url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd",
 | 
			
		||||
        cdm_dir="PR",
 | 
			
		||||
        cdm_name="PlayReady",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,13 @@
 | 
			
		||||
"""Module to check for the config file."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
    else:
 | 
			
		||||
        default_config = """\
 | 
			
		||||
    default_config = """
 | 
			
		||||
default_wv_cdm: ''
 | 
			
		||||
default_pr_cdm: ''
 | 
			
		||||
secret_key_flask: 'secretkey'
 | 
			
		||||
@ -21,6 +24,8 @@ remote_cdm_secret: ''
 | 
			
		||||
#  port: ''
 | 
			
		||||
#  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)
 | 
			
		||||
        return
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
@ -1,37 +1,159 @@
 | 
			
		||||
"""Module to check for the database with unified backend support."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from typing import Dict, Any
 | 
			
		||||
import yaml
 | 
			
		||||
 | 
			
		||||
def check_for_sqlite_database():
 | 
			
		||||
    with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
 | 
			
		||||
        config = yaml.safe_load(file)
 | 
			
		||||
    if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'):
 | 
			
		||||
        return
 | 
			
		||||
    else:
 | 
			
		||||
        if config['database_type'].lower() != 'mariadb':
 | 
			
		||||
            from custom_functions.database.cache_to_db_sqlite import create_database
 | 
			
		||||
            create_database()
 | 
			
		||||
            return
 | 
			
		||||
from custom_functions.database.unified_db_ops import (
 | 
			
		||||
    db_ops,
 | 
			
		||||
    get_backend_info,
 | 
			
		||||
    key_count,
 | 
			
		||||
)
 | 
			
		||||
from custom_functions.database.user_db import create_user_database
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_database_config() -> Dict[str, Any]:
 | 
			
		||||
    """Get the database configuration from config.yaml."""
 | 
			
		||||
    try:
 | 
			
		||||
        config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
 | 
			
		||||
        with open(config_path, "r", encoding="utf-8") as file:
 | 
			
		||||
            config = yaml.safe_load(file)
 | 
			
		||||
        return config
 | 
			
		||||
    except (FileNotFoundError, KeyError, yaml.YAMLError) as e:
 | 
			
		||||
        print(f"Warning: Could not load config.yaml: {e}")
 | 
			
		||||
        return {"database_type": "sqlite"}  # Default fallback
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_sqlite_database() -> None:
 | 
			
		||||
    """Check for the SQLite database file and create if needed."""
 | 
			
		||||
    config = get_database_config()
 | 
			
		||||
    database_type = config.get("database_type", "sqlite").lower()
 | 
			
		||||
 | 
			
		||||
    # Only check for SQLite file if we're using SQLite
 | 
			
		||||
    if database_type == "sqlite":
 | 
			
		||||
        sqlite_path = os.path.join(os.getcwd(), "databases", "sql", "key_cache.db")
 | 
			
		||||
        if not os.path.exists(sqlite_path):
 | 
			
		||||
            print("SQLite database not found, creating...")
 | 
			
		||||
            # Ensure directory exists
 | 
			
		||||
            os.makedirs(os.path.dirname(sqlite_path), exist_ok=True)
 | 
			
		||||
            db_ops.create_database()
 | 
			
		||||
            print(f"SQLite database created at: {sqlite_path}")
 | 
			
		||||
        else:
 | 
			
		||||
            return
 | 
			
		||||
            print(f"SQLite database found at: {sqlite_path}")
 | 
			
		||||
 | 
			
		||||
def check_for_user_database():
 | 
			
		||||
    if os.path.exists(f'{os.getcwd()}/databases/users.db'):
 | 
			
		||||
        return
 | 
			
		||||
    else:
 | 
			
		||||
        from custom_functions.database.user_db import create_user_database
 | 
			
		||||
 | 
			
		||||
def check_for_mariadb_database() -> None:
 | 
			
		||||
    """Check for the MariaDB database and create if needed."""
 | 
			
		||||
    config = get_database_config()
 | 
			
		||||
    database_type = config.get("database_type", "sqlite").lower()
 | 
			
		||||
 | 
			
		||||
    # Only check MariaDB if we're using MariaDB
 | 
			
		||||
    if database_type == "mariadb":
 | 
			
		||||
        try:
 | 
			
		||||
            print("Checking MariaDB connection and creating database if needed...")
 | 
			
		||||
            db_ops.create_database()
 | 
			
		||||
            print("MariaDB database check completed successfully")
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"Error checking/creating MariaDB database: {e}")
 | 
			
		||||
            print("Falling back to SQLite...")
 | 
			
		||||
            # Fallback to SQLite if MariaDB fails
 | 
			
		||||
            fallback_config_path = os.path.join(os.getcwd(), "configs", "config.yaml")
 | 
			
		||||
            try:
 | 
			
		||||
                with open(fallback_config_path, "r", encoding="utf-8") as file:
 | 
			
		||||
                    config = yaml.safe_load(file)
 | 
			
		||||
                config["database_type"] = "sqlite"
 | 
			
		||||
                with open(fallback_config_path, "w", encoding="utf-8") as file:
 | 
			
		||||
                    yaml.safe_dump(config, file)
 | 
			
		||||
                check_for_sqlite_database()
 | 
			
		||||
            except Exception as fallback_error:
 | 
			
		||||
                print(f"Error during fallback to SQLite: {fallback_error}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_user_database() -> None:
 | 
			
		||||
    """Check for the user database and create if needed."""
 | 
			
		||||
    user_db_path = os.path.join(os.getcwd(), "databases", "users.db")
 | 
			
		||||
    if not os.path.exists(user_db_path):
 | 
			
		||||
        print("User database not found, creating...")
 | 
			
		||||
        # Ensure directory exists
 | 
			
		||||
        os.makedirs(os.path.dirname(user_db_path), exist_ok=True)
 | 
			
		||||
        create_user_database()
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
        print(f"User database created at: {user_db_path}")
 | 
			
		||||
    else:
 | 
			
		||||
        return
 | 
			
		||||
        print(f"User database found at: {user_db_path}")
 | 
			
		||||
 | 
			
		||||
def check_for_sql_database():
 | 
			
		||||
    check_for_sqlite_database()
 | 
			
		||||
    check_for_mariadb_database()
 | 
			
		||||
    check_for_user_database()
 | 
			
		||||
 | 
			
		||||
def check_for_sql_database() -> None:
 | 
			
		||||
    """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()
 | 
			
		||||
 | 
			
		||||
    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,44 +1,53 @@
 | 
			
		||||
"""Module to check for the folders."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_config_folder():
 | 
			
		||||
    if os.path.isdir(f'{os.getcwd()}/configs'):
 | 
			
		||||
        return
 | 
			
		||||
    else:
 | 
			
		||||
        os.mkdir(f'{os.getcwd()}/configs')
 | 
			
		||||
    """Check for the config folder."""
 | 
			
		||||
    if os.path.isdir(os.path.join(os.getcwd(), "configs")):
 | 
			
		||||
        return
 | 
			
		||||
    os.mkdir(os.path.join(os.getcwd(), "configs"))
 | 
			
		||||
    return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_database_folder():
 | 
			
		||||
    if os.path.isdir(f'{os.getcwd()}/databases'):
 | 
			
		||||
        return
 | 
			
		||||
    else:
 | 
			
		||||
        os.mkdir(f'{os.getcwd()}/databases')
 | 
			
		||||
        os.mkdir(f'{os.getcwd()}/databases/sql')
 | 
			
		||||
    """Check for the database folder."""
 | 
			
		||||
    if os.path.isdir(os.path.join(os.getcwd(), "databases")):
 | 
			
		||||
        return
 | 
			
		||||
    os.mkdir(os.path.join(os.getcwd(), "databases"))
 | 
			
		||||
    os.mkdir(os.path.join(os.getcwd(), "databases", "sql"))
 | 
			
		||||
    return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_cdm_folder():
 | 
			
		||||
    if os.path.isdir(f'{os.getcwd()}/configs/CDMs'):
 | 
			
		||||
        return
 | 
			
		||||
    else:
 | 
			
		||||
        os.mkdir(f'{os.getcwd()}/configs/CDMs')
 | 
			
		||||
    """Check for the CDM folder."""
 | 
			
		||||
    if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs")):
 | 
			
		||||
        return
 | 
			
		||||
    os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs"))
 | 
			
		||||
    return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_wv_cdm_folder():
 | 
			
		||||
    if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'):
 | 
			
		||||
        return
 | 
			
		||||
    else:
 | 
			
		||||
        os.mkdir(f'{os.getcwd()}/configs/CDMs/WV')
 | 
			
		||||
    """Check for the Widevine CDM folder."""
 | 
			
		||||
    if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV")):
 | 
			
		||||
        return
 | 
			
		||||
    os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "WV"))
 | 
			
		||||
    return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_for_cdm_pr_folder():
 | 
			
		||||
    if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'):
 | 
			
		||||
        return
 | 
			
		||||
    else:
 | 
			
		||||
        os.mkdir(f'{os.getcwd()}/configs/CDMs/PR')
 | 
			
		||||
    """Check for the PlayReady CDM folder."""
 | 
			
		||||
    if os.path.isdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR")):
 | 
			
		||||
        return
 | 
			
		||||
    os.mkdir(os.path.join(os.getcwd(), "configs", "CDMs", "PR"))
 | 
			
		||||
    return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def folder_checks():
 | 
			
		||||
    """Check for the folders."""
 | 
			
		||||
    check_for_config_folder()
 | 
			
		||||
    check_for_database_folder()
 | 
			
		||||
    check_for_cdm_folder()
 | 
			
		||||
    check_for_wv_cdm_folder()
 | 
			
		||||
    check_for_cdm_pr_folder()
 | 
			
		||||
    check_for_cdm_pr_folder()
 | 
			
		||||
 | 
			
		||||
@ -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.config_file_checks import check_for_config_file
 | 
			
		||||
from custom_functions.prechecks.database_checks import check_for_sql_database
 | 
			
		||||
from custom_functions.prechecks.cdm_checks import check_for_cdms
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_frontend_built():
 | 
			
		||||
    """Check if the frontend is built; if not, run build.py."""
 | 
			
		||||
    frontend_dist = os.path.join(os.getcwd(), "frontend-dist")
 | 
			
		||||
    frontend_dist = os.path.abspath(frontend_dist)
 | 
			
		||||
    if not os.path.exists(frontend_dist) or not os.listdir(frontend_dist):
 | 
			
		||||
        print("Frontend has not been built. Running build.py...")
 | 
			
		||||
        subprocess.run(["python", "build.py"], check=True)
 | 
			
		||||
    else:
 | 
			
		||||
        print("Frontend build found.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_precheck():
 | 
			
		||||
    """Run the prechecks."""
 | 
			
		||||
    check_frontend_built()
 | 
			
		||||
    folder_checks()
 | 
			
		||||
    check_for_config_file()
 | 
			
		||||
    check_for_cdms()
 | 
			
		||||
    check_for_sql_database()
 | 
			
		||||
    return
 | 
			
		||||
@ -1,49 +1,52 @@
 | 
			
		||||
"""Module to check for the Python version and environment."""
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
import venv
 | 
			
		||||
import importlib.util
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def version_check():
 | 
			
		||||
    major_version = sys.version_info.major
 | 
			
		||||
    minor_version = sys.version_info.minor
 | 
			
		||||
    """Check for the Python version."""
 | 
			
		||||
    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():
 | 
			
		||||
    try:
 | 
			
		||||
        import pip
 | 
			
		||||
        return
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        exit("Pip is not installed")
 | 
			
		||||
    """Check for the pip installation."""
 | 
			
		||||
    if importlib.util.find_spec("pip") is None:
 | 
			
		||||
        sys.exit("Pip is not installed")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def venv_check():
 | 
			
		||||
    # Check if we're already inside a virtual environment
 | 
			
		||||
    if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
 | 
			
		||||
    """Check for the virtual environment."""
 | 
			
		||||
    if hasattr(sys, "real_prefix") or (
 | 
			
		||||
        hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
 | 
			
		||||
    ):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    venv_path = os.path.join(os.getcwd(), 'cdrm-venv')
 | 
			
		||||
    venv_python = os.path.join(venv_path, 'bin', 'python') if os.name != 'nt' else os.path.join(venv_path, 'Scripts', 'python.exe')
 | 
			
		||||
    venv_path = os.path.join(os.getcwd(), "cdrm-venv")
 | 
			
		||||
    venv_python = (
 | 
			
		||||
        os.path.join(venv_path, "bin", "python")
 | 
			
		||||
        if os.name != "nt"
 | 
			
		||||
        else os.path.join(venv_path, "Scripts", "python.exe")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # If venv already exists, restart script using its Python
 | 
			
		||||
    if os.path.exists(venv_path):
 | 
			
		||||
        subprocess.call([venv_python] + sys.argv)
 | 
			
		||||
        sys.exit()
 | 
			
		||||
 | 
			
		||||
    # Ask user for permission to create a virtual environment
 | 
			
		||||
    answer = ''
 | 
			
		||||
    while not answer or answer[0].upper() not in {'Y', 'N'}:
 | 
			
		||||
        answer = input(
 | 
			
		||||
            'Program is not running from a venv. To maintain compatibility and dependencies, this program must be run from one.\n'
 | 
			
		||||
            'Would you like me to create one for you? (Y/N): '
 | 
			
		||||
    answer = (
 | 
			
		||||
        input(
 | 
			
		||||
            "Program is not running from a virtual environment. To maintain "
 | 
			
		||||
            "compatibility, this program must be run from one.\n"
 | 
			
		||||
            "Would you like to create one? (Y/N): "
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if answer[0].upper() == 'Y':
 | 
			
		||||
        .strip()
 | 
			
		||||
        .upper()
 | 
			
		||||
    )
 | 
			
		||||
    if answer.startswith("Y"):
 | 
			
		||||
        print("Creating virtual environment...")
 | 
			
		||||
        venv.create(venv_path, with_pip=True)
 | 
			
		||||
        subprocess.call([venv_python] + sys.argv)
 | 
			
		||||
@ -54,32 +57,57 @@ def venv_check():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def requirements_check():
 | 
			
		||||
    try:
 | 
			
		||||
        import pywidevine
 | 
			
		||||
        import pyplayready
 | 
			
		||||
        import flask
 | 
			
		||||
        import flask_cors
 | 
			
		||||
        import yaml
 | 
			
		||||
        import mysql.connector
 | 
			
		||||
    """Check for the requirements."""
 | 
			
		||||
    required_packages = [
 | 
			
		||||
        "pywidevine",
 | 
			
		||||
        "pyplayready",
 | 
			
		||||
        "flask",
 | 
			
		||||
        "flask_cors",
 | 
			
		||||
        "yaml",
 | 
			
		||||
        "mysql.connector",
 | 
			
		||||
    ]
 | 
			
		||||
    missing = []
 | 
			
		||||
    for pkg in required_packages:
 | 
			
		||||
        if "." in pkg:
 | 
			
		||||
            parent, _ = pkg.split(".", 1)
 | 
			
		||||
            if (
 | 
			
		||||
                importlib.util.find_spec(parent) is None
 | 
			
		||||
                or importlib.util.find_spec(pkg) is None
 | 
			
		||||
            ):
 | 
			
		||||
                missing.append(pkg)
 | 
			
		||||
        else:
 | 
			
		||||
            if importlib.util.find_spec(pkg) is None:
 | 
			
		||||
                missing.append(pkg)
 | 
			
		||||
    if not missing:
 | 
			
		||||
        return
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        while True:
 | 
			
		||||
            user_input = input("Missing packages. Do you want to install them? (Y/N): ").strip().upper()
 | 
			
		||||
            if user_input == 'Y':
 | 
			
		||||
                print("Installing packages from requirements.txt...")
 | 
			
		||||
                subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
 | 
			
		||||
                print("Installation complete.")
 | 
			
		||||
                break
 | 
			
		||||
            elif user_input == 'N':
 | 
			
		||||
                print("Dependencies required, please install them and run again.")
 | 
			
		||||
                sys.exit()
 | 
			
		||||
            else:
 | 
			
		||||
                print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
 | 
			
		||||
 | 
			
		||||
    while True:
 | 
			
		||||
        user_input = (
 | 
			
		||||
            input(
 | 
			
		||||
                f"Missing packages: {', '.join(missing)}. Do you want to install them? (Y/N): "
 | 
			
		||||
            )
 | 
			
		||||
            .strip()
 | 
			
		||||
            .upper()
 | 
			
		||||
        )
 | 
			
		||||
        if user_input == "Y":
 | 
			
		||||
            print("Installing packages from requirements.txt...")
 | 
			
		||||
            subprocess.check_call(
 | 
			
		||||
                [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]
 | 
			
		||||
            )
 | 
			
		||||
            print("Installation complete.")
 | 
			
		||||
            break
 | 
			
		||||
        if user_input == "N":
 | 
			
		||||
            print("Dependencies required, please install them and run again.")
 | 
			
		||||
            sys.exit()
 | 
			
		||||
        else:
 | 
			
		||||
            print("Invalid input. Please enter 'Y' to install or 'N' to exit.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
    version_check()
 | 
			
		||||
    pip_check()
 | 
			
		||||
    venv_check()
 | 
			
		||||
    requirements_check()
 | 
			
		||||
    requirements_check()
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,28 @@
 | 
			
		||||
"""Module to check if the user is allowed to use the device."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import glob
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sanitize_username(username):
 | 
			
		||||
    """Sanitize the username."""
 | 
			
		||||
    return re.sub(r"[^a-zA-Z0-9_\-]", "_", username).lower()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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", "users_uploaded", sanitize_username(username)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Get filenames with extensions
 | 
			
		||||
    pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
 | 
			
		||||
    wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
 | 
			
		||||
    pr_files = [
 | 
			
		||||
        os.path.basename(f) for f in glob.glob(os.path.join(base_path, "PR", "*.prd"))
 | 
			
		||||
    ]
 | 
			
		||||
    wv_files = [
 | 
			
		||||
        os.path.basename(f) for f in glob.glob(os.path.join(base_path, "WV", "*.wvd"))
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # Combine all filenames
 | 
			
		||||
    all_files = pr_files + wv_files
 | 
			
		||||
@ -14,4 +30,4 @@ def user_allowed_to_use_device(device, username):
 | 
			
		||||
    # Check if filename matches directly or by adding extensions
 | 
			
		||||
    possible_names = {device, f"{device}.prd", f"{device}.wvd"}
 | 
			
		||||
 | 
			
		||||
    return any(name in all_files for name in possible_names)
 | 
			
		||||
    return any(name in all_files for name in possible_names)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								main.py
									
									
									
									
									
								
							@ -1,9 +1,10 @@
 | 
			
		||||
from custom_functions.prechecks.python_checks import run_python_checks
 | 
			
		||||
run_python_checks()
 | 
			
		||||
from custom_functions.prechecks.precheck import run_precheck
 | 
			
		||||
run_precheck()
 | 
			
		||||
"""Main module to run the application."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import yaml
 | 
			
		||||
from flask import Flask
 | 
			
		||||
from flask_cors import CORS
 | 
			
		||||
 | 
			
		||||
from routes.react import react_bp
 | 
			
		||||
from routes.api import api_bp
 | 
			
		||||
from routes.remote_device_wv import remotecdm_wv_bp
 | 
			
		||||
@ -13,12 +14,19 @@ from routes.user_info import user_info_bp
 | 
			
		||||
from routes.register import register_bp
 | 
			
		||||
from routes.login import login_bp
 | 
			
		||||
from routes.user_changes import user_change_bp
 | 
			
		||||
import os
 | 
			
		||||
import yaml
 | 
			
		||||
from custom_functions.prechecks.python_checks import run_python_checks
 | 
			
		||||
from custom_functions.prechecks.precheck import run_precheck
 | 
			
		||||
 | 
			
		||||
run_python_checks()
 | 
			
		||||
run_precheck()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
app.secret_key = config['secret_key_flask']
 | 
			
		||||
app.secret_key = config["secret_key_flask"]
 | 
			
		||||
 | 
			
		||||
CORS(app)
 | 
			
		||||
 | 
			
		||||
@ -33,5 +41,5 @@ app.register_blueprint(remotecdm_wv_bp)
 | 
			
		||||
app.register_blueprint(remotecdm_pr_bp)
 | 
			
		||||
app.register_blueprint(user_change_bp)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    app.run(debug=True, host='0.0.0.0')
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    app.run(debug=True, host="0.0.0.0")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
pywidevine~=1.8.0
 | 
			
		||||
pyplayready~=0.6.0
 | 
			
		||||
requests~=2.32.3
 | 
			
		||||
requests
 | 
			
		||||
protobuf~=4.25.6
 | 
			
		||||
PyYAML~=6.0.2
 | 
			
		||||
PyYAML
 | 
			
		||||
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 sqlite3
 | 
			
		||||
from flask import Blueprint, jsonify, request, send_file, session
 | 
			
		||||
import json
 | 
			
		||||
from custom_functions.decrypt.api_decrypt import api_decrypt
 | 
			
		||||
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
 | 
			
		||||
import shutil
 | 
			
		||||
import math
 | 
			
		||||
import yaml
 | 
			
		||||
import mysql.connector
 | 
			
		||||
from io import StringIO
 | 
			
		||||
import tempfile
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from flask import Blueprint, jsonify, request, send_file, session, after_this_request
 | 
			
		||||
import yaml
 | 
			
		||||
import mysql.connector
 | 
			
		||||
 | 
			
		||||
from custom_functions.decrypt.api_decrypt import api_decrypt
 | 
			
		||||
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
 | 
			
		||||
from custom_functions.database.unified_db_ops import (
 | 
			
		||||
    search_by_pssh_or_kid,
 | 
			
		||||
    cache_to_db,
 | 
			
		||||
    get_key_by_kid_and_service,
 | 
			
		||||
    get_unique_services,
 | 
			
		||||
    get_kid_key_dict,
 | 
			
		||||
    key_count,
 | 
			
		||||
)
 | 
			
		||||
from configs.icon_links import data as icon_data
 | 
			
		||||
 | 
			
		||||
api_bp = Blueprint('api', __name__)
 | 
			
		||||
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
 | 
			
		||||
api_bp = Blueprint("api", __name__)
 | 
			
		||||
with open(os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8") as file:
 | 
			
		||||
    config = yaml.safe_load(file)
 | 
			
		||||
if config['database_type'].lower() != 'mariadb':
 | 
			
		||||
    from custom_functions.database.cache_to_db_sqlite import search_by_pssh_or_kid, cache_to_db, \
 | 
			
		||||
        get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count
 | 
			
		||||
elif config['database_type'].lower() == 'mariadb':
 | 
			
		||||
    from custom_functions.database.cache_to_db_mariadb import search_by_pssh_or_kid, cache_to_db, \
 | 
			
		||||
        get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_db_config():
 | 
			
		||||
    # Configure your MariaDB connection
 | 
			
		||||
    with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
 | 
			
		||||
        config = yaml.safe_load(file)
 | 
			
		||||
    """Get the MariaDB database configuration."""
 | 
			
		||||
    with open(
 | 
			
		||||
        os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
 | 
			
		||||
    ) as file_mariadb:
 | 
			
		||||
        config_mariadb = yaml.safe_load(file_mariadb)
 | 
			
		||||
    db_config = {
 | 
			
		||||
        'host': f'{config["mariadb"]["host"]}',
 | 
			
		||||
        'user': f'{config["mariadb"]["user"]}',
 | 
			
		||||
        'password': f'{config["mariadb"]["password"]}',
 | 
			
		||||
        'database': f'{config["mariadb"]["database"]}'
 | 
			
		||||
        "host": f'{config_mariadb["mariadb"]["host"]}',
 | 
			
		||||
        "user": f'{config_mariadb["mariadb"]["user"]}',
 | 
			
		||||
        "password": f'{config_mariadb["mariadb"]["password"]}',
 | 
			
		||||
        "database": f'{config_mariadb["mariadb"]["database"]}',
 | 
			
		||||
    }
 | 
			
		||||
    return db_config
 | 
			
		||||
 | 
			
		||||
@api_bp.route('/api/cache/search', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@api_bp.route("/api/cache/search", methods=["POST"])
 | 
			
		||||
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)
 | 
			
		||||
    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):
 | 
			
		||||
    """Get the multiple keys from the database."""
 | 
			
		||||
    result = get_kid_key_dict(service_name=service)
 | 
			
		||||
    pages = math.ceil(len(result) / 10)
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'code': 0,
 | 
			
		||||
        'content_keys': result,
 | 
			
		||||
        'pages': pages
 | 
			
		||||
    })
 | 
			
		||||
    return jsonify({"code": 0, "content_keys": result, "pages": pages})
 | 
			
		||||
 | 
			
		||||
@api_bp.route('/api/cache/<service>/<kid>', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@api_bp.route("/api/cache/<service>/<kid>", methods=["POST"])
 | 
			
		||||
def add_single_key_service(service, kid):
 | 
			
		||||
    """Add the single key to the database."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    content_key = body['content_key']
 | 
			
		||||
    content_key = body["content_key"]
 | 
			
		||||
    result = cache_to_db(service=service, kid=kid, key=content_key)
 | 
			
		||||
    if result:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'code': 0,
 | 
			
		||||
            'updated': True,
 | 
			
		||||
        })
 | 
			
		||||
    elif result is False:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'code': 0,
 | 
			
		||||
            'updated': True,
 | 
			
		||||
        })
 | 
			
		||||
        return jsonify(
 | 
			
		||||
            {
 | 
			
		||||
                "code": 0,
 | 
			
		||||
                "updated": True,
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "code": 0,
 | 
			
		||||
            "updated": True,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@api_bp.route('/api/cache/<service>', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@api_bp.route("/api/cache/<service>", methods=["POST"])
 | 
			
		||||
def add_multiple_key_service(service):
 | 
			
		||||
    """Add the multiple keys to the database."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    keys_added = 0
 | 
			
		||||
    keys_updated = 0
 | 
			
		||||
    for kid, key in body['content_keys'].items():
 | 
			
		||||
    for kid, key in body["content_keys"].items():
 | 
			
		||||
        result = cache_to_db(service=service, kid=kid, key=key)
 | 
			
		||||
        if result is True:
 | 
			
		||||
            keys_updated += 1
 | 
			
		||||
        elif result is False:
 | 
			
		||||
        else:
 | 
			
		||||
            keys_added += 1
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'code': 0,
 | 
			
		||||
        'added': str(keys_added),
 | 
			
		||||
        'updated': str(keys_updated),
 | 
			
		||||
    })
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "code": 0,
 | 
			
		||||
            "added": str(keys_added),
 | 
			
		||||
            "updated": str(keys_updated),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@api_bp.route('/api/cache', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@api_bp.route("/api/cache", methods=["POST"])
 | 
			
		||||
def unique_service():
 | 
			
		||||
    """Get the unique services from the database."""
 | 
			
		||||
    services = get_unique_services()
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'code': 0,
 | 
			
		||||
        'service_list': services,
 | 
			
		||||
    })
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "code": 0,
 | 
			
		||||
            "service_list": services,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_bp.route('/api/cache/download', methods=['GET'])
 | 
			
		||||
@api_bp.route("/api/cache/download", methods=["GET"])
 | 
			
		||||
def download_database():
 | 
			
		||||
    if config['database_type'].lower() != 'mariadb':
 | 
			
		||||
        original_database_path = f'{os.getcwd()}/databases/sql/key_cache.db'
 | 
			
		||||
    """Download the database."""
 | 
			
		||||
    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)
 | 
			
		||||
        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.)
 | 
			
		||||
        shutil.copy2(original_database_path, modified_database_path)
 | 
			
		||||
@ -117,151 +144,156 @@ def download_database():
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
            # Update all rows to remove Headers and Cookies (set them to NULL or empty strings)
 | 
			
		||||
            cursor.execute('''
 | 
			
		||||
            cursor.execute(
 | 
			
		||||
                """
 | 
			
		||||
            UPDATE licenses
 | 
			
		||||
            SET Headers = NULL,
 | 
			
		||||
                Cookies = NULL
 | 
			
		||||
            ''')
 | 
			
		||||
            """
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # No need for explicit commit, it's done automatically with the 'with' block
 | 
			
		||||
            # The connection will automatically be committed and closed when the block ends
 | 
			
		||||
 | 
			
		||||
        # Send the modified database as an attachment
 | 
			
		||||
        return send_file(modified_database_path, as_attachment=True, download_name='key_cache.db')
 | 
			
		||||
    if config['database_type'].lower() == 'mariadb':
 | 
			
		||||
        try:
 | 
			
		||||
            # Connect to MariaDB
 | 
			
		||||
            conn = mysql.connector.connect(**get_db_config())
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
        return send_file(
 | 
			
		||||
            modified_database_path, as_attachment=True, download_name="key_cache.db"
 | 
			
		||||
        )
 | 
			
		||||
    try:
 | 
			
		||||
        conn = mysql.connector.connect(**get_db_config())
 | 
			
		||||
        cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
            # Update sensitive data (this updates the live DB, you may want to duplicate rows instead)
 | 
			
		||||
            cursor.execute('''
 | 
			
		||||
            UPDATE licenses
 | 
			
		||||
            SET Headers = NULL,
 | 
			
		||||
                Cookies = NULL
 | 
			
		||||
            ''')
 | 
			
		||||
        # Get column names
 | 
			
		||||
        cursor.execute("SHOW COLUMNS FROM licenses")
 | 
			
		||||
        columns = [row[0] for row in cursor.fetchall()]
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
            cursor.execute('SELECT * FROM licenses')
 | 
			
		||||
            rows = cursor.fetchall()
 | 
			
		||||
            column_names = [desc[0] for desc in cursor.description]
 | 
			
		||||
        # Dump to SQL-like format
 | 
			
		||||
        output = StringIO()
 | 
			
		||||
        output.write("-- Dump of `licenses` table (Headers and Cookies are NULL)\n")
 | 
			
		||||
        for row in rows:
 | 
			
		||||
            values = ", ".join(
 | 
			
		||||
                f"'{str(v).replace('\'', '\\\'')}'" if v is not None else "NULL"
 | 
			
		||||
                for v in row
 | 
			
		||||
            )
 | 
			
		||||
            output.write(
 | 
			
		||||
                f"INSERT INTO licenses ({', '.join(columns)}) VALUES ({values});\n"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # Dump to SQL-like format
 | 
			
		||||
            output = StringIO()
 | 
			
		||||
            output.write(f"-- Dump of `licenses` table\n")
 | 
			
		||||
            for row in rows:
 | 
			
		||||
                values = ', '.join(f"'{str(v).replace('\'', '\\\'')}'" if v is not None else 'NULL' for v in row)
 | 
			
		||||
                output.write(f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n")
 | 
			
		||||
        # Write to a temp file for download
 | 
			
		||||
        temp_dir = tempfile.gettempdir()
 | 
			
		||||
        temp_path = os.path.join(temp_dir, "key_cache.sql")
 | 
			
		||||
        with open(temp_path, "w", encoding="utf-8") as f:
 | 
			
		||||
            f.write(output.getvalue())
 | 
			
		||||
 | 
			
		||||
            # Write to a temp file for download
 | 
			
		||||
            temp_dir = tempfile.gettempdir()
 | 
			
		||||
            temp_path = os.path.join(temp_dir, 'key_cache.sql')
 | 
			
		||||
            with open(temp_path, 'w', encoding='utf-8') as f:
 | 
			
		||||
                f.write(output.getvalue())
 | 
			
		||||
        @after_this_request
 | 
			
		||||
        def remove_file(response):
 | 
			
		||||
            try:
 | 
			
		||||
                os.remove(temp_path)
 | 
			
		||||
            except Exception:
 | 
			
		||||
                pass
 | 
			
		||||
            return response
 | 
			
		||||
 | 
			
		||||
            return send_file(temp_path, as_attachment=True, download_name='licenses_dump.sql')
 | 
			
		||||
        except mysql.connector.Error as err:
 | 
			
		||||
            return {"error": str(err)}, 500
 | 
			
		||||
        return send_file(
 | 
			
		||||
            temp_path, as_attachment=True, download_name="licenses_dump.sql"
 | 
			
		||||
        )
 | 
			
		||||
    except mysql.connector.Error as err:
 | 
			
		||||
        return {"error": str(err)}, 500
 | 
			
		||||
 | 
			
		||||
_keycount_cache = {
 | 
			
		||||
    'count': None,
 | 
			
		||||
    'timestamp': 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@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():
 | 
			
		||||
    """Get the count of the keys in the database."""
 | 
			
		||||
    now = time.time()
 | 
			
		||||
    if now - _keycount_cache['timestamp'] > 10 or _keycount_cache['count'] is None:
 | 
			
		||||
        _keycount_cache['count'] = key_count()
 | 
			
		||||
        _keycount_cache['timestamp'] = now
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'count': _keycount_cache['count']
 | 
			
		||||
    })
 | 
			
		||||
    if now - _keycount_cache["timestamp"] > 10 or _keycount_cache["count"] is None:
 | 
			
		||||
        _keycount_cache["count"] = key_count()
 | 
			
		||||
        _keycount_cache["timestamp"] = now
 | 
			
		||||
    return jsonify({"count": _keycount_cache["count"]})
 | 
			
		||||
 | 
			
		||||
@api_bp.route('/api/decrypt', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@api_bp.route("/api/decrypt", methods=["POST"])
 | 
			
		||||
def decrypt_data():
 | 
			
		||||
    api_request_data = json.loads(request.data)
 | 
			
		||||
    if 'pssh' in api_request_data:
 | 
			
		||||
        if api_request_data['pssh'] == '':
 | 
			
		||||
            api_request_pssh = None
 | 
			
		||||
        else:
 | 
			
		||||
            api_request_pssh = api_request_data['pssh']
 | 
			
		||||
    """Decrypt the data."""
 | 
			
		||||
    api_request_data = request.get_json(force=True)
 | 
			
		||||
 | 
			
		||||
    # Helper to get fields or None if missing/empty
 | 
			
		||||
    def get_field(key, default=""):
 | 
			
		||||
        value = api_request_data.get(key, default)
 | 
			
		||||
        return value if value != "" else default
 | 
			
		||||
 | 
			
		||||
    api_request_pssh = get_field("pssh")
 | 
			
		||||
    api_request_licurl = get_field("licurl")
 | 
			
		||||
    api_request_proxy = get_field("proxy")
 | 
			
		||||
    api_request_headers = get_field("headers")
 | 
			
		||||
    api_request_cookies = get_field("cookies")
 | 
			
		||||
    api_request_data_func = get_field("data")
 | 
			
		||||
 | 
			
		||||
    # Device logic
 | 
			
		||||
    device = get_field("device", "public")
 | 
			
		||||
    if device in [
 | 
			
		||||
        "default",
 | 
			
		||||
        "CDRM-Project Public Widevine CDM",
 | 
			
		||||
        "CDRM-Project Public PlayReady CDM",
 | 
			
		||||
        "",
 | 
			
		||||
        None,
 | 
			
		||||
    ]:
 | 
			
		||||
        api_request_device = "public"
 | 
			
		||||
    else:
 | 
			
		||||
        api_request_pssh = None
 | 
			
		||||
    if 'licurl' in api_request_data:
 | 
			
		||||
        if api_request_data['licurl'] == '':
 | 
			
		||||
            api_request_licurl = None
 | 
			
		||||
        else:
 | 
			
		||||
            api_request_licurl = api_request_data['licurl']
 | 
			
		||||
    else:
 | 
			
		||||
        api_request_licurl = None
 | 
			
		||||
    if 'proxy' in api_request_data:
 | 
			
		||||
        if api_request_data['proxy'] == '':
 | 
			
		||||
            api_request_proxy = None
 | 
			
		||||
        else:
 | 
			
		||||
            api_request_proxy = api_request_data['proxy']
 | 
			
		||||
    else:
 | 
			
		||||
        api_request_proxy = None
 | 
			
		||||
    if 'headers' in api_request_data:
 | 
			
		||||
        if api_request_data['headers'] == '':
 | 
			
		||||
            api_request_headers = None
 | 
			
		||||
        else:
 | 
			
		||||
            api_request_headers = api_request_data['headers']
 | 
			
		||||
    else:
 | 
			
		||||
        api_request_headers = None
 | 
			
		||||
    if 'cookies' in api_request_data:
 | 
			
		||||
        if api_request_data['cookies'] == '':
 | 
			
		||||
            api_request_cookies = None
 | 
			
		||||
        else:
 | 
			
		||||
            api_request_cookies = api_request_data['cookies']
 | 
			
		||||
    else:
 | 
			
		||||
        api_request_cookies = None
 | 
			
		||||
    if 'data' in api_request_data:
 | 
			
		||||
        if api_request_data['data'] == '':
 | 
			
		||||
            api_request_data_func = None
 | 
			
		||||
        else:
 | 
			
		||||
            api_request_data_func = api_request_data['data']
 | 
			
		||||
    else: api_request_data_func = None
 | 
			
		||||
    if 'device' in api_request_data:
 | 
			
		||||
        if api_request_data['device'] == 'default' or api_request_data['device'] == 'CDRM-Project Public Widevine CDM' or api_request_data['device'] == 'CDRM-Project Public PlayReady CDM':
 | 
			
		||||
            api_request_device = 'public'
 | 
			
		||||
        else:
 | 
			
		||||
            api_request_device = api_request_data['device']
 | 
			
		||||
    else:
 | 
			
		||||
        api_request_device = 'public'
 | 
			
		||||
    username = None
 | 
			
		||||
    if api_request_device != 'public':
 | 
			
		||||
        username = session.get('username')
 | 
			
		||||
        api_request_device = device
 | 
			
		||||
 | 
			
		||||
    username = ""
 | 
			
		||||
    if api_request_device != "public":
 | 
			
		||||
        username = session.get("username")
 | 
			
		||||
        if not username:
 | 
			
		||||
            return jsonify({'message': 'Not logged in, not allowed'}), 400
 | 
			
		||||
        if user_allowed_to_use_device(device=api_request_device, username=username):
 | 
			
		||||
            api_request_device = api_request_device
 | 
			
		||||
        else:
 | 
			
		||||
            return jsonify({'message': f'Not authorized / Not found'}), 403
 | 
			
		||||
    result = api_decrypt(pssh=api_request_pssh, proxy=api_request_proxy, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data_func, device=api_request_device, username=username)
 | 
			
		||||
    if result['status'] == 'success':
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 'success',
 | 
			
		||||
            'message': result['message']
 | 
			
		||||
        })
 | 
			
		||||
    else:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 'fail',
 | 
			
		||||
            'message': result['message']
 | 
			
		||||
        })
 | 
			
		||||
            return jsonify({"message": "Not logged in, not allowed"}), 400
 | 
			
		||||
        if not user_allowed_to_use_device(device=api_request_device, username=username):
 | 
			
		||||
            return jsonify({"message": "Not authorized / Not found"}), 403
 | 
			
		||||
 | 
			
		||||
@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():
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'discord': icon_data['discord'],
 | 
			
		||||
        'telegram': icon_data['telegram'],
 | 
			
		||||
        'gitea': icon_data['gitea'],
 | 
			
		||||
    })
 | 
			
		||||
    """Get the links."""
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "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():
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'status': True,
 | 
			
		||||
    })
 | 
			
		||||
    """Verify the extension."""
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "status": True,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -1,37 +1,42 @@
 | 
			
		||||
"""Module to handle the login process."""
 | 
			
		||||
 | 
			
		||||
from flask import Blueprint, request, jsonify, session
 | 
			
		||||
from custom_functions.database.user_db import verify_user
 | 
			
		||||
 | 
			
		||||
login_bp = Blueprint(
 | 
			
		||||
    'login_bp',
 | 
			
		||||
    "login_bp",
 | 
			
		||||
    __name__,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@login_bp.route('/login', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@login_bp.route("/login", methods=["POST"])
 | 
			
		||||
def login():
 | 
			
		||||
    if request.method == 'POST':
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        for required_field in ['username', 'password']:
 | 
			
		||||
            if required_field not in data:
 | 
			
		||||
                return jsonify({'error': f'Missing required field: {required_field}'}), 400
 | 
			
		||||
    """Handle the login process."""
 | 
			
		||||
    data = request.get_json()
 | 
			
		||||
    for required_field in ["username", "password"]:
 | 
			
		||||
        if required_field not in data:
 | 
			
		||||
            return (
 | 
			
		||||
                jsonify({"error": f"Missing required field: {required_field}"}),
 | 
			
		||||
                400,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if verify_user(data['username'], data['password']):
 | 
			
		||||
            session['username'] = data['username'].lower()  # Stored securely in a signed cookie
 | 
			
		||||
            return jsonify({'message': 'Successfully logged in!'})
 | 
			
		||||
        else:
 | 
			
		||||
            return jsonify({'error': 'Invalid username or password!'}), 401
 | 
			
		||||
    if verify_user(data["username"], data["password"]):
 | 
			
		||||
        session["username"] = data[
 | 
			
		||||
            "username"
 | 
			
		||||
        ].lower()  # Stored securely in a signed cookie
 | 
			
		||||
        return jsonify({"message": "Successfully logged in!"})
 | 
			
		||||
    return jsonify({"error": "Invalid username or password!"}), 401
 | 
			
		||||
 | 
			
		||||
@login_bp.route('/login/status', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@login_bp.route("/login/status", methods=["POST"])
 | 
			
		||||
def login_status():
 | 
			
		||||
    try:
 | 
			
		||||
        username = session.get('username')
 | 
			
		||||
        if username:
 | 
			
		||||
            return jsonify({'message': 'True'})
 | 
			
		||||
        else:
 | 
			
		||||
            return jsonify({'message': 'False'})
 | 
			
		||||
    except:
 | 
			
		||||
        return jsonify({'message': 'False'})
 | 
			
		||||
    """Check if the user is logged in."""
 | 
			
		||||
    username = session.get("username")
 | 
			
		||||
    return jsonify({"message": "True" if username else "False"})
 | 
			
		||||
 | 
			
		||||
@login_bp.route('/logout', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@login_bp.route("/logout", methods=["POST"])
 | 
			
		||||
def logout():
 | 
			
		||||
    session.pop('username', None)
 | 
			
		||||
    return jsonify({'message': 'Successfully logged out!'})
 | 
			
		||||
    """Logout the user."""
 | 
			
		||||
    session.pop("username", None)
 | 
			
		||||
    return jsonify({"message": "Successfully logged out!"})
 | 
			
		||||
 | 
			
		||||
@ -1,33 +1,48 @@
 | 
			
		||||
import sys
 | 
			
		||||
"""Module to handle the React routes."""
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
if getattr(sys, 'frozen', False):  # Running as a bundled app
 | 
			
		||||
    base_path = sys._MEIPASS
 | 
			
		||||
if getattr(sys, "frozen", False):  # Running as a bundled app
 | 
			
		||||
    base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
 | 
			
		||||
else:  # Running in a normal Python environment
 | 
			
		||||
    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',
 | 
			
		||||
    "react_bp",
 | 
			
		||||
    __name__,
 | 
			
		||||
    static_folder=static_folder,
 | 
			
		||||
    static_url_path='/',
 | 
			
		||||
    template_folder=static_folder
 | 
			
		||||
    static_url_path="/",
 | 
			
		||||
    template_folder=static_folder,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@react_bp.route('/', methods=['GET'])
 | 
			
		||||
@react_bp.route('/<path:path>', methods=["GET"])
 | 
			
		||||
@react_bp.route('/<path>', methods=["GET"])
 | 
			
		||||
def index(path=''):
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        file_path = os.path.join(react_bp.static_folder, path)
 | 
			
		||||
        if path != "" and os.path.exists(file_path):
 | 
			
		||||
            return send_from_directory(react_bp.static_folder, path)
 | 
			
		||||
        elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']:
 | 
			
		||||
            data = index_tags.tags.get(path.lower(), index_tags.tags['index'])
 | 
			
		||||
            return render_template('index.html', data=data)
 | 
			
		||||
        else:
 | 
			
		||||
            return send_from_directory(react_bp.static_folder, 'index.html')
 | 
			
		||||
 | 
			
		||||
@react_bp.route("/", methods=["GET"])
 | 
			
		||||
@react_bp.route("/<path:path>", methods=["GET"])
 | 
			
		||||
@react_bp.route("/<path>", methods=["GET"])
 | 
			
		||||
def index(path=""):
 | 
			
		||||
    """Handle the index route."""
 | 
			
		||||
    # Ensure static_folder is not None
 | 
			
		||||
    if react_bp.static_folder is None:
 | 
			
		||||
        raise ValueError("Static folder is not configured for the blueprint")
 | 
			
		||||
 | 
			
		||||
    # Normalize the path to prevent directory traversal
 | 
			
		||||
    safe_path = os.path.normpath(path)
 | 
			
		||||
    file_path = os.path.join(react_bp.static_folder, safe_path)
 | 
			
		||||
 | 
			
		||||
    if path and os.path.exists(file_path):
 | 
			
		||||
        return send_from_directory(react_bp.static_folder, safe_path)
 | 
			
		||||
 | 
			
		||||
    # Only allow certain paths to render index.html with tags
 | 
			
		||||
    allowed_paths = ["", "cache", "api", "testplayer", "account"]
 | 
			
		||||
    if safe_path.lower() in allowed_paths:
 | 
			
		||||
        data = index_tags.tags.get(safe_path.lower(), index_tags.tags.get("index", {}))
 | 
			
		||||
        return render_template("index.html", data=data)
 | 
			
		||||
 | 
			
		||||
    # Fallback: serve index.html for all other routes (SPA)
 | 
			
		||||
    return send_from_directory(react_bp.static_folder, "index.html")
 | 
			
		||||
 | 
			
		||||
@ -1,42 +1,56 @@
 | 
			
		||||
"""Module to handle the register process."""
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
import uuid
 | 
			
		||||
from flask import Blueprint, request, jsonify
 | 
			
		||||
from custom_functions.database.user_db import add_user
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
register_bp = Blueprint('register_bp', __name__)
 | 
			
		||||
register_bp = Blueprint("register_bp", __name__)
 | 
			
		||||
 | 
			
		||||
USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$')
 | 
			
		||||
PASSWORD_REGEX = re.compile(r'^\S+$')
 | 
			
		||||
USERNAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$")
 | 
			
		||||
PASSWORD_REGEX = re.compile(r"^\S+$")
 | 
			
		||||
 | 
			
		||||
@register_bp.route('/register', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@register_bp.route("/register", methods=["POST"])
 | 
			
		||||
def register():
 | 
			
		||||
    if request.method != 'POST':
 | 
			
		||||
        return jsonify({'error': 'Method not supported'}), 405
 | 
			
		||||
 | 
			
		||||
    """Handle the register process."""
 | 
			
		||||
    data = request.get_json()
 | 
			
		||||
    if data is None:
 | 
			
		||||
        return jsonify({"error": "Invalid or missing JSON in request body."}), 400
 | 
			
		||||
 | 
			
		||||
    # Check required fields
 | 
			
		||||
    for required_field in ['username', 'password']:
 | 
			
		||||
    for required_field in ["username", "password"]:
 | 
			
		||||
        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']
 | 
			
		||||
    password = data['password']
 | 
			
		||||
    username = data["username"].lower()
 | 
			
		||||
    password = data["password"]
 | 
			
		||||
    api_key = str(uuid.uuid4())
 | 
			
		||||
 | 
			
		||||
    # Length checks
 | 
			
		||||
    if not (3 <= len(username) <= 32):
 | 
			
		||||
        return jsonify({"error": "Username must be 3-32 characters."}), 400
 | 
			
		||||
    if not (8 <= len(password) <= 128):
 | 
			
		||||
        return jsonify({"error": "Password must be 8-128 characters."}), 400
 | 
			
		||||
 | 
			
		||||
    # Validate username and password
 | 
			
		||||
    if not USERNAME_REGEX.fullmatch(username):
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'error': 'Invalid username. Only letters, numbers, hyphens, and underscores are allowed.'
 | 
			
		||||
        }), 400
 | 
			
		||||
        return (
 | 
			
		||||
            jsonify(
 | 
			
		||||
                {
 | 
			
		||||
                    "error": "Invalid username. Only letters, numbers, hyphens, and underscores are allowed."
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if not PASSWORD_REGEX.fullmatch(password):
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'error': 'Invalid password. Spaces are not allowed.'
 | 
			
		||||
        }), 400
 | 
			
		||||
        return jsonify({"error": "Invalid password. Spaces are not allowed."}), 400
 | 
			
		||||
 | 
			
		||||
    # Attempt to add user
 | 
			
		||||
    if add_user(username, password, api_key):
 | 
			
		||||
        return jsonify({'message': 'User successfully registered!'}), 201
 | 
			
		||||
    else:
 | 
			
		||||
        return jsonify({'error': 'User already exists!'}), 409
 | 
			
		||||
        return (
 | 
			
		||||
            jsonify({"message": "User successfully registered!", "api_key": api_key}),
 | 
			
		||||
            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
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import re
 | 
			
		||||
import yaml
 | 
			
		||||
from flask import Blueprint, jsonify, request, current_app, Response
 | 
			
		||||
 | 
			
		||||
from pyplayready.device import Device as PlayReadyDevice
 | 
			
		||||
from pyplayready.cdm import Cdm as PlayReadyCDM
 | 
			
		||||
from pyplayready import PSSH as PlayReadyPSSH
 | 
			
		||||
from pyplayready.exceptions import (InvalidSession, 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.decrypt.api_decrypt import is_base64
 | 
			
		||||
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(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
 | 
			
		||||
remotecdm_pr_bp = Blueprint("remotecdm_pr", __name__)
 | 
			
		||||
with open(
 | 
			
		||||
    os.path.join(os.getcwd(), "configs", "config.yaml"), "r", encoding="utf-8"
 | 
			
		||||
) as 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():
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': 'OK'
 | 
			
		||||
        })
 | 
			
		||||
    if request.method == 'HEAD':
 | 
			
		||||
    """Handle the remote device PlayReady."""
 | 
			
		||||
    if request.method == "GET":
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Success",
 | 
			
		||||
            "OK",
 | 
			
		||||
            http_status=200,
 | 
			
		||||
        )
 | 
			
		||||
    if request.method == "HEAD":
 | 
			
		||||
        response = Response(status=200)
 | 
			
		||||
        response.headers['Server'] = 'playready serve'
 | 
			
		||||
        response.headers["Server"] = "playready serve"
 | 
			
		||||
        return response
 | 
			
		||||
    return make_response("Failed", "Method not allowed", http_status=405)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo', methods=['GET'])
 | 
			
		||||
@remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo", methods=["GET"])
 | 
			
		||||
def remote_cdm_playready_deviceinfo():
 | 
			
		||||
    """Handle the remote device PlayReady device info."""
 | 
			
		||||
    base_name = config["default_pr_cdm"]
 | 
			
		||||
    if not base_name.endswith(".prd"):
 | 
			
		||||
        full_file_name = (base_name + ".prd")
 | 
			
		||||
    device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}')
 | 
			
		||||
    device = PlayReadyDevice.load(
 | 
			
		||||
        os.path.join(os.getcwd(), "configs", "CDMs", "PR", base_name + ".prd")
 | 
			
		||||
    )
 | 
			
		||||
    cdm = PlayReadyCDM.from_device(device)
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'security_level': cdm.security_level,
 | 
			
		||||
        'host': f'{config["fqdn"]}/remotecdm/playready',
 | 
			
		||||
        'secret': f'{config["remote_cdm_secret"]}',
 | 
			
		||||
        'device_name': Path(base_name).stem
 | 
			
		||||
    })
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "security_level": cdm.security_level,
 | 
			
		||||
            "host": f'{config["fqdn"]}/remotecdm/playready',
 | 
			
		||||
            "secret": f'{config["remote_cdm_secret"]}',
 | 
			
		||||
            "device_name": 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):
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        base_name = Path(device).with_suffix('.prd').name
 | 
			
		||||
        api_key = request.headers['X-Secret-Key']
 | 
			
		||||
        username = fetch_username_by_api_key(api_key)
 | 
			
		||||
        device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}')
 | 
			
		||||
        cdm = PlayReadyCDM.from_device(device)
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'security_level': cdm.security_level,
 | 
			
		||||
            'host': f'{config["fqdn"]}/remotecdm/widevine',
 | 
			
		||||
            'secret': f'{api_key}',
 | 
			
		||||
            'device_name': Path(base_name).stem
 | 
			
		||||
        })
 | 
			
		||||
    """Handle the remote device PlayReady device info specific."""
 | 
			
		||||
    base_name = Path(device).with_suffix(".prd").name
 | 
			
		||||
    api_key = request.headers["X-Secret-Key"]
 | 
			
		||||
    username = fetch_username_by_api_key(api_key)
 | 
			
		||||
    if not username:
 | 
			
		||||
        return jsonify({"message": "Invalid or missing API key."}), 403
 | 
			
		||||
    safe_username = sanitize_username(username)
 | 
			
		||||
    device = PlayReadyDevice.load(
 | 
			
		||||
        os.path.join(
 | 
			
		||||
            os.getcwd(),
 | 
			
		||||
            "configs",
 | 
			
		||||
            "CDMs",
 | 
			
		||||
            "users_uploaded",
 | 
			
		||||
            safe_username,
 | 
			
		||||
            "PR",
 | 
			
		||||
            base_name,
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    cdm = PlayReadyCDM.from_device(device)
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "security_level": cdm.security_level,
 | 
			
		||||
            "host": f'{config["fqdn"]}/remotecdm/playready',
 | 
			
		||||
            "secret": f"{api_key}",
 | 
			
		||||
            "device_name": Path(base_name).stem,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', methods=['GET'])
 | 
			
		||||
 | 
			
		||||
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/open", methods=["GET"])
 | 
			
		||||
def remote_cdm_playready_open(device):
 | 
			
		||||
    if str(device).lower() == config['default_pr_cdm'].lower():
 | 
			
		||||
        pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd')
 | 
			
		||||
        cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
 | 
			
		||||
        session_id = cdm.open()
 | 
			
		||||
        return 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
 | 
			
		||||
    """Handle the remote device PlayReady open."""
 | 
			
		||||
    unauthorized_msg = {
 | 
			
		||||
        "message": f"Device '{device}' is not found or you are not authorized to use it."
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@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):
 | 
			
		||||
    """Handle the remote device PlayReady close."""
 | 
			
		||||
    try:
 | 
			
		||||
        session_id = bytes.fromhex(session_id)
 | 
			
		||||
        cdm = current_app.config["CDM"]
 | 
			
		||||
        if not cdm:
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'message': f'No CDM for "{device}" has been opened yet. No session to close'
 | 
			
		||||
            }), 400
 | 
			
		||||
        cdm = get_cdm_or_error(device)
 | 
			
		||||
        if isinstance(cdm, tuple):  # error response
 | 
			
		||||
            return cdm
 | 
			
		||||
        try:
 | 
			
		||||
            cdm.close(session_id)
 | 
			
		||||
        except InvalidSession:
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
 | 
			
		||||
            }), 400
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f'Successfully closed Session "{session_id.hex()}".',
 | 
			
		||||
        }), 200
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f'Failed to close Session "{session_id.hex()}".'
 | 
			
		||||
        }), 400
 | 
			
		||||
            return make_response(
 | 
			
		||||
                "Error",
 | 
			
		||||
                f'Invalid session ID "{session_id.hex()}", it may have expired',
 | 
			
		||||
                http_status=400,
 | 
			
		||||
            )
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Success",
 | 
			
		||||
            f'Successfully closed Session "{session_id.hex()}".',
 | 
			
		||||
            http_status=200,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as error:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f'Failed to close Session "{session_id.hex()}", {error}.',
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@remotecdm_pr_bp.route(
 | 
			
		||||
    "/remotecdm/playready/<device>/get_license_challenge", methods=["POST"]
 | 
			
		||||
)
 | 
			
		||||
def remote_cdm_playready_get_license_challenge(device):
 | 
			
		||||
    """Handle the remote device PlayReady get license challenge."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    for required_field in ("session_id", "init_data"):
 | 
			
		||||
        if not body.get(required_field):
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'message': f'Missing required field "{required_field}" in JSON body'
 | 
			
		||||
            }), 400
 | 
			
		||||
    cdm = current_app.config["CDM"]
 | 
			
		||||
    missing_field = check_required_fields(body, ("session_id", "init_data"))
 | 
			
		||||
    if missing_field:
 | 
			
		||||
        return missing_field
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
    init_data = body["init_data"]
 | 
			
		||||
    if not init_data.startswith("<WRMHEADER"):
 | 
			
		||||
@ -144,43 +238,46 @@ def remote_cdm_playready_get_license_challenge(device):
 | 
			
		||||
            pssh = PlayReadyPSSH(init_data)
 | 
			
		||||
            if pssh.wrm_headers:
 | 
			
		||||
                init_data = pssh.wrm_headers[0]
 | 
			
		||||
        except InvalidPssh as e:
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'message': f'Unable to parse base64 PSSH, {e}'
 | 
			
		||||
            })
 | 
			
		||||
        except InvalidPssh as error:
 | 
			
		||||
            return make_response(
 | 
			
		||||
                "Error",
 | 
			
		||||
                f"Unable to parse base64 PSSH, {error}",
 | 
			
		||||
                http_status=400,
 | 
			
		||||
            )
 | 
			
		||||
    try:
 | 
			
		||||
        license_request = cdm.get_license_challenge(
 | 
			
		||||
            session_id=session_id,
 | 
			
		||||
            wrm_header=init_data
 | 
			
		||||
            session_id=session_id, wrm_header=init_data
 | 
			
		||||
        )
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
 | 
			
		||||
        })
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f'Error, {e}'
 | 
			
		||||
        })
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'message': 'success',
 | 
			
		||||
        'data': {
 | 
			
		||||
            'challenge': license_request
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid Session ID '{session_id.hex()}', it may have expired.",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
    except ValueError as error:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid License, {error}",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        "Successfully got the License Challenge",
 | 
			
		||||
        {"challenge_b64": base64.b64encode(license_request.encode("utf-8")).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):
 | 
			
		||||
    """Handle the remote device PlayReady parse license."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    for required_field in ("license_message", "session_id"):
 | 
			
		||||
        if not body.get(required_field):
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'message': f'Missing required field "{required_field}" in JSON body'
 | 
			
		||||
            })
 | 
			
		||||
    cdm = current_app.config["CDM"]
 | 
			
		||||
    if not cdm:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f"No Cdm session for {device} has been opened yet. No session to use."
 | 
			
		||||
        })
 | 
			
		||||
    missing_field = check_required_fields(body, ("license_message", "session_id"))
 | 
			
		||||
    if missing_field:
 | 
			
		||||
        return missing_field
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
    license_message = body["license_message"]
 | 
			
		||||
    if is_base64(license_message):
 | 
			
		||||
@ -188,45 +285,49 @@ def remote_cdm_playready_parse_license(device):
 | 
			
		||||
    try:
 | 
			
		||||
        cdm.parse_license(session_id, license_message)
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
 | 
			
		||||
        })
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid Session ID '{session_id.hex()}', it may have expired.",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
    except InvalidLicense as e:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f"Invalid License, {e}"
 | 
			
		||||
        })
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid License, {e}",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f"Error, {e}"
 | 
			
		||||
        })
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'message': 'Successfully parsed and loaded the Keys from the License message'
 | 
			
		||||
    })
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Error, {e}",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        "Successfully parsed and loaded the Keys from the License message",
 | 
			
		||||
        http_status=200,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@remotecdm_pr_bp.route("/remotecdm/playready/<device>/get_keys", methods=["POST"])
 | 
			
		||||
def remote_cdm_playready_get_keys(device):
 | 
			
		||||
    """Handle the remote device PlayReady get keys."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    for required_field in ("session_id",):
 | 
			
		||||
        if not body.get(required_field):
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'message': f'Missing required field "{required_field}" in JSON body'
 | 
			
		||||
            })
 | 
			
		||||
    missing_field = check_required_fields(body, ("session_id",))
 | 
			
		||||
    if missing_field:
 | 
			
		||||
        return missing_field
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
    cdm = current_app.config["CDM"]
 | 
			
		||||
    if not cdm:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f"Missing required field '{required_field}' in JSON body."
 | 
			
		||||
        })
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
    try:
 | 
			
		||||
        keys = cdm.get_keys(session_id)
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
 | 
			
		||||
        })
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'message': f"Error, {e}"
 | 
			
		||||
        })
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid Session ID '{session_id.hex()}', it may have expired.",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
    keys_json = [
 | 
			
		||||
        {
 | 
			
		||||
            "key_id": key.key_id.hex,
 | 
			
		||||
@ -237,9 +338,9 @@ def remote_cdm_playready_get_keys(device):
 | 
			
		||||
        }
 | 
			
		||||
        for key in keys
 | 
			
		||||
    ]
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'message': 'success',
 | 
			
		||||
        'data': {
 | 
			
		||||
            'keys': keys_json
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        "Successfully got the Keys",
 | 
			
		||||
        {"keys": keys_json},
 | 
			
		||||
        http_status=200,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -1,387 +1,493 @@
 | 
			
		||||
"""Module to handle the remote device Widevine."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from flask import Blueprint, jsonify, request, current_app, Response
 | 
			
		||||
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 pywidevine.pssh import PSSH as widevinePSSH
 | 
			
		||||
from pywidevine import __version__
 | 
			
		||||
from pywidevine.cdm import Cdm as widevineCDM
 | 
			
		||||
from pywidevine.device import Device as widevineDevice
 | 
			
		||||
from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
 | 
			
		||||
                                   InvalidSession, SignatureMismatch, TooManySessions)
 | 
			
		||||
from pywidevine.exceptions import (
 | 
			
		||||
    InvalidContext,
 | 
			
		||||
    InvalidInitData,
 | 
			
		||||
    InvalidLicenseMessage,
 | 
			
		||||
    InvalidLicenseType,
 | 
			
		||||
    InvalidSession,
 | 
			
		||||
    SignatureMismatch,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
import yaml
 | 
			
		||||
from custom_functions.database.user_db import fetch_api_key, fetch_username_by_api_key
 | 
			
		||||
from custom_functions.database.user_db import fetch_username_by_api_key
 | 
			
		||||
from custom_functions.database.unified_db_ops import cache_to_db
 | 
			
		||||
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
@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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_cdm_or_error(device: str):
 | 
			
		||||
    """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", methods=["GET", "HEAD"])
 | 
			
		||||
def remote_cdm_widevine():
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 200,
 | 
			
		||||
            'message': f"{config['fqdn'].upper()} Remote Widevine CDM."
 | 
			
		||||
        })
 | 
			
		||||
    if request.method == 'HEAD':
 | 
			
		||||
    """Handle the remote device Widevine."""
 | 
			
		||||
    if request.method == "GET":
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Success",
 | 
			
		||||
            f"{config['fqdn'].upper()} Remote Widevine CDM.",
 | 
			
		||||
            http_status=200,
 | 
			
		||||
        )
 | 
			
		||||
    if request.method == "HEAD":
 | 
			
		||||
        response = Response(status=200)
 | 
			
		||||
        response.headers['Server'] = f'https://github.com/devine-dl/pywidevine serve v{__version__}'
 | 
			
		||||
        response.headers["Server"] = (
 | 
			
		||||
            f"https://github.com/devine-dl/pywidevine serve v{__version__}"
 | 
			
		||||
        )
 | 
			
		||||
        return response
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Error",
 | 
			
		||||
        "Invalid request method",
 | 
			
		||||
        http_status=405,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo', methods=['GET'])
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo", methods=["GET"])
 | 
			
		||||
def remote_cdm_widevine_deviceinfo():
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        base_name = config["default_wv_cdm"]
 | 
			
		||||
        if not base_name.endswith(".wvd"):
 | 
			
		||||
            base_name = (base_name + ".wvd")
 | 
			
		||||
        device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
 | 
			
		||||
        cdm = widevineCDM.from_device(device)
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'device_type': cdm.device_type.name,
 | 
			
		||||
            'system_id': cdm.system_id,
 | 
			
		||||
            'security_level': cdm.security_level,
 | 
			
		||||
            'host': f'{config["fqdn"]}/remotecdm/widevine',
 | 
			
		||||
            'secret': f'{config["remote_cdm_secret"]}',
 | 
			
		||||
            'device_name': Path(base_name).stem
 | 
			
		||||
        })
 | 
			
		||||
    """Handle the remote device Widevine device info."""
 | 
			
		||||
    base_name = config["default_wv_cdm"]
 | 
			
		||||
    if not base_name.endswith(".wvd"):
 | 
			
		||||
        base_name = base_name + ".wvd"
 | 
			
		||||
    device = widevineDevice.load(
 | 
			
		||||
        os.path.join(os.getcwd(), "configs", "CDMs", "WV", base_name)
 | 
			
		||||
    )
 | 
			
		||||
    cdm = widevineCDM.from_device(device)
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "device_type": cdm.device_type.name,
 | 
			
		||||
            "system_id": cdm.system_id,
 | 
			
		||||
            "security_level": cdm.security_level,
 | 
			
		||||
            "host": f'{config["fqdn"]}/remotecdm/widevine',
 | 
			
		||||
            "secret": f'{config["remote_cdm_secret"]}',
 | 
			
		||||
            "device_name": Path(base_name).stem,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@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):
 | 
			
		||||
    if request.method == 'GET':
 | 
			
		||||
        base_name = Path(device).with_suffix('.wvd').name
 | 
			
		||||
        api_key = request.headers['X-Secret-Key']
 | 
			
		||||
        username = fetch_username_by_api_key(api_key)
 | 
			
		||||
        device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}')
 | 
			
		||||
        cdm = widevineCDM.from_device(device)
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'device_type': cdm.device_type.name,
 | 
			
		||||
            'system_id': cdm.system_id,
 | 
			
		||||
            'security_level': cdm.security_level,
 | 
			
		||||
            'host': f'{config["fqdn"]}/remotecdm/widevine',
 | 
			
		||||
            'secret': f'{api_key}',
 | 
			
		||||
            'device_name': Path(base_name).stem
 | 
			
		||||
        })
 | 
			
		||||
    """Handle the remote device Widevine device info specific."""
 | 
			
		||||
    base_name = Path(device).with_suffix(".wvd").name
 | 
			
		||||
    api_key = request.headers["X-Secret-Key"]
 | 
			
		||||
    username = fetch_username_by_api_key(api_key)
 | 
			
		||||
    device = widevineDevice.load(
 | 
			
		||||
        os.path.join(
 | 
			
		||||
            os.getcwd(),
 | 
			
		||||
            "configs",
 | 
			
		||||
            "CDMs",
 | 
			
		||||
            "users_uploaded",
 | 
			
		||||
            sanitize_username(username),
 | 
			
		||||
            "WV",
 | 
			
		||||
            base_name,
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    cdm = widevineCDM.from_device(device)
 | 
			
		||||
    return jsonify(
 | 
			
		||||
        {
 | 
			
		||||
            "device_type": cdm.device_type.name,
 | 
			
		||||
            "system_id": cdm.system_id,
 | 
			
		||||
            "security_level": cdm.security_level,
 | 
			
		||||
            "host": f'{config["fqdn"]}/remotecdm/widevine',
 | 
			
		||||
            "secret": f"{api_key}",
 | 
			
		||||
            "device_name": Path(base_name).stem,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET'])
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route("/remotecdm/widevine/<device>/open", methods=["GET"])
 | 
			
		||||
def remote_cdm_widevine_open(device):
 | 
			
		||||
    if str(device).lower() == config['default_wv_cdm'].lower():
 | 
			
		||||
        wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd')
 | 
			
		||||
    """Handle the remote device Widevine open."""
 | 
			
		||||
    if str(device).lower() == config["default_wv_cdm"].lower():
 | 
			
		||||
        wv_device = widevineDevice.load(
 | 
			
		||||
            os.path.join(
 | 
			
		||||
                os.getcwd(), "configs", "CDMs", "WV", config["default_wv_cdm"] + ".wvd"
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
 | 
			
		||||
        session_id = cdm.open()
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 200,
 | 
			
		||||
            'message': 'Success',
 | 
			
		||||
            'data': {
 | 
			
		||||
                'session_id': session_id.hex(),
 | 
			
		||||
                'device': {
 | 
			
		||||
                    'system_id': cdm.system_id,
 | 
			
		||||
                    'security_level': cdm.security_level,
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }), 200
 | 
			
		||||
    if request.headers['X-Secret-Key'] and str(device).lower() != config['default_wv_cdm'].lower():
 | 
			
		||||
        api_key = request.headers['X-Secret-Key']
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Success",
 | 
			
		||||
            "Successfully opened the Widevine Session",
 | 
			
		||||
            {
 | 
			
		||||
                "session_id": session_id.hex(),
 | 
			
		||||
                "device": {
 | 
			
		||||
                    "system_id": cdm.system_id,
 | 
			
		||||
                    "security_level": cdm.security_level,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            http_status=200,
 | 
			
		||||
        )
 | 
			
		||||
    if (
 | 
			
		||||
        request.headers["X-Secret-Key"]
 | 
			
		||||
        and str(device).lower() != config["default_wv_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):
 | 
			
		||||
                wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/WV/{device}.wvd')
 | 
			
		||||
                cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
 | 
			
		||||
                session_id = cdm.open()
 | 
			
		||||
                return jsonify({
 | 
			
		||||
                    'status': 200,
 | 
			
		||||
                    'message': 'Success',
 | 
			
		||||
                    'data': {
 | 
			
		||||
                        'session_id': session_id.hex(),
 | 
			
		||||
                        'device': {
 | 
			
		||||
                            'system_id': cdm.system_id,
 | 
			
		||||
                            'security_level': cdm.security_level,
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }), 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
 | 
			
		||||
        if user and user_allowed_to_use_device(device=device, username=user):
 | 
			
		||||
            wv_device = widevineDevice.load(
 | 
			
		||||
                os.path.join(
 | 
			
		||||
                    os.getcwd(), "configs", "CDMs", user, "WV", device + ".wvd"
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device)
 | 
			
		||||
            session_id = cdm.open()
 | 
			
		||||
            return make_response(
 | 
			
		||||
                "Success",
 | 
			
		||||
                "Successfully opened the Widevine Session",
 | 
			
		||||
                {
 | 
			
		||||
                    "session_id": session_id.hex(),
 | 
			
		||||
                    "device": {
 | 
			
		||||
                        "system_id": cdm.system_id,
 | 
			
		||||
                        "security_level": cdm.security_level,
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                http_status=200,
 | 
			
		||||
            )
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Device '{device}' is not found or you are not authorized to use it.",
 | 
			
		||||
            http_status=403,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Error",
 | 
			
		||||
        f"Device '{device}' is not found or you are not authorized to use it.",
 | 
			
		||||
        http_status=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):
 | 
			
		||||
        session_id = bytes.fromhex(session_id)
 | 
			
		||||
        cdm = current_app.config["CDM"]
 | 
			
		||||
        if not cdm:
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'status': 400,
 | 
			
		||||
                'message': f'No CDM for "{device}" has been opened yet. No session to close'
 | 
			
		||||
            }), 400
 | 
			
		||||
        try:
 | 
			
		||||
            cdm.close(session_id)
 | 
			
		||||
        except InvalidSession:
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                '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
 | 
			
		||||
    """Handle the remote device Widevine close."""
 | 
			
		||||
    session_id = bytes.fromhex(session_id)
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
 | 
			
		||||
    try:
 | 
			
		||||
        cdm.close(session_id)
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f'Invalid session ID "{session_id.hex()}", it may have expired',
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        f'Successfully closed Session "{session_id.hex()}".',
 | 
			
		||||
        http_status=200,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route(
 | 
			
		||||
    "/remotecdm/widevine/<device>/set_service_certificate", methods=["POST"]
 | 
			
		||||
)
 | 
			
		||||
def remote_cdm_widevine_set_service_certificate(device):
 | 
			
		||||
    """Handle the remote device Widevine set service certificate."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    for required_field in ("session_id", "certificate"):
 | 
			
		||||
        if required_field == "certificate":
 | 
			
		||||
            has_field = required_field in body  # it needs the key, but can be empty/null
 | 
			
		||||
        else:
 | 
			
		||||
            has_field = body.get(required_field)
 | 
			
		||||
        if not has_field:
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'status': 400,
 | 
			
		||||
                'message': f'Missing required field "{required_field}" in JSON body'
 | 
			
		||||
            }), 400
 | 
			
		||||
    missing_field = check_required_fields(body, ("session_id", "certificate"))
 | 
			
		||||
    if missing_field:
 | 
			
		||||
        return missing_field
 | 
			
		||||
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
 | 
			
		||||
    cdm = current_app.config["CDM"]
 | 
			
		||||
    if not cdm:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'No CDM session for "{device}" has been opened yet. No session to use'
 | 
			
		||||
        }), 400
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
 | 
			
		||||
    certificate = body["certificate"]
 | 
			
		||||
    try:
 | 
			
		||||
        provider_id = cdm.set_service_certificate(session_id, certificate)
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
 | 
			
		||||
        }), 400
 | 
			
		||||
    except DecodeError as error:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid Service Certificate, {error}'
 | 
			
		||||
        }), 400
 | 
			
		||||
    except SignatureMismatch:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': 'Signature Validation failed on the Service Certificate, rejecting'
 | 
			
		||||
        }), 400
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'status': 200,
 | 
			
		||||
        'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
 | 
			
		||||
        'data': {
 | 
			
		||||
            'provider_id': provider_id,
 | 
			
		||||
        }
 | 
			
		||||
    }), 200
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f'Invalid session id: "{session_id.hex()}", it may have expired',
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
 | 
			
		||||
    except DecodeError as error:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid Service Certificate, {error}",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    except SignatureMismatch:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            "Signature Validation failed on the Service Certificate, rejecting",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
 | 
			
		||||
        {
 | 
			
		||||
            "provider_id": provider_id,
 | 
			
		||||
        },
 | 
			
		||||
        http_status=200,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route(
 | 
			
		||||
    "/remotecdm/widevine/<device>/get_service_certificate", methods=["POST"]
 | 
			
		||||
)
 | 
			
		||||
def remote_cdm_widevine_get_service_certificate(device):
 | 
			
		||||
    """Handle the remote device Widevine get service certificate."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    for required_field in ("session_id",):
 | 
			
		||||
        if not body.get(required_field):
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'status': 400,
 | 
			
		||||
                'message': f'Missing required field "{required_field}" in JSON body'
 | 
			
		||||
            }), 400
 | 
			
		||||
    missing_field = check_required_fields(body, ("session_id",))
 | 
			
		||||
    if missing_field:
 | 
			
		||||
        return missing_field
 | 
			
		||||
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
 | 
			
		||||
    cdm = current_app.config["CDM"]
 | 
			
		||||
 | 
			
		||||
    if not cdm:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'No CDM session for "{device}" has been opened yet. No session to use'
 | 
			
		||||
        }), 400
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        service_certificate = cdm.get_service_certificate(session_id)
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
 | 
			
		||||
        }), 400
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f'Invalid Session ID "{session_id.hex()}", it may have expired',
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if service_certificate:
 | 
			
		||||
        service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
 | 
			
		||||
        service_certificate_b64 = base64.b64encode(
 | 
			
		||||
            service_certificate.SerializeToString()
 | 
			
		||||
        ).decode()
 | 
			
		||||
    else:
 | 
			
		||||
        service_certificate_b64 = None
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'status': 200,
 | 
			
		||||
        'message': 'Successfully got the Service Certificate',
 | 
			
		||||
        'data': {
 | 
			
		||||
            'service_certificate': service_certificate_b64,
 | 
			
		||||
        }
 | 
			
		||||
    }), 200
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        "Successfully got the Service Certificate",
 | 
			
		||||
        {"service_certificate": service_certificate_b64},
 | 
			
		||||
        http_status=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):
 | 
			
		||||
    """Handle the remote device Widevine get license challenge."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    for required_field in ("session_id", "init_data"):
 | 
			
		||||
        if not body.get(required_field):
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'status': 400,
 | 
			
		||||
                'message': f'Missing required field "{required_field}" in JSON body'
 | 
			
		||||
            }), 400
 | 
			
		||||
    missing_field = check_required_fields(body, ("session_id", "init_data"))
 | 
			
		||||
    if missing_field:
 | 
			
		||||
        return missing_field
 | 
			
		||||
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
    privacy_mode = body.get("privacy_mode", True)
 | 
			
		||||
    cdm = current_app.config["CDM"]
 | 
			
		||||
    if not cdm:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'No CDM session for "{device}" has been opened yet. No session to use'
 | 
			
		||||
        }), 400
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
 | 
			
		||||
    if current_app.config.get("force_privacy_mode"):
 | 
			
		||||
        privacy_mode = True
 | 
			
		||||
        if not cdm.get_service_certificate(session_id):
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'status': 403,
 | 
			
		||||
                'message': 'No Service Certificate set but Privacy Mode is Enforced.'
 | 
			
		||||
            }), 403
 | 
			
		||||
            return (
 | 
			
		||||
                jsonify(
 | 
			
		||||
                    {
 | 
			
		||||
                        "status": 403,
 | 
			
		||||
                        "message": "No Service Certificate set but Privacy Mode is Enforced.",
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                403,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    current_app.config['pssh'] = body['init_data']
 | 
			
		||||
    init_data = widevinePSSH(body['init_data'])
 | 
			
		||||
    current_app.config["pssh"] = body["init_data"]
 | 
			
		||||
    init_data = widevinePSSH(body["init_data"])
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        license_request = cdm.get_license_challenge(
 | 
			
		||||
            session_id=session_id,
 | 
			
		||||
            pssh=init_data,
 | 
			
		||||
            license_type=license_type,
 | 
			
		||||
            privacy_mode=privacy_mode
 | 
			
		||||
            privacy_mode=privacy_mode,
 | 
			
		||||
        )
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
 | 
			
		||||
        }), 400
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f'Invalid Session ID "{session_id.hex()}", it may have expired',
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    except InvalidInitData as error:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid Init Data, {error}'
 | 
			
		||||
        }), 400
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid Init Data, {error}",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    except InvalidLicenseType:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid License Type {license_type}'
 | 
			
		||||
        }), 400
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'status': 200,
 | 
			
		||||
        'message': 'Success',
 | 
			
		||||
        'data': {
 | 
			
		||||
            'challenge_b64': base64.b64encode(license_request).decode()
 | 
			
		||||
        }
 | 
			
		||||
    }), 200
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid License Type {license_type}",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        "Successfully got the License Challenge",
 | 
			
		||||
        {"challenge_b64": base64.b64encode(license_request).decode()},
 | 
			
		||||
        http_status=200,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
 | 
			
		||||
@remotecdm_wv_bp.route("/remotecdm/widevine/<device>/parse_license", methods=["POST"])
 | 
			
		||||
def remote_cdm_widevine_parse_license(device):
 | 
			
		||||
    """Handle the remote device Widevine parse license."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    for required_field in ("session_id", "license_message"):
 | 
			
		||||
        if not body.get(required_field):
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'status': 400,
 | 
			
		||||
                'message': f'Missing required field "{required_field}" in JSON body'
 | 
			
		||||
            }), 400
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
    cdm = current_app.config["CDM"]
 | 
			
		||||
    if not cdm:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'No CDM session for "{device}" has been opened yet. No session to use'
 | 
			
		||||
        }), 400
 | 
			
		||||
    try:
 | 
			
		||||
        cdm.parse_license(session_id, body['license_message'])
 | 
			
		||||
    except InvalidLicenseMessage as error:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid License Message, {error}'
 | 
			
		||||
        }), 400
 | 
			
		||||
    except InvalidContext as error:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid Context, {error}'
 | 
			
		||||
        }), 400
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
 | 
			
		||||
        }), 400
 | 
			
		||||
    except SignatureMismatch:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Signature Validation failed on the License Message, rejecting.'
 | 
			
		||||
        }), 400
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'status': 200,
 | 
			
		||||
        'message': 'Successfully parsed and loaded the Keys from the License message.',
 | 
			
		||||
    }), 200
 | 
			
		||||
    missing_field = check_required_fields(body, ("session_id", "license_message"))
 | 
			
		||||
    if missing_field:
 | 
			
		||||
        return missing_field
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
 | 
			
		||||
def remote_cdm_widevine_get_keys(device, key_type):
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    for required_field in ("session_id",):
 | 
			
		||||
        if not body.get(required_field):
 | 
			
		||||
            return jsonify({
 | 
			
		||||
                'status': 400,
 | 
			
		||||
                'message': f'Missing required field "{required_field}" in JSON body'
 | 
			
		||||
            }), 400
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
    key_type: Optional[str] = key_type
 | 
			
		||||
    if key_type == 'ALL':
 | 
			
		||||
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        cdm.parse_license(session_id, body["license_message"])
 | 
			
		||||
    except InvalidLicenseMessage as error:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f"Invalid License Message, {error}",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    except InvalidContext as error:
 | 
			
		||||
        return jsonify({"status": 400, "message": f"Invalid Context, {error}"}), 400
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f'Invalid Session ID "{session_id.hex()}", it may have expired',
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    except SignatureMismatch:
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            "Signature Validation failed on the License Message, rejecting.",
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        "Successfully parsed and loaded the Keys from the License message.",
 | 
			
		||||
        http_status=200,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@remotecdm_wv_bp.route(
 | 
			
		||||
    "/remotecdm/widevine/<device>/get_keys/<key_type>", methods=["POST"]
 | 
			
		||||
)
 | 
			
		||||
def remote_cdm_widevine_get_keys(device, key_type):
 | 
			
		||||
    """Handle the remote device Widevine get keys."""
 | 
			
		||||
    body = request.get_json()
 | 
			
		||||
    missing_field = check_required_fields(body, ("session_id",))
 | 
			
		||||
    if missing_field:
 | 
			
		||||
        return missing_field
 | 
			
		||||
 | 
			
		||||
    session_id = bytes.fromhex(body["session_id"])
 | 
			
		||||
    if key_type == "ALL":
 | 
			
		||||
        key_type = None
 | 
			
		||||
    cdm = current_app.config["CDM"]
 | 
			
		||||
    if not cdm:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'No CDM session for "{device}" has been opened yet. No session to use'
 | 
			
		||||
        }), 400
 | 
			
		||||
    cdm = get_cdm_or_error(device)
 | 
			
		||||
    if isinstance(cdm, tuple):  # error response
 | 
			
		||||
        return cdm
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        keys = cdm.get_keys(session_id, key_type)
 | 
			
		||||
    except InvalidSession:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
 | 
			
		||||
        }), 400
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f'Invalid Session ID "{session_id.hex()}", it may have expired',
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
    except ValueError as error:
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'status': 400,
 | 
			
		||||
            'message': f'The Key Type value "{key_type}" is invalid, {error}'
 | 
			
		||||
        }), 400
 | 
			
		||||
        return make_response(
 | 
			
		||||
            "Error",
 | 
			
		||||
            f'The Key Type value "{key_type}" is invalid, {error}',
 | 
			
		||||
            http_status=400,
 | 
			
		||||
        )
 | 
			
		||||
    keys_json = [
 | 
			
		||||
        {
 | 
			
		||||
            "key_id": key.kid.hex,
 | 
			
		||||
            "key": key.key.hex(),
 | 
			
		||||
            "type": key.type,
 | 
			
		||||
            "permissions": key.permissions
 | 
			
		||||
            "permissions": key.permissions,
 | 
			
		||||
        }
 | 
			
		||||
        for key in keys
 | 
			
		||||
        if not key_type or key.type == key_type
 | 
			
		||||
    ]
 | 
			
		||||
    for entry in keys_json:
 | 
			
		||||
        if config['database_type'].lower() != 'mariadb':
 | 
			
		||||
            from custom_functions.database.cache_to_db_sqlite import cache_to_db
 | 
			
		||||
        elif config['database_type'].lower() == 'mariadb':
 | 
			
		||||
            from custom_functions.database.cache_to_db_mariadb import cache_to_db
 | 
			
		||||
        if entry['type'] != 'SIGNING':
 | 
			
		||||
            cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
 | 
			
		||||
        if entry["type"] != "SIGNING":
 | 
			
		||||
            cache_to_db(
 | 
			
		||||
                pssh=str(current_app.config["pssh"]),
 | 
			
		||||
                kid=entry["key_id"],
 | 
			
		||||
                key=entry["key"],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    return jsonify({
 | 
			
		||||
        'status': 200,
 | 
			
		||||
        'message': 'Success',
 | 
			
		||||
        'data': {
 | 
			
		||||
            'keys': keys_json
 | 
			
		||||
        }
 | 
			
		||||
    }), 200
 | 
			
		||||
    return make_response(
 | 
			
		||||
        "Success",
 | 
			
		||||
        "Successfully got the Keys",
 | 
			
		||||
        {"keys": keys_json},
 | 
			
		||||
        http_status=200,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -1,42 +1,57 @@
 | 
			
		||||
from flask import Blueprint, request, jsonify, session
 | 
			
		||||
"""Module to handle the upload process."""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
from flask import Blueprint, request, jsonify, session
 | 
			
		||||
 | 
			
		||||
upload_bp = Blueprint('upload_bp', __name__)
 | 
			
		||||
upload_bp = Blueprint("upload_bp", __name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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):
 | 
			
		||||
    """Handle the upload process."""
 | 
			
		||||
    try:
 | 
			
		||||
        username = session.get('username')
 | 
			
		||||
        username = session.get("username")
 | 
			
		||||
        if not username:
 | 
			
		||||
            return jsonify({'message': 'False', 'error': 'No username in session'}), 400
 | 
			
		||||
            return jsonify({"message": "False", "error": "No username in session"}), 400
 | 
			
		||||
 | 
			
		||||
        safe_username = sanitize_username(username)
 | 
			
		||||
 | 
			
		||||
        # Validate CDM type
 | 
			
		||||
        if cdmtype not in ['PR', 'WV']:
 | 
			
		||||
            return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400
 | 
			
		||||
        if cdmtype not in ["PR", "WV"]:
 | 
			
		||||
            return jsonify({"message": "False", "error": "Invalid CDM type"}), 400
 | 
			
		||||
 | 
			
		||||
        # Set up user directory paths
 | 
			
		||||
        base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
 | 
			
		||||
        pr_path = os.path.join(base_path, 'PR')
 | 
			
		||||
        wv_path = os.path.join(base_path, 'WV')
 | 
			
		||||
        base_path = os.path.join(
 | 
			
		||||
            os.getcwd(), "configs", "CDMs", "users_uploaded", safe_username
 | 
			
		||||
        )
 | 
			
		||||
        pr_path = os.path.join(base_path, "PR")
 | 
			
		||||
        wv_path = os.path.join(base_path, "WV")
 | 
			
		||||
 | 
			
		||||
        # Create necessary directories if they don't exist
 | 
			
		||||
        os.makedirs(pr_path, exist_ok=True)
 | 
			
		||||
        os.makedirs(wv_path, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
        # Get uploaded file
 | 
			
		||||
        uploaded_file = request.files.get('file')
 | 
			
		||||
        uploaded_file = request.files.get("file")
 | 
			
		||||
        if not uploaded_file:
 | 
			
		||||
            return jsonify({'message': 'False', 'error': 'No file provided'}), 400
 | 
			
		||||
            return jsonify({"message": "False", "error": "No file provided"}), 400
 | 
			
		||||
 | 
			
		||||
        # Determine correct save path based on cdmtype
 | 
			
		||||
        filename = uploaded_file.filename
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
        return jsonify({'message': 'Success', 'file_saved_to': save_path})
 | 
			
		||||
        return jsonify({"message": "Success", "file_saved_to": save_path})
 | 
			
		||||
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logging.exception("Upload failed")
 | 
			
		||||
        return jsonify({'message': 'False', 'error': 'Server error'}), 500
 | 
			
		||||
    except (OSError, IOError, ValueError, AttributeError) as e:
 | 
			
		||||
        logging.exception("Upload failed: %s", {e})
 | 
			
		||||
        return jsonify({"message": "False", "error": "Server error"}), 500
 | 
			
		||||
 | 
			
		||||
@ -1,54 +1,65 @@
 | 
			
		||||
"""Module to handle the user changes."""
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from flask import Blueprint, request, jsonify, session
 | 
			
		||||
from custom_functions.database.user_db import change_password, change_api_key
 | 
			
		||||
 | 
			
		||||
user_change_bp = Blueprint('user_change_bp', __name__)
 | 
			
		||||
user_change_bp = Blueprint("user_change_bp", __name__)
 | 
			
		||||
 | 
			
		||||
# Define allowed characters regex (no spaces allowed)
 | 
			
		||||
PASSWORD_REGEX = re.compile(r'^[A-Za-z0-9!@#$%^&*()_+\-=\[\]{};\'":\\|,.<>\/?`~]+$')
 | 
			
		||||
 | 
			
		||||
@user_change_bp.route('/user/change_password', methods=['POST'])
 | 
			
		||||
 | 
			
		||||
@user_change_bp.route("/user/change_password", methods=["POST"])
 | 
			
		||||
def change_password_route():
 | 
			
		||||
    username = session.get('username')
 | 
			
		||||
    """Handle the change password route."""
 | 
			
		||||
    username = session.get("username")
 | 
			
		||||
    if not username:
 | 
			
		||||
        return jsonify({'message': 'False'}), 400
 | 
			
		||||
        return jsonify({"message": "False"}), 400
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        data = request.get_json()
 | 
			
		||||
        new_password = data.get('new_password', '')
 | 
			
		||||
        new_password = data.get("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)
 | 
			
		||||
        return jsonify({'message': 'True'}), 200
 | 
			
		||||
        return jsonify({"message": "True"}), 200
 | 
			
		||||
 | 
			
		||||
    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():
 | 
			
		||||
    """Handle the change API key route."""
 | 
			
		||||
    # Ensure the user is logged in by checking session for 'username'
 | 
			
		||||
    username = session.get('username')
 | 
			
		||||
    username = session.get("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
 | 
			
		||||
    new_api_key = request.json.get('new_api_key')
 | 
			
		||||
    new_api_key = request.get_json().get("new_api_key")
 | 
			
		||||
 | 
			
		||||
    if not new_api_key:
 | 
			
		||||
        return jsonify({'message': 'False', 'error': 'New API key not provided'}), 400
 | 
			
		||||
        return jsonify({"message": "False", "error": "New API key not provided"}), 400
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        # Call the function to update the API key in the database
 | 
			
		||||
        success = change_api_key(username=username, new_api_key=new_api_key)
 | 
			
		||||
 | 
			
		||||
        if success:
 | 
			
		||||
            return jsonify({'message': 'True', 'success': 'API key changed successfully'}), 200
 | 
			
		||||
            return (
 | 
			
		||||
                jsonify({"message": "True", "success": "API key changed successfully"}),
 | 
			
		||||
                200,
 | 
			
		||||
            )
 | 
			
		||||
        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:
 | 
			
		||||
        # 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 glob
 | 
			
		||||
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():
 | 
			
		||||
    username = session.get('username')
 | 
			
		||||
    """Handle the user info request."""
 | 
			
		||||
    username = session.get("username")
 | 
			
		||||
    if not username:
 | 
			
		||||
        try:
 | 
			
		||||
            headers = request.headers
 | 
			
		||||
            api_key = headers['Api-Key']
 | 
			
		||||
            api_key = headers["Api-Key"]
 | 
			
		||||
            username = fetch_username_by_api_key(api_key)
 | 
			
		||||
        except:
 | 
			
		||||
            return jsonify({'message': 'False'}), 400
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logging.exception("Error retrieving username by API key, %s", {e})
 | 
			
		||||
            return jsonify({"message": "False"}), 400
 | 
			
		||||
 | 
			
		||||
    safe_username = sanitize_username(username)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username.lower())
 | 
			
		||||
        pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
 | 
			
		||||
        wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
 | 
			
		||||
        base_path = os.path.join(
 | 
			
		||||
            os.getcwd(), "configs", "CDMs", "users_uploaded", safe_username
 | 
			
		||||
        )
 | 
			
		||||
        pr_files = [
 | 
			
		||||
            os.path.basename(f)
 | 
			
		||||
            for f in glob.glob(os.path.join(base_path, "PR", "*.prd"))
 | 
			
		||||
        ]
 | 
			
		||||
        wv_files = [
 | 
			
		||||
            os.path.basename(f)
 | 
			
		||||
            for f in glob.glob(os.path.join(base_path, "WV", "*.wvd"))
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        return jsonify({
 | 
			
		||||
            'Username': username,
 | 
			
		||||
            'Widevine_Devices': wv_files,
 | 
			
		||||
            'Playready_Devices': pr_files,
 | 
			
		||||
            'API_Key': fetch_api_key(username),
 | 
			
		||||
            'Styled_Username': fetch_styled_username(username)
 | 
			
		||||
        })
 | 
			
		||||
        return jsonify(
 | 
			
		||||
            {
 | 
			
		||||
                "Username": username,
 | 
			
		||||
                "Widevine_Devices": wv_files,
 | 
			
		||||
                "Playready_Devices": pr_files,
 | 
			
		||||
                "API_Key": fetch_api_key(username),
 | 
			
		||||
                "Styled_Username": fetch_styled_username(username),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logging.exception("Error retrieving device files")
 | 
			
		||||
        return jsonify({'message': 'False'}), 500
 | 
			
		||||
        logging.exception("Error retrieving device files, %s", {e})
 | 
			
		||||
        return jsonify({"message": "False"}), 500
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user