Compare commits
No commits in common. "main" and "2.3" have entirely different histories.
@ -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`
|
||||||
|
1
cdrm-frontend/.gitignore
vendored
1
cdrm-frontend/.gitignore
vendored
@ -8,6 +8,7 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
1
cdrm-frontend/dist/assets/index-DQNyIeaF.css
vendored
1
cdrm-frontend/dist/assets/index-DQNyIeaF.css
vendored
File diff suppressed because one or more lines are too long
160
cdrm-frontend/dist/assets/index-DWCLK6jB.js
vendored
160
cdrm-frontend/dist/assets/index-DWCLK6jB.js
vendored
File diff suppressed because one or more lines are too long
BIN
cdrm-frontend/dist/favico.png
vendored
BIN
cdrm-frontend/dist/favico.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 862 B |
21
cdrm-frontend/dist/index.html
vendored
21
cdrm-frontend/dist/index.html
vendored
@ -1,21 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" class="w-full h-full">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favico.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="description" content="{{ data.description }}"/>
|
|
||||||
<meta name="keywords" content="{{ data.keywords }}"/>
|
|
||||||
<meta property='og:title' content="{{ data.opengraph_title }}" />
|
|
||||||
<meta property='og:description' content="{{ data.opengraph_description }}" />
|
|
||||||
<meta property='og:image' content="{{ data.opengraph_image }}" />
|
|
||||||
<meta property='og:url' content="{{ data.opengraph_url }}" />
|
|
||||||
<meta property='og:locale' content='en_US' />
|
|
||||||
<title>{{ data.tab_title }}</title>
|
|
||||||
<script type="module" crossorigin src="/assets/index-DWCLK6jB.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DQNyIeaF.css">
|
|
||||||
</head>
|
|
||||||
<body class="w-full h-full">
|
|
||||||
<div id="root" class="w-full h-full"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
BIN
cdrm-frontend/dist/og-api.jpg
vendored
BIN
cdrm-frontend/dist/og-api.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 189 KiB |
BIN
cdrm-frontend/dist/og-cache.jpg
vendored
BIN
cdrm-frontend/dist/og-cache.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 207 KiB |
BIN
cdrm-frontend/dist/og-home.jpg
vendored
BIN
cdrm-frontend/dist/og-home.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 302 KiB |
BIN
cdrm-frontend/dist/og-testplayer.jpg
vendored
BIN
cdrm-frontend/dist/og-testplayer.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 99 KiB |
279
cdrm-frontend/package-lock.json
generated
279
cdrm-frontend/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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 |
@ -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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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;
|
|
@ -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"
|
||||||
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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'
|
|
||||||
}
|
|
@ -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',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
|
@ -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',
|
||||||
|
@ -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: ''
|
||||||
|
@ -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()
|
|
@ -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
16
main.py
@ -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')
|
@ -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
|
|
@ -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,
|
|
||||||
})
|
|
@ -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!'})
|
|
@ -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:
|
||||||
|
@ -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
|
|
@ -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
|
|
||||||
}
|
|
||||||
})
|
|
@ -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
|
}
|
||||||
|
})
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
Loading…
x
Reference in New Issue
Block a user