Compare commits

..

No commits in common. "main" and "2.3" have entirely different histories.
main ... 2.3

39 changed files with 518 additions and 2039 deletions

View File

@ -30,5 +30,5 @@
- Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder - Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder
- Install python dependencies `pip install -r requirements.txt` - 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 `/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` - (Optional) Create the folder structur `/config/CDMs/PR` and place your .PRD file into `/configs/CDMs/PR`
- Run the application with `python main.py` - Run the application with `python main.py`

View File

@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
dist
dist-ssr dist-ssr
*.local *.local

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@ -9,7 +9,6 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"axios": "^1.9.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
@ -1670,23 +1669,6 @@
"dev": true, "dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1738,19 +1720,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -1819,18 +1788,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1901,15 +1858,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -1919,20 +1867,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.143", "version": "1.5.143",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.143.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.143.tgz",
@ -1959,51 +1893,6 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.3", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
@ -2331,41 +2220,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2380,15 +2234,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -2399,43 +2244,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -2462,18 +2270,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -2490,45 +2286,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -2968,36 +2725,6 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -3209,12 +2936,6 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@ -11,7 +11,6 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"axios": "^1.9.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",

View File

@ -6,7 +6,6 @@ import TestPlayer from "./components/Pages/TestPlayer";
import NavBar from "./components/NavBar"; import NavBar from "./components/NavBar";
import NavBarMain from "./components/NavBarMain"; import NavBarMain from "./components/NavBarMain";
import SideMenu from "./components/SideMenu"; // Add this import import SideMenu from "./components/SideMenu"; // Add this import
import Account from "./components/Pages/Account";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
function App() { function App() {
@ -17,7 +16,7 @@ function App() {
{/* The SideMenu should be visible when isMenuOpen is true */} {/* The SideMenu should be visible when isMenuOpen is true */}
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} /> <SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
<div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0"> <div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 0">
<NavBar /> <NavBar />
</div> </div>
@ -32,7 +31,6 @@ function App() {
<Route path="/cache" element={<Cache />} /> <Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} /> <Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} /> <Route path="/testplayer" element={<TestPlayer />} />
<Route path="/account" element={<Account />} />
</Routes> </Routes>
</div> </div>
</div> </div>

View File

@ -1,13 +0,0 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" fill="#000000">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier">
<defs>
<style>.cls-1{fill:none;stroke:#ffffff;stroke-miterlimit:10;stroke-width:1.91px;}</style>
</defs>
<circle class="cls-1" cx="12" cy="7.25" r="5.73"/>
<path class="cls-1" d="M1.5,23.48l.37-2.05A10.3,10.3,0,0,1,12,13h0a10.3,10.3,0,0,1,10.13,8.45l.37,2.05"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 760 B

View File

@ -1,159 +1,112 @@
import { useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import homeIcon from '../assets/icons/home.svg'; import homeIcon from '../assets/icons/home.svg';
import cacheIcon from '../assets/icons/cache.svg'; import cacheIcon from '../assets/icons/cache.svg';
import apiIcon from '../assets/icons/api.svg'; import apiIcon from '../assets/icons/api.svg';
import testPlayerIcon from '../assets/icons/testplayer.svg'; import testPlayerIcon from '../assets/icons/testplayer.svg';
import accountIcon from '../assets/icons/account.svg';
import discordIcon from '../assets/icons/discord.svg'; import discordIcon from '../assets/icons/discord.svg';
import telegramIcon from '../assets/icons/telegram.svg'; import telegramIcon from '../assets/icons/telegram.svg';
import giteaIcon from '../assets/icons/gitea.svg'; import giteaIcon from '../assets/icons/gitea.svg';
function NavBar() { function NavBar() {
const [externalLinks, setExternalLinks] = useState({
discord: '#',
telegram: '#',
gitea: '#',
});
useEffect(() => {
fetch('/api/links')
.then(response => response.json())
.then(data => setExternalLinks(data))
.catch(error => console.error('Error fetching links:', error));
}, []);
return ( return (
<div className="flex flex-col w-full h-full bg-white/1"> <div className="flex flex-col w-full h-full bg-white/1">
{/* Header */}
<div> <div>
<p className="text-white text-2xl font-bold p-3 text-center mb-5"> <p className='text-white text-2xl font-bold p-3 text-center mb-5'>
<a href="/">CDRM-Project</a> <a href='/'>
CDRM-Project
</a>
</p> </p>
</div> </div>
<div className='overflow-y-auto grow'>
{/* Scrollable navigation area */}
<div className="overflow-y-auto grow flex flex-col">
{/* Main NavLinks */}
<NavLink <NavLink
to="/" to='/'
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `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'}`
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"> <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" /> <img src={homeIcon} alt="Home" className='w-1/2 cursor-pointer' />
</button> </button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start"> <p className='grow text-white md:text-2xl font-bold flex items-center justify-start'>
Home Home
</p> </p>
</NavLink> </NavLink>
<NavLink <NavLink
to="/cache" to='/cache'
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `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'}`
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"> <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" /> <img src={cacheIcon} alt="Cache" className='w-1/2 cursor-pointer' />
</button> </button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start"> <p className='grow text-white md:text-2xl font-bold flex items-center justify-start'>
Cache Cache
</p> </p>
</NavLink> </NavLink>
<NavLink <NavLink
to="/api" to='/api'
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `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'}`
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"> <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" /> <img src={apiIcon} alt="API" className='w-1/2 cursor-pointer' />
</button> </button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start"> <p className='grow text-white md:text-2xl font-bold flex items-center justify-start'>
API API
</p> </p>
</NavLink> </NavLink>
<NavLink <NavLink
to="/testplayer" to='/testplayer'
className={({ isActive }) => className={({ isActive }) =>
`flex flex-row p-3 border-l-3 ${ `flex flex-row p-3 border-l-3 ${isActive ? 'border-l-rose-700/50 bg-black/50' : 'hover:border-l-rose-700/50 hover:bg-white/5'}`
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"> <button className='w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer'>
<img src={testPlayerIcon} alt="Test Player" className="w-1/2 cursor-pointer" /> <img src={testPlayerIcon} alt="Test Player" className='w-1/2 cursor-pointer' />
</button> </button>
<p className="grow text-white md:text-2xl font-bold flex items-center justify-start"> <p className='grow text-white md:text-2xl font-bold flex items-center justify-start'>
Test Player Test Player
</p> </p>
</NavLink> </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">
My Account
</p>
</NavLink>
</div>
</div> </div>
<div className='flex flex-row w-full h-16 self-end bg-black/25'>
{/* External links at very bottom */}
<div className="flex flex-row w-full h-16 bg-black/25">
<a <a
href={externalLinks.discord} href='https://discord.cdrm-project.com'
target="_blank" target='_blank'
rel="noopener noreferrer" rel='noopener noreferrer'
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group" className='w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-950 group'
> >
<img src={discordIcon} alt="Discord" className="w-1/2 group-hover:animate-bounce" /> <img
src={discordIcon}
alt="Discord"
className='w-1/2 cursor-pointer group-hover:animate-bounce'
/>
</a> </a>
<a <a
href={externalLinks.telegram} href='https://telegram.cdrm-project.com'
target="_blank" target='_blank'
rel="noopener noreferrer" rel='noopener noreferrer'
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group" className='w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-400 group'
> >
<img src={telegramIcon} alt="Telegram" className="w-1/2 group-hover:animate-bounce" /> <img
src={telegramIcon}
alt="Telegram"
className='w-1/2 cursor-pointer group-hover:animate-bounce'
/>
</a> </a>
<a <a
href={externalLinks.gitea} href='https://cdm-project.com/tpd94/cdrm-project'
target="_blank" target='_blank'
rel="noopener noreferrer" rel='noopener noreferrer'
className="w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer hover:bg-green-700 group" 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" /> <img
src={giteaIcon}
alt="Gitea"
className='w-1/2 cursor-pointer group-hover:animate-bounce'
/>
</a> </a>
</div> </div>
</div> </div>

View File

@ -104,7 +104,7 @@ print(requests.post(
</details> </details>
<details open className='w-full list-none mt-5'> <details open className='w-full list-none mt-5'>
<summary className='text-2xl'>PyWidevine RemoteCDM info</summary> <summary className='text-2xl'>PyWidevine RemoteCDM info</summary>
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg overflow-x-auto'> <div className='mt-5 border-2 border-indigo-500 p-5 rounded-lg overflow-x-auto'>
<p> <p>
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br /> <strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
<strong>System ID:</strong> {deviceInfo.system_id}<br /> <strong>System ID:</strong> {deviceInfo.system_id}<br />
@ -117,7 +117,7 @@ print(requests.post(
</details> </details>
<details open className='w-full list-none mt-5'> <details open className='w-full list-none mt-5'>
<summary className='text-2xl'>PyPlayready RemoteCDM info</summary> <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'> <div className='mt-5 border-2 border-indigo-500 p-5 rounded-lg overflow-x-auto'>
<p> <p>
<strong>Security Level:</strong> {prDeviceInfo.security_level}<br /> <strong>Security Level:</strong> {prDeviceInfo.security_level}<br />
<strong>Host:</strong> {fullHost}/remotecdm/playready<br /> <strong>Host:</strong> {fullHost}/remotecdm/playready<br />

View File

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

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { readTextFromClipboard } from '../Functions/ParseChallenge'; import { readTextFromClipboard } from '../Functions/ParseChallenge'
import { Helmet } from 'react-helmet'; // Import Helmet import { Helmet } from 'react-helmet'; // Import Helmet
function HomePage() { function HomePage() {
@ -11,8 +11,6 @@ function HomePage() {
const [data, setData] = useState(''); const [data, setData] = useState('');
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState('default');
const bottomRef = useRef(null); const bottomRef = useRef(null);
const messageRef = useRef(null); // Reference to result container const messageRef = useRef(null); // Reference to result container
@ -43,8 +41,7 @@ function HomePage() {
proxy: proxy, proxy: proxy,
headers: headers, headers: headers,
cookies: cookies, cookies: cookies,
data: data, data: data
device: selectedDevice, // Include selected device in the request
}), }),
}) })
.then(response => response.json()) .then(response => response.json())
@ -71,6 +68,7 @@ function HomePage() {
} }
}; };
const handleFetchPaste = () => { const handleFetchPaste = () => {
event.preventDefault(); event.preventDefault();
readTextFromClipboard().then(() => { readTextFromClipboard().then(() => {
@ -81,7 +79,7 @@ function HomePage() {
}).catch(err => { }).catch(err => {
alert('Failed to paste from fetch!'); alert('Failed to paste from fetch!');
}); });
}; }
useEffect(() => { useEffect(() => {
if (isVisible && bottomRef.current) { if (isVisible && bottomRef.current) {
@ -89,43 +87,6 @@ function HomePage() {
} }
}, [message, isVisible]); }, [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');
}
})
.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 ( return (
<> <>
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full"> <div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
@ -179,23 +140,6 @@ function HomePage() {
onChange={(e) => setData(e.target.value)} onChange={(e) => setData(e.target.value)}
/> />
{/* Device Selection Dropdown, only show if logged in */}
{devices.length > 0 && (
<>
<label htmlFor="device" className="text-white w-8/10 self-center">Select Device:</label>
<select
id="device"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
value={selectedDevice}
onChange={(e) => setSelectedDevice(e.target.value)}
>
{devices.map((device, index) => (
<option key={index} value={device}>{device}</option>
))}
</select>
</>
)}
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch"> <div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button <button
type="button" type="button"

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
data = {
'discord': 'https://discord.cdrm-project.com/',
'telegram': 'https://telegram.cdrm-project.com/',
'gitea': 'https://cdm-project.com/tpd94/cdrm-project'
}

View File

@ -34,14 +34,5 @@ tags = {
'opengraph_image': 'https://cdrm-project.com/og-api.jpg', 'opengraph_image': 'https://cdrm-project.com/og-api.jpg',
'opengraph_url': 'https://cdrm-project.com/api', 'opengraph_url': 'https://cdrm-project.com/api',
'tab_title': 'API', 'tab_title': 'API',
},
'account': {
'description': 'Account for CDRM-Project',
'keywords': 'Login, CDRM, CDM, CDRM-Project, register, account',
'opengraph_title': 'My account',
'opengraph_description': 'Account for CDRM-Project',
'opengraph_image': 'https://cdrm-project.com/og-home.jpg',
'opengraph_url': 'https://cdrm-project.com/account',
'tab_title': 'My account',
} }
} }

View File

@ -1,100 +0,0 @@
import sqlite3
import os
import bcrypt
def create_user_database():
os.makedirs(f'{os.getcwd()}/databases/sql', exist_ok=True)
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
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())
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
try:
cursor.execute('INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)', (username.lower(), hashed_pw, username, api_key))
conn.commit()
return True
except sqlite3.IntegrityError:
return False
def verify_user(username, password):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),))
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
def fetch_api_key(username):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT API_Key FROM user_info WHERE Username = ?', (username.lower(),))
result = cursor.fetchone()
if result:
return result[0]
else:
return None
def change_password(username, new_password):
# Hash the new password
new_hashed_pw = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
# Update the password in the database
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('UPDATE user_info SET Password = ? WHERE Username = ?', (new_hashed_pw, username.lower()))
conn.commit()
return True
def change_api_key(username, new_api_key):
# Update the API key in the database
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('UPDATE user_info SET API_Key = ? WHERE Username = ?', (new_api_key, username.lower()))
conn.commit()
return True
def fetch_styled_username(username):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT Styled_Username FROM user_info WHERE Username = ?', (username.lower(),))
result = cursor.fetchone()
if result:
return result[0]
else:
return None
def fetch_username_by_api_key(api_key):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT Username FROM user_info WHERE API_Key = ?', (api_key,))
result = cursor.fetchone()
if result:
return result[0] # Return the username
else:
return None # If no user is found for the API key

View File

@ -84,8 +84,7 @@ def is_url_and_split(input_str):
else: 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): def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, headers: str = None, cookies: str = None, json_data: str = None):
print(f'Using device {device} for user {username}')
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if config['database_type'].lower() == 'sqlite': if config['database_type'].lower() == 'sqlite':
@ -107,34 +106,19 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
'message': f'An error occurred processing PSSH\n\n{error}' 'message': f'An error occurred processing PSSH\n\n{error}'
} }
try: try:
if device == 'public': base_name = config["default_pr_cdm"]
base_name = config["default_pr_cdm"] if not base_name.endswith(".prd"):
if not base_name.endswith(".prd"): base_name += ".prd"
base_name += ".prd" prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
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: else:
base_name = device prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}')
if not base_name.endswith(".prd"): if prd_files:
base_name += ".prd" pr_device = playreadyDevice.load(prd_files[0])
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}') else:
else: return {
prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}') 'status': 'error',
if prd_files: 'message': 'No default .prd file found'
pr_device = playreadyDevice.load(prd_files[0]) }
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
}
except Exception as error: except Exception as error:
return { return {
'status': 'error', 'status': 'error',
@ -282,34 +266,19 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea
'message': f'An error occurred processing PSSH\n\n{error}' 'message': f'An error occurred processing PSSH\n\n{error}'
} }
try: try:
if device == 'public': base_name = config["default_wv_cdm"]
base_name = config["default_wv_cdm"] if not base_name.endswith(".wvd"):
if not base_name.endswith(".wvd"): base_name += ".wvd"
base_name += ".wvd" wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
else:
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
if wvd_files:
wv_device = widevineDevice.load(wvd_files[0])
else:
return {
'status': 'error',
'message': 'No default .wvd file found'
}
else: else:
base_name = device wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}')
if not base_name.endswith(".wvd"): if wvd_files:
base_name += ".wvd" wv_device = widevineDevice.load(wvd_files[0])
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}') else:
else: return {
wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}') 'status': 'error',
if wvd_files: 'message': 'No default .wvd file found'
wv_device = widevineDevice.load(wvd_files[0]) }
else:
return {
'status': 'error',
'message': f'{base_name} does not exist'
}
except Exception as error: except Exception as error:
return { return {
'status': 'error', 'status': 'error',

View File

@ -7,7 +7,6 @@ def check_for_config_file():
default_config = """\ default_config = """\
default_wv_cdm: '' default_wv_cdm: ''
default_pr_cdm: '' default_pr_cdm: ''
secret_key_flask: 'secretkey'
# change the type to mariadb to use mariadb below # change the type to mariadb to use mariadb below
database_type: 'sqlite' database_type: 'sqlite'
fqdn: '' fqdn: ''

View File

@ -14,13 +14,6 @@ def check_for_sqlite_database():
else: else:
return return
def check_for_user_database():
if os.path.exists(f'{os.getcwd()}/databases/users.db'):
return
else:
from custom_functions.database.user_db import create_user_database
create_user_database()
def check_for_mariadb_database(): def check_for_mariadb_database():
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
@ -34,4 +27,3 @@ def check_for_mariadb_database():
def check_for_sql_database(): def check_for_sql_database():
check_for_sqlite_database() check_for_sqlite_database()
check_for_mariadb_database() check_for_mariadb_database()
check_for_user_database()

View File

@ -1,17 +0,0 @@
import os
import glob
def user_allowed_to_use_device(device, username):
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
# Get filenames with extensions
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
# Combine all filenames
all_files = pr_files + wv_files
# 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)

16
main.py
View File

@ -8,30 +8,16 @@ from routes.react import react_bp
from routes.api import api_bp from routes.api import api_bp
from routes.remote_device_wv import remotecdm_wv_bp from routes.remote_device_wv import remotecdm_wv_bp
from routes.remote_device_pr import remotecdm_pr_bp from routes.remote_device_pr import remotecdm_pr_bp
from routes.upload import upload_bp
from routes.user_info import user_info_bp
from routes.register import register_bp
from routes.login import login_bp
from routes.user_changes import user_change_bp
import os
import yaml
app = Flask(__name__) app = Flask(__name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
config = yaml.safe_load(file)
app.secret_key = config['secret_key_flask']
CORS(app) CORS(app)
# Register the blueprint # Register the blueprint
app.register_blueprint(react_bp) app.register_blueprint(react_bp)
app.register_blueprint(api_bp) app.register_blueprint(api_bp)
app.register_blueprint(register_bp)
app.register_blueprint(login_bp)
app.register_blueprint(user_info_bp)
app.register_blueprint(upload_bp)
app.register_blueprint(remotecdm_wv_bp) app.register_blueprint(remotecdm_wv_bp)
app.register_blueprint(remotecdm_pr_bp) app.register_blueprint(remotecdm_pr_bp)
app.register_blueprint(user_change_bp)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0') app.run(debug=True, host='0.0.0.0')

View File

@ -6,4 +6,3 @@ requests~=2.32.3
protobuf~=4.25.6 protobuf~=4.25.6
PyYAML~=6.0.2 PyYAML~=6.0.2
mysql-connector-python mysql-connector-python
bcrypt

View File

@ -1,9 +1,8 @@
import os import os
import sqlite3 import sqlite3
from flask import Blueprint, jsonify, request, send_file, session from flask import Blueprint, jsonify, request, send_file
import json import json
from custom_functions.decrypt.api_decrypt import api_decrypt from custom_functions.decrypt.api_decrypt import api_decrypt
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
import shutil import shutil
import math import math
import yaml import yaml
@ -11,7 +10,6 @@ import mysql.connector
from io import StringIO from io import StringIO
import tempfile import tempfile
import time import time
from configs.icon_links import data as icon_data
api_bp = Blueprint('api', __name__) api_bp = Blueprint('api', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
@ -220,27 +218,12 @@ def decrypt_data():
api_request_cookies = None api_request_cookies = None
if 'data' in api_request_data: if 'data' in api_request_data:
if api_request_data['data'] == '': if api_request_data['data'] == '':
api_request_data_func = None api_request_data = None
else: else:
api_request_data_func = api_request_data['data'] api_request_data = 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: else:
api_request_device = 'public' api_request_data = None
username = None 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)
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': if result['status'] == 'success':
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
@ -251,17 +234,3 @@ def decrypt_data():
'status': 'fail', 'status': 'fail',
'message': result['message'] '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'],
})
@api_bp.route('/api/extension', methods=['POST'])
def verify_extension():
return jsonify({
'status': True,
})

View File

@ -1,37 +0,0 @@
from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import verify_user
login_bp = Blueprint(
'login_bp',
__name__,
)
@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
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
@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'})
@login_bp.route('/logout', methods=['POST'])
def logout():
session.pop('username', None)
return jsonify({'message': 'Successfully logged out!'})

View File

@ -26,7 +26,7 @@ def index(path=''):
file_path = os.path.join(react_bp.static_folder, path) file_path = os.path.join(react_bp.static_folder, path)
if path != "" and os.path.exists(file_path): if path != "" and os.path.exists(file_path):
return send_from_directory(react_bp.static_folder, path) return send_from_directory(react_bp.static_folder, path)
elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']: elif path.lower() in ['', 'cache', 'api', 'testplayer']:
data = index_tags.tags.get(path.lower(), index_tags.tags['index']) data = index_tags.tags.get(path.lower(), index_tags.tags['index'])
return render_template('index.html', data=data) return render_template('index.html', data=data)
else: else:

View File

@ -1,42 +0,0 @@
import re
from flask import Blueprint, request, jsonify
from custom_functions.database.user_db import add_user
import uuid
register_bp = Blueprint('register_bp', __name__)
USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$')
PASSWORD_REGEX = re.compile(r'^\S+$')
@register_bp.route('/register', methods=['POST'])
def register():
if request.method != 'POST':
return jsonify({'error': 'Method not supported'}), 405
data = request.get_json()
# Check required fields
for required_field in ['username', 'password']:
if required_field not in data:
return jsonify({'error': f'Missing required field: {required_field}'}), 400
username = data['username']
password = data['password']
api_key = str(uuid.uuid4())
# Validate username and password
if not USERNAME_REGEX.fullmatch(username):
return jsonify({
'error': 'Invalid username. Only letters, numbers, hyphens, and underscores are allowed.'
}), 400
if not PASSWORD_REGEX.fullmatch(password):
return jsonify({
'error': 'Invalid password. Spaces are not allowed.'
}), 400
# Attempt to add user
if add_user(username, password, api_key):
return jsonify({'message': 'User successfully registered!'}), 201
else:
return jsonify({'error': 'User already exists!'}), 409

View File

@ -1,5 +1,3 @@
import base64
from flask import Blueprint, jsonify, request, current_app, Response from flask import Blueprint, jsonify, request, current_app, Response
import os import os
import yaml import yaml
@ -7,11 +5,6 @@ from pyplayready.device import Device as PlayReadyDevice
from pyplayready.cdm import Cdm as PlayReadyCDM from pyplayready.cdm import Cdm as PlayReadyCDM
from pyplayready import PSSH as PlayReadyPSSH from pyplayready import PSSH as PlayReadyPSSH
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh) from pyplayready.exceptions import (InvalidSession, TooManySessions, 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
@ -42,24 +35,9 @@ def remote_cdm_playready_deviceinfo():
'security_level': cdm.security_level, 'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/playready', 'host': f'{config["fqdn"]}/remotecdm/playready',
'secret': f'{config["remote_cdm_secret"]}', 'secret': f'{config["remote_cdm_secret"]}',
'device_name': Path(base_name).stem 'device_name': f'{base_name}'
}) })
@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
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', methods=['GET']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/open', methods=['GET'])
def remote_cdm_playready_open(device): def remote_cdm_playready_open(device):
if str(device).lower() == config['default_pr_cdm'].lower(): if str(device).lower() == config['default_pr_cdm'].lower():
@ -75,171 +53,148 @@ def remote_cdm_playready_open(device):
} }
} }
}) })
if request.headers['X-Secret-Key'] and str(device).lower() != config['default_pr_cdm'].lower():
api_key = request.headers['X-Secret-Key']
user = fetch_username_by_api_key(api_key=api_key)
if user:
if user_allowed_to_use_device(device=device, username=user):
pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/PR/{device}.prd')
cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device)
session_id = cdm.open()
return jsonify({
'message': 'Success',
'data': {
'session_id': session_id.hex(),
'device': {
'security_level': cdm.security_level
}
}
})
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
}), 403
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
}), 403
else:
return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.",
}), 403
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_playready_close(device, session_id): def remote_cdm_playready_close(device, session_id):
try: if str(device).lower() == config['default_pr_cdm'].lower():
session_id = bytes.fromhex(session_id) session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close' 'message': f'No CDM for "{device}" has been opened yet. No session to close'
}), 400 })
try: try:
cdm.close(session_id) cdm.close(session_id)
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify({
'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired' 'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
}), 400 })
return jsonify({ return jsonify({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".', 'message': f'Successfully closed Session "{session_id.hex()}".',
}), 200 })
except Exception as e: else:
return jsonify({ return jsonify({
'message': f'Failed to close Session "{session_id.hex()}".' 'status': 400,
}), 400 'message': f'Unauthorized'
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
def remote_cdm_playready_get_license_challenge(device): def remote_cdm_playready_get_license_challenge(device):
body = request.get_json() if str(device).lower() == config['default_pr_cdm'].lower():
for required_field in ("session_id", "init_data"): body = request.get_json()
if not body.get(required_field): for required_field in ("session_id", "init_data"):
return jsonify({ if not body.get(required_field):
'message': f'Missing required field "{required_field}" in JSON body' return jsonify({
}), 400 'status': 400,
cdm = current_app.config["CDM"] 'message': f'Missing required field "{required_field}" in JSON body'
session_id = bytes.fromhex(body["session_id"]) })
init_data = body["init_data"] cdm = current_app.config["CDM"]
if not init_data.startswith("<WRMHEADER"): session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"):
try:
pssh = PlayReadyPSSH(init_data)
if pssh.wrm_headers:
init_data = pssh.wrm_headers[0]
except InvalidPssh as e:
return jsonify({
'message': f'Unable to parse base64 PSSH, {e}'
})
try: try:
pssh = PlayReadyPSSH(init_data) license_request = cdm.get_license_challenge(
if pssh.wrm_headers: session_id=session_id,
init_data = pssh.wrm_headers[0] wrm_header=init_data
except InvalidPssh as e: )
except InvalidSession:
return jsonify({ return jsonify({
'message': f'Unable to parse base64 PSSH, {e}' 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f'Error, {e}'
}) })
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
wrm_header=init_data
)
except InvalidSession:
return jsonify({ return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired." 'message': 'success',
'data': {
'challenge': license_request
}
}) })
except Exception as e:
return jsonify({
'message': f'Error, {e}'
})
return jsonify({
'message': 'success',
'data': {
'challenge': license_request
}
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST'])
def remote_cdm_playready_parse_license(device): def remote_cdm_playready_parse_license(device):
body = request.get_json() if str(device).lower() == config['default_pr_cdm'].lower():
for required_field in ("license_message", "session_id"): body = request.get_json()
if not body.get(required_field): 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({ return jsonify({
'message': f'Missing required field "{required_field}" in JSON body' 'message': f"No Cdm session for {device} has been opened yet. No session to use."
})
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except InvalidLicense as e:
return jsonify({
'message': f"Invalid License, {e}"
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
}) })
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({ return jsonify({
'message': f"No Cdm session for {device} has been opened yet. No session to use." 'message': 'Successfully parsed and loaded the Keys from the License message'
}) })
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
if is_base64(license_message):
license_message = base64.b64decode(license_message).decode("utf-8")
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except InvalidLicense as e:
return jsonify({
'message': f"Invalid License, {e}"
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
return jsonify({
'message': 'Successfully parsed and loaded the Keys from the License message'
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
def remote_cdm_playready_get_keys(device): def remote_cdm_playready_get_keys(device):
body = request.get_json() if str(device).lower() == config['default_pr_cdm'].lower():
for required_field in ("session_id",): body = request.get_json()
if not body.get(required_field): for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({ return jsonify({
'message': f'Missing required field "{required_field}" in JSON body' 'message': f"Missing required field '{required_field}' in JSON body."
}) })
session_id = bytes.fromhex(body["session_id"]) try:
cdm = current_app.config["CDM"] keys = cdm.get_keys(session_id)
if not cdm: except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
keys_json = [
{
"key_id": key.key_id.hex,
"key": key.key.hex(),
"type": key.key_type.value,
"cipher_type": key.cipher_type.value,
"key_length": key.key_length,
}
for key in keys
]
return jsonify({ return jsonify({
'message': f"Missing required field '{required_field}' in JSON body." 'message': 'success',
'data': {
'keys': keys_json
}
}) })
try:
keys = cdm.get_keys(session_id)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
keys_json = [
{
"key_id": key.key_id.hex,
"key": key.key.hex(),
"type": key.key_type.value,
"cipher_type": key.cipher_type.value,
"key_length": key.key_length,
}
for key in keys
]
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})

View File

@ -11,9 +11,6 @@ from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicen
InvalidSession, SignatureMismatch, TooManySessions) InvalidSession, SignatureMismatch, TooManySessions)
import yaml import yaml
from custom_functions.database.user_db import fetch_api_key, fetch_username_by_api_key
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
from pathlib import Path
remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__) remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
@ -36,8 +33,8 @@ def remote_cdm_widevine_deviceinfo():
if request.method == 'GET': if request.method == 'GET':
base_name = config["default_wv_cdm"] base_name = config["default_wv_cdm"]
if not base_name.endswith(".wvd"): if not base_name.endswith(".wvd"):
base_name = (base_name + ".wvd") full_file_name = (base_name + ".wvd")
device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{base_name}') device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{full_file_name}')
cdm = widevineCDM.from_device(device) cdm = widevineCDM.from_device(device)
return jsonify({ return jsonify({
'device_type': cdm.device_type.name, 'device_type': cdm.device_type.name,
@ -45,24 +42,7 @@ def remote_cdm_widevine_deviceinfo():
'security_level': cdm.security_level, 'security_level': cdm.security_level,
'host': f'{config["fqdn"]}/remotecdm/widevine', 'host': f'{config["fqdn"]}/remotecdm/widevine',
'secret': f'{config["remote_cdm_secret"]}', 'secret': f'{config["remote_cdm_secret"]}',
'device_name': Path(base_name).stem 'device_name': f'{base_name}'
})
@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
}) })
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/open', methods=['GET'])
@ -81,307 +61,309 @@ def remote_cdm_widevine_open(device):
'security_level': cdm.security_level, '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']
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: else:
return jsonify({ return jsonify({
'message': f"Device '{device}' is not found or you are not authorized to use it.", 'status': 400,
'status': 403 'message': 'Unauthorized'
}), 403 })
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/close/<session_id>', methods=['GET'])
def remote_cdm_widevine_close(device, session_id): def remote_cdm_widevine_close(device, session_id):
if str(device).lower() == config['default_wv_cdm'].lower():
session_id = bytes.fromhex(session_id) session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"] cdm = current_app.config["CDM"]
if not cdm: if not cdm:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close' 'message': f'No CDM for "{device}" has been opened yet. No session to close'
}), 400 })
try: try:
cdm.close(session_id) cdm.close(session_id)
except InvalidSession: except InvalidSession:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired' 'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
}), 400 })
return jsonify({ return jsonify({
'status': 200, 'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".', 'message': f'Successfully closed Session "{session_id.hex()}".',
}), 200 })
else:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/set_service_certificate', methods=['POST'])
def remote_cdm_widevine_set_service_certificate(device): def remote_cdm_widevine_set_service_certificate(device):
body = request.get_json() if str(device).lower() == config['default_wv_cdm'].lower():
for required_field in ("session_id", "certificate"): body = request.get_json()
if required_field == "certificate": for required_field in ("session_id", "certificate"):
has_field = required_field in body # it needs the key, but can be empty/null if required_field == "certificate":
else: has_field = required_field in body # it needs the key, but can be empty/null
has_field = body.get(required_field) else:
if not has_field: has_field = body.get(required_field)
if not has_field:
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400 })
session_id = bytes.fromhex(body["session_id"]) certificate = body["certificate"]
try:
cdm = current_app.config["CDM"] provider_id = cdm.set_service_certificate(session_id, certificate)
if not cdm: except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session id: "{session_id.hex()}", it may have expired'
})
except DecodeError as error:
return jsonify({
'status': 400,
'message': f'Invalid Service Certificate, {error}'
})
except SignatureMismatch:
return jsonify({
'status': 400,
'message': 'Signature Validation failed on the Service Certificate, rejecting'
})
return jsonify({
'status': 200,
'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
'data': {
'provider_id': provider_id,
}
})
else:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use' 'message': f'Unauthorized'
}), 400 })
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
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_service_certificate', methods=['POST'])
def remote_cdm_widevine_get_service_certificate(device): def remote_cdm_widevine_get_service_certificate(device):
body = request.get_json() if str(device).lower() == config['default_wv_cdm'].lower():
for required_field in ("session_id",): body = request.get_json()
if not body.get(required_field): for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400 })
session_id = bytes.fromhex(body["session_id"]) try:
service_certificate = cdm.get_service_certificate(session_id)
cdm = current_app.config["CDM"] except InvalidSession:
return jsonify({
if not cdm: 'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
if service_certificate:
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
else:
service_certificate_b64 = None
return jsonify({ return jsonify({
'status': 400, 'status': 200,
'message': f'No CDM session for "{device}" has been opened yet. No session to use' 'message': 'Successfully got the Service Certificate',
}), 400 'data': {
'service_certificate': service_certificate_b64,
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
if service_certificate:
service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
else: else:
service_certificate_b64 = None return jsonify({
return jsonify({ 'status': 400,
'status': 200, 'message': f'Unauthorized'
'message': 'Successfully got the Service Certificate', })
'data': {
'service_certificate': service_certificate_b64,
}
}), 200
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_license_challenge/<license_type>', methods=['POST'])
def remote_cdm_widevine_get_license_challenge(device, license_type): def remote_cdm_widevine_get_license_challenge(device, license_type):
body = request.get_json() if str(device).lower() == config['default_wv_cdm'].lower():
for required_field in ("session_id", "init_data"): body = request.get_json()
if not body.get(required_field): for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
privacy_mode = body.get("privacy_mode", True)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400 })
session_id = bytes.fromhex(body["session_id"]) if current_app.config.get("force_privacy_mode"):
privacy_mode = body.get("privacy_mode", True) privacy_mode = True
cdm = current_app.config["CDM"] if not cdm.get_service_certificate(session_id):
if not cdm: return jsonify({
return jsonify({ 'status': 403,
'status': 400, 'message': 'No Service Certificate set but Privacy Mode is Enforced.'
'message': f'No CDM session for "{device}" has been opened yet. No session to use' })
}), 400
if current_app.config.get("force_privacy_mode"): current_app.config['pssh'] = body['init_data']
privacy_mode = True init_data = widevinePSSH(body['init_data'])
if not cdm.get_service_certificate(session_id):
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
pssh=init_data,
license_type=license_type,
privacy_mode=privacy_mode
)
except InvalidSession:
return jsonify({ return jsonify({
'status': 403, 'status': 400,
'message': 'No Service Certificate set but Privacy Mode is Enforced.' 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
}), 403 })
except InvalidInitData as error:
current_app.config['pssh'] = body['init_data'] return jsonify({
init_data = widevinePSSH(body['init_data']) 'status': 400,
'message': f'Invalid Init Data, {error}'
try: })
license_request = cdm.get_license_challenge( except InvalidLicenseType:
session_id=session_id, return jsonify({
pssh=init_data, 'status': 400,
license_type=license_type, 'message': f'Invalid License Type {license_type}'
privacy_mode=privacy_mode })
) return jsonify({
except InvalidSession: 'status': 200,
'message': 'Success',
'data': {
'challenge_b64': base64.b64encode(license_request).decode()
}
})
else:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' 'message': f'Unauthorized'
}), 400 })
except InvalidInitData as error:
return jsonify({
'status': 400,
'message': f'Invalid Init Data, {error}'
}), 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
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/parse_license', methods=['POST'])
def remote_cdm_widevine_parse_license(device): def remote_cdm_widevine_parse_license(device):
body = request.get_json() if str(device).lower() == config['default_wv_cdm'].lower():
for required_field in ("session_id", "license_message"): body = request.get_json()
if not body.get(required_field): for required_field in ("session_id", "license_message"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400 })
session_id = bytes.fromhex(body["session_id"]) try:
cdm = current_app.config["CDM"] cdm.parse_license(session_id, body['license_message'])
if not cdm: except InvalidLicenseMessage as error:
return jsonify({
'status': 400,
'message': f'Invalid License Message, {error}'
})
except InvalidContext as error:
return jsonify({
'status': 400,
'message': f'Invalid Context, {error}'
})
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
})
except SignatureMismatch:
return jsonify({
'status': 400,
'message': f'Signature Validation failed on the License Message, rejecting.'
})
return jsonify({
'status': 200,
'message': 'Successfully parsed and loaded the Keys from the License message.',
})
else:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'No CDM session for "{device}" has been opened yet. No session to use' 'message': 'Unauthorized'
}), 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
@remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST']) @remotecdm_wv_bp.route('/remotecdm/widevine/<device>/get_keys/<key_type>', methods=['POST'])
def remote_cdm_widevine_get_keys(device, key_type): def remote_cdm_widevine_get_keys(device, key_type):
body = request.get_json() if str(device).lower() == config['default_wv_cdm'].lower():
for required_field in ("session_id",): body = request.get_json()
if not body.get(required_field): for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
key_type: Optional[str] = key_type
if key_type == 'ALL':
key_type = None
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({ return jsonify({
'status': 400, 'status': 400,
'message': f'Missing required field "{required_field}" in JSON body' 'message': f'No CDM session for "{device}" has been opened yet. No session to use'
}), 400 })
session_id = bytes.fromhex(body["session_id"]) try:
key_type: Optional[str] = key_type keys = cdm.get_keys(session_id, key_type)
if key_type == 'ALL': except InvalidSession:
key_type = None return jsonify({
cdm = current_app.config["CDM"] 'status': 400,
if not cdm: 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired'
return jsonify({ })
'status': 400, except ValueError as error:
'message': f'No CDM session for "{device}" has been opened yet. No session to use' return jsonify({
}), 400 'status': 400,
try: 'message': f'The Key Type value "{key_type}" is invalid, {error}'
keys = cdm.get_keys(session_id, key_type) })
except InvalidSession: keys_json = [
return jsonify({ {
'status': 400, "key_id": key.kid.hex,
'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' "key": key.key.hex(),
}), 400 "type": key.type,
except ValueError as error: "permissions": key.permissions
return jsonify({ }
'status': 400, for key in keys
'message': f'The Key Type value "{key_type}" is invalid, {error}' if not key_type or key.type == key_type
}), 400 ]
keys_json = [ for entry in keys_json:
{ if config['database_type'].lower() != 'mariadb':
"key_id": key.kid.hex, from custom_functions.database.cache_to_db_sqlite import cache_to_db
"key": key.key.hex(), elif config['database_type'].lower() == 'mariadb':
"type": key.type, from custom_functions.database.cache_to_db_mariadb import cache_to_db
"permissions": key.permissions if entry['type'] != 'SIGNING':
} cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
for key in keys
if not key_type or key.type == key_type
]
for entry in keys_json:
if config['database_type'].lower() != 'mariadb':
from custom_functions.database.cache_to_db_sqlite import cache_to_db
elif config['database_type'].lower() == 'mariadb':
from custom_functions.database.cache_to_db_mariadb import cache_to_db
if entry['type'] != 'SIGNING':
cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key'])
return jsonify({
'status': 200, return jsonify({
'message': 'Success', 'status': 200,
'data': { 'message': 'Success',
'keys': keys_json 'data': {
} 'keys': keys_json
}), 200 }
})

View File

@ -1,42 +0,0 @@
from flask import Blueprint, request, jsonify, session
import os
import logging
upload_bp = Blueprint('upload_bp', __name__)
@upload_bp.route('/upload/<cdmtype>', methods=['POST'])
def upload(cdmtype):
try:
username = session.get('username')
if not username:
return jsonify({'message': 'False', 'error': 'No username in session'}), 400
# Validate CDM type
if cdmtype not in ['PR', 'WV']:
return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400
# 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')
# 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')
if not uploaded_file:
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)
uploaded_file.save(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

View File

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

View File

@ -1,34 +0,0 @@
from flask import Blueprint, request, jsonify, session
import os
import glob
import logging
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.route('/userinfo', methods=['POST'])
def user_info():
username = session.get('username')
if not username:
try:
headers = request.headers
api_key = headers['Api-Key']
username = fetch_username_by_api_key(api_key)
except:
return jsonify({'message': 'False'}), 400
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'))]
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