Compare commits

...

23 Commits
2.3 ... main

Author SHA1 Message Date
55f1bd9d9b fixed storing login as lowercase to maintain compatibility 2025-05-01 17:01:20 -04:00
6b78ac120f Update user_info.py 2025-04-30 22:16:38 -04:00
3f7538838d Update icon_links.py 2025-04-30 22:02:16 -04:00
c25a891ea9 Dist 2025-04-30 20:18:25 -04:00
29b61668a4 User updates 2025-04-30 20:11:17 -04:00
c218ae4cc6 Index tags, logout button 2025-04-30 04:42:47 -04:00
a74ba64696 Added User support
- Use your own Widevine / Playready CDMs!
2025-04-30 03:42:38 -04:00
4382ff2e5f Fin 2025-04-28 18:42:00 -04:00
84dd67afba IDK 2025-04-28 18:40:17 -04:00
5ab76ed088 Merge branch 'main' of https://cdm-project.com/tpd94/CDRM-Project 2025-04-28 18:36:28 -04:00
afd55c4195 Rollback 2025-04-28 18:36:07 -04:00
3d8ec7a998 Merge branch 'github' into main 2025-04-28 22:26:39 +00:00
76bf6e9aca Added dist 2025-04-28 18:26:06 -04:00
92b7df673e Remove opengraph links 2025-04-28 18:13:01 -04:00
fa9353153c Update upstream, remove links from icons 2025-04-28 18:11:08 -04:00
1512cfbe88 Merge pull request 'Added icon_links.py to customize where the icons for discord/telegram/gitea take you' (#7) from main into github
Reviewed-on: #7
2025-04-28 22:07:38 +00:00
e068e2b827 Added icon_links.py to customize where the icons for discord/telegram/gitea take you 2025-04-28 18:06:52 -04:00
65ea79b93b Merge pull request 'Fix borders / navbar' (#6) from main into github
Reviewed-on: #6
2025-04-28 21:50:36 +00:00
51877cf56e Fix borders / navbar 2025-04-28 17:49:37 -04:00
c8e89bb2a2 Update README.md 2025-04-28 17:27:28 -04:00
024ffc83f7 Removed automatic CDM download for github maximum compliance 2025-04-28 17:22:38 -04:00
57413e8e08 Merge branch 'main' of https://cdm-project.com/tpd94/CDRM-Project 2025-04-28 15:03:40 -04:00
84bdf566f0 Added dist 2025-04-28 15:02:21 -04:00
39 changed files with 1988 additions and 518 deletions

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.1.4",
"axios": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-helmet": "^6.1.0",
@ -1669,6 +1670,23 @@
"dev": true,
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1720,6 +1738,19 @@
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -1788,6 +1819,18 @@
"dev": true,
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1858,6 +1901,15 @@
"dev": true,
"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": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -1867,6 +1919,20 @@
"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": {
"version": "1.5.143",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.143.tgz",
@ -1893,6 +1959,51 @@
"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": {
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
@ -2220,6 +2331,41 @@
"dev": true,
"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": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -2234,6 +2380,15 @@
"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": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -2244,6 +2399,43 @@
"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": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -2270,6 +2462,18 @@
"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": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -2286,6 +2490,45 @@
"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": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -2725,6 +2968,36 @@
"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": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -2936,6 +3209,12 @@
"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": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

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

View File

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

View File

@ -0,0 +1,13 @@
<!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>

After

Width:  |  Height:  |  Size: 760 B

View File

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

View File

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

View File

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

View File

@ -0,0 +1,262 @@
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

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

5
configs/icon_links.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,17 @@
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,16 +8,30 @@ from routes.react import react_bp
from routes.api import api_bp
from routes.remote_device_wv import remotecdm_wv_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__)
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)
# Register the blueprint
app.register_blueprint(react_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_pr_bp)
app.register_blueprint(user_change_bp)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')

View File

@ -5,4 +5,5 @@ pyplayready~=0.6.0
requests~=2.32.3
protobuf~=4.25.6
PyYAML~=6.0.2
mysql-connector-python
mysql-connector-python
bcrypt

View File

@ -1,8 +1,9 @@
import os
import sqlite3
from flask import Blueprint, jsonify, request, send_file
from flask import Blueprint, jsonify, request, send_file, session
import json
from custom_functions.decrypt.api_decrypt import api_decrypt
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
import shutil
import math
import yaml
@ -10,6 +11,7 @@ import mysql.connector
from io import StringIO
import tempfile
import time
from configs.icon_links import data as icon_data
api_bp = Blueprint('api', __name__)
with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file:
@ -218,12 +220,27 @@ def decrypt_data():
api_request_cookies = None
if 'data' in api_request_data:
if api_request_data['data'] == '':
api_request_data = None
api_request_data_func = None
else:
api_request_data = api_request_data['data']
api_request_data_func = api_request_data['data']
else: api_request_data_func = None
if 'device' in api_request_data:
if api_request_data['device'] == 'default' or api_request_data['device'] == 'CDRM-Project Public Widevine CDM' or api_request_data['device'] == 'CDRM-Project Public PlayReady CDM':
api_request_device = 'public'
else:
api_request_device = api_request_data['device']
else:
api_request_data = 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)
api_request_device = 'public'
username = None
if api_request_device != 'public':
username = session.get('username')
if not username:
return jsonify({'message': 'Not logged in, not allowed'}), 400
if user_allowed_to_use_device(device=api_request_device, username=username):
api_request_device = api_request_device
else:
return jsonify({'message': f'Not authorized / Not found'}), 403
result = api_decrypt(pssh=api_request_pssh, proxy=api_request_proxy, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data_func, device=api_request_device, username=username)
if result['status'] == 'success':
return jsonify({
'status': 'success',
@ -233,4 +250,12 @@ def decrypt_data():
return jsonify({
'status': 'fail',
'message': result['message']
})
})
@api_bp.route('/api/links', methods=['GET'])
def get_links():
return jsonify({
'discord': icon_data['discord'],
'telegram': icon_data['telegram'],
'gitea': icon_data['gitea'],
})

37
routes/login.py Normal file
View File

@ -0,0 +1,37 @@
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)
if path != "" and os.path.exists(file_path):
return send_from_directory(react_bp.static_folder, path)
elif path.lower() in ['', 'cache', 'api', 'testplayer']:
elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']:
data = index_tags.tags.get(path.lower(), index_tags.tags['index'])
return render_template('index.html', data=data)
else:

42
routes/register.py Normal file
View File

@ -0,0 +1,42 @@
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

@ -5,6 +5,8 @@ from pyplayready.device import Device as PlayReadyDevice
from pyplayready.cdm import Cdm as PlayReadyCDM
from pyplayready import PSSH as PlayReadyPSSH
from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh)
from custom_functions.database.user_db import fetch_username_by_api_key
from custom_functions.user_checks.device_allowed import user_allowed_to_use_device
@ -53,148 +55,169 @@ 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'])
def remote_cdm_playready_close(device, session_id):
if str(device).lower() == config['default_pr_cdm'].lower():
try:
session_id = bytes.fromhex(session_id)
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'status': 400,
'message': f'No CDM for "{device}" has been opened yet. No session to close'
})
}), 400
try:
cdm.close(session_id)
except InvalidSession:
return jsonify({
'status': 400,
'message': f'Invalid session ID "{session_id.hex()}", it may have expired'
})
}), 400
return jsonify({
'status': 200,
'message': f'Successfully closed Session "{session_id.hex()}".',
})
else:
}), 200
except Exception as e:
return jsonify({
'status': 400,
'message': f'Unauthorized'
})
'message': f'Failed to close Session "{session_id.hex()}".'
}), 400
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
def remote_cdm_playready_get_license_challenge(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'status': 400,
'message': f'Missing required field "{required_field}" in JSON body'
})
cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"):
try:
pssh = PlayReadyPSSH(init_data)
if pssh.wrm_headers:
init_data = pssh.wrm_headers[0]
except InvalidPssh as e:
return jsonify({
'message': f'Unable to parse base64 PSSH, {e}'
})
body = request.get_json()
for required_field in ("session_id", "init_data"):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
}), 400
cdm = current_app.config["CDM"]
session_id = bytes.fromhex(body["session_id"])
init_data = body["init_data"]
if not init_data.startswith("<WRMHEADER"):
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
wrm_header=init_data
)
except InvalidSession:
pssh = PlayReadyPSSH(init_data)
if pssh.wrm_headers:
init_data = pssh.wrm_headers[0]
except InvalidPssh as e:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f'Error, {e}'
'message': f'Unable to parse base64 PSSH, {e}'
})
try:
license_request = cdm.get_license_challenge(
session_id=session_id,
wrm_header=init_data
)
except InvalidSession:
return jsonify({
'message': 'success',
'data': {
'challenge': license_request
}
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f'Error, {e}'
})
return jsonify({
'message': 'success',
'data': {
'challenge': license_request
}
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/parse_license', methods=['POST'])
def remote_cdm_playready_parse_license(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("license_message", "session_id"):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
cdm = current_app.config["CDM"]
if not cdm:
body = request.get_json()
for required_field in ("license_message", "session_id"):
if not body.get(required_field):
return jsonify({
'message': f"No Cdm session for {device} has been opened yet. No session to use."
})
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except InvalidLicense as e:
return jsonify({
'message': f"Invalid License, {e}"
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
'message': f'Missing required field "{required_field}" in JSON body'
})
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': 'Successfully parsed and loaded the Keys from the License message'
'message': f"No Cdm session for {device} has been opened yet. No session to use."
})
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
try:
cdm.parse_license(session_id, license_message)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except InvalidLicense as e:
return jsonify({
'message': f"Invalid License, {e}"
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
return jsonify({
'message': 'Successfully parsed and loaded the Keys from the License message'
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_keys', methods=['POST'])
def remote_cdm_playready_get_keys(device):
if str(device).lower() == config['default_pr_cdm'].lower():
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'message': f'Missing required field "{required_field}" in JSON body'
})
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
body = request.get_json()
for required_field in ("session_id",):
if not body.get(required_field):
return jsonify({
'message': f"Missing required field '{required_field}' in JSON body."
'message': f'Missing required field "{required_field}" in JSON body'
})
try:
keys = cdm.get_keys(session_id)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
keys_json = [
{
"key_id": key.key_id.hex,
"key": key.key.hex(),
"type": key.key_type.value,
"cipher_type": key.cipher_type.value,
"key_length": key.key_length,
}
for key in keys
]
session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"]
if not cdm:
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})
'message': f"Missing required field '{required_field}' in JSON body."
})
try:
keys = cdm.get_keys(session_id)
except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
keys_json = [
{
"key_id": key.key_id.hex,
"key": key.key.hex(),
"type": key.key_type.value,
"cipher_type": key.cipher_type.value,
"key_length": key.key_length,
}
for key in keys
]
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})

View File

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

42
routes/upload.py Normal file
View File

@ -0,0 +1,42 @@
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

54
routes/user_changes.py Normal file
View File

@ -0,0 +1,54 @@
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

29
routes/user_info.py Normal file
View File

@ -0,0 +1,29 @@
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
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:
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