Compare commits

..

26 Commits
2.3 ... main

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -9,6 +9,7 @@
"version": "0.0.0", "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",
@ -1669,6 +1670,23 @@
"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",
@ -1720,6 +1738,19 @@
"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",
@ -1788,6 +1819,18 @@
"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",
@ -1858,6 +1901,15 @@
"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",
@ -1867,6 +1919,20 @@
"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",
@ -1893,6 +1959,51 @@
"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",
@ -2220,6 +2331,41 @@
"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",
@ -2234,6 +2380,15 @@
"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",
@ -2244,6 +2399,43 @@
"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",
@ -2270,6 +2462,18 @@
"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",
@ -2286,6 +2490,45 @@
"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",
@ -2725,6 +2968,36 @@
"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",
@ -2936,6 +3209,12 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

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

View File

@ -6,6 +6,7 @@ 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() {
@ -16,7 +17,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 0"> <div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shrink-0">
<NavBar /> <NavBar />
</div> </div>
@ -31,6 +32,7 @@ function App() {
<Route path="/cache" element={<Cache />} /> <Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} /> <Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} /> <Route path="/testplayer" element={<TestPlayer />} />
<Route path="/account" element={<Account />} />
</Routes> </Routes>
</div> </div>
</div> </div>

View File

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

After

Width:  |  Height:  |  Size: 760 B

View File

@ -1,112 +1,159 @@
import { useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom'; import { 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='/'> <a href="/">CDRM-Project</a>
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 ${isActive ? 'border-l-sky-500/50 bg-black/50' : 'hover:border-l-sky-500/50 hover:bg-white/5'}` `flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-sky-500/50 bg-black/50'
: 'hover:border-l-sky-500/50 hover:bg-white/5'
}`
} }
> >
<button className='w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer'> <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 ${isActive ? 'border-l-emerald-500/50 bg-black/50' : 'hover:border-l-emerald-500/50 hover:bg-white/5'}` `flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-emerald-500/50 bg-black/50'
: 'hover:border-l-emerald-500/50 hover:bg-white/5'
}`
} }
> >
<button className='w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer'> <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 ${isActive ? 'border-l-indigo-500/50 bg-black/50' : 'hover:border-l-indigo-500/50 hover:bg-white/5'}` `flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-indigo-500/50 bg-black/50'
: 'hover:border-l-indigo-500/50 hover:bg-white/5'
}`
} }
> >
<button className='w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer'> <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 ${isActive ? 'border-l-rose-700/50 bg-black/50' : 'hover:border-l-rose-700/50 hover:bg-white/5'}` `flex flex-row p-3 border-l-3 ${
isActive
? 'border-l-rose-500/50 bg-black/50'
: 'hover:border-l-rose-500/50 hover:bg-white/5'
}`
} }
> >
<button className='w-1/3 p-3 flex flex-col items-center justify-center cursor-pointer'> <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='https://discord.cdrm-project.com' href={externalLinks.discord}
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 <img src={discordIcon} alt="Discord" className="w-1/2 group-hover:animate-bounce" />
src={discordIcon}
alt="Discord"
className='w-1/2 cursor-pointer group-hover:animate-bounce'
/>
</a> </a>
<a <a
href='https://telegram.cdrm-project.com' href={externalLinks.telegram}
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 <img src={telegramIcon} alt="Telegram" className="w-1/2 group-hover:animate-bounce" />
src={telegramIcon}
alt="Telegram"
className='w-1/2 cursor-pointer group-hover:animate-bounce'
/>
</a> </a>
<a <a
href='https://cdm-project.com/tpd94/cdrm-project' href={externalLinks.gitea}
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 <img src={giteaIcon} alt="Gitea" className="w-1/2 group-hover:animate-bounce" />
src={giteaIcon}
alt="Gitea"
className='w-1/2 cursor-pointer group-hover:animate-bounce'
/>
</a> </a>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'; import 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,6 +11,8 @@ 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
@ -41,7 +43,8 @@ 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())
@ -67,7 +70,6 @@ function HomePage() {
}); });
} }
}; };
const handleFetchPaste = () => { const handleFetchPaste = () => {
event.preventDefault(); event.preventDefault();
@ -79,7 +81,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) {
@ -87,6 +89,43 @@ 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">
@ -140,6 +179,23 @@ function HomePage() {
onChange={(e) => setData(e.target.value)} onChange={(e) => setData(e.target.value)}
/> />
{/* Device Selection Dropdown, only show if logged in */}
{devices.length > 0 && (
<>
<label htmlFor="device" className="text-white w-8/10 self-center">Select Device:</label>
<select
id="device"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white bg-black p-1"
value={selectedDevice}
onChange={(e) => setSelectedDevice(e.target.value)}
>
{devices.map((device, index) => (
<option key={index} value={device}>{device}</option>
))}
</select>
</>
)}
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch"> <div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button <button
type="button" type="button"

View File

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

View File

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

View File

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

5
configs/icon_links.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,100 @@
import sqlite3
import os
import bcrypt
def create_user_database():
os.makedirs(f'{os.getcwd()}/databases/sql', exist_ok=True)
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS user_info (
Username TEXT PRIMARY KEY,
Password TEXT,
Styled_Username TEXT,
API_Key TEXT
)
''')
def add_user(username, password, api_key):
hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
try:
cursor.execute('INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)', (username.lower(), hashed_pw, username, api_key))
conn.commit()
return True
except sqlite3.IntegrityError:
return False
def verify_user(username, password):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT Password FROM user_info WHERE Username = ?', (username.lower(),))
result = cursor.fetchone()
if result:
stored_hash = result[0]
# Ensure stored_hash is bytes; decode if it's still a string (SQLite may store as TEXT)
if isinstance(stored_hash, str):
stored_hash = stored_hash.encode('utf-8')
return bcrypt.checkpw(password.encode('utf-8'), stored_hash)
else:
return False
def fetch_api_key(username):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT API_Key FROM user_info WHERE Username = ?', (username.lower(),))
result = cursor.fetchone()
if result:
return result[0]
else:
return None
def change_password(username, new_password):
# Hash the new password
new_hashed_pw = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
# Update the password in the database
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('UPDATE user_info SET Password = ? WHERE Username = ?', (new_hashed_pw, username.lower()))
conn.commit()
return True
def change_api_key(username, new_api_key):
# Update the API key in the database
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('UPDATE user_info SET API_Key = ? WHERE Username = ?', (new_api_key, username.lower()))
conn.commit()
return True
def fetch_styled_username(username):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT Styled_Username FROM user_info WHERE Username = ?', (username.lower(),))
result = cursor.fetchone()
if result:
return result[0]
else:
return None
def fetch_username_by_api_key(api_key):
with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn:
cursor = conn.cursor()
cursor.execute('SELECT Username FROM user_info WHERE API_Key = ?', (api_key,))
result = cursor.fetchone()
if result:
return result[0] # Return the username
else:
return None # If no user is found for the API key

View File

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

View File

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

View File

@ -14,6 +14,13 @@ 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)
@ -26,4 +33,5 @@ def check_for_mariadb_database():
def check_for_sql_database(): def check_for_sql_database():
check_for_sqlite_database() check_for_sqlite_database()
check_for_mariadb_database() check_for_mariadb_database()
check_for_user_database()

View File

@ -0,0 +1,17 @@
import os
import glob
def user_allowed_to_use_device(device, username):
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
# Get filenames with extensions
pr_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'PR', '*.prd'))]
wv_files = [os.path.basename(f) for f in glob.glob(os.path.join(base_path, 'WV', '*.wvd'))]
# Combine all filenames
all_files = pr_files + wv_files
# Check if filename matches directly or by adding extensions
possible_names = {device, f"{device}.prd", f"{device}.wvd"}
return any(name in all_files for name in possible_names)

16
main.py
View File

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

View File

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

View File

@ -1,8 +1,9 @@
import os import os
import sqlite3 import sqlite3
from flask import Blueprint, jsonify, request, send_file from flask import Blueprint, jsonify, request, send_file, session
import json import json
from custom_functions.decrypt.api_decrypt import api_decrypt from custom_functions.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
@ -10,6 +11,7 @@ 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:
@ -218,12 +220,27 @@ 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 = None api_request_data_func = None
else: else:
api_request_data = api_request_data['data'] api_request_data_func = api_request_data['data']
else: api_request_data_func = None
if 'device' in api_request_data:
if api_request_data['device'] == 'default' or api_request_data['device'] == 'CDRM-Project Public Widevine CDM' or api_request_data['device'] == 'CDRM-Project Public PlayReady CDM':
api_request_device = 'public'
else:
api_request_device = api_request_data['device']
else: else:
api_request_data = None api_request_device = 'public'
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) username = None
if api_request_device != 'public':
username = session.get('username')
if not username:
return jsonify({'message': 'Not logged in, not allowed'}), 400
if user_allowed_to_use_device(device=api_request_device, username=username):
api_request_device = api_request_device
else:
return jsonify({'message': f'Not authorized / Not found'}), 403
result = api_decrypt(pssh=api_request_pssh, proxy=api_request_proxy, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data_func, device=api_request_device, username=username)
if result['status'] == 'success': if result['status'] == 'success':
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
@ -233,4 +250,18 @@ def decrypt_data():
return jsonify({ return jsonify({
'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,
})

37
routes/login.py Normal file
View File

@ -0,0 +1,37 @@
from flask import Blueprint, request, jsonify, session
from custom_functions.database.user_db import verify_user
login_bp = Blueprint(
'login_bp',
__name__,
)
@login_bp.route('/login', methods=['POST'])
def login():
if request.method == 'POST':
data = request.get_json()
for required_field in ['username', 'password']:
if required_field not in data:
return jsonify({'error': f'Missing required field: {required_field}'}), 400
if verify_user(data['username'], data['password']):
session['username'] = data['username'].lower() # Stored securely in a signed cookie
return jsonify({'message': 'Successfully logged in!'})
else:
return jsonify({'error': 'Invalid username or password!'}), 401
@login_bp.route('/login/status', methods=['POST'])
def login_status():
try:
username = session.get('username')
if username:
return jsonify({'message': 'True'})
else:
return jsonify({'message': 'False'})
except:
return jsonify({'message': 'False'})
@login_bp.route('/logout', methods=['POST'])
def logout():
session.pop('username', None)
return jsonify({'message': 'Successfully logged out!'})

View File

@ -26,7 +26,7 @@ def index(path=''):
file_path = os.path.join(react_bp.static_folder, path) 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']: elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']:
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:

42
routes/register.py Normal file
View File

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

View File

@ -1,3 +1,5 @@
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
@ -5,6 +7,11 @@ 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
@ -35,9 +42,24 @@ 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': f'{base_name}' 'device_name': Path(base_name).stem
}) })
@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():
@ -53,148 +75,171 @@ 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):
if str(device).lower() == config['default_pr_cdm'].lower(): try:
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
else: except Exception as e:
return jsonify({ return jsonify({
'status': 400, 'message': f'Failed to close Session "{session_id.hex()}".'
'message': f'Unauthorized' }), 400
})
@remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST']) @remotecdm_pr_bp.route('/remotecdm/playready/<device>/get_license_challenge', methods=['POST'])
def remote_cdm_playready_get_license_challenge(device): def remote_cdm_playready_get_license_challenge(device):
if str(device).lower() == config['default_pr_cdm'].lower(): body = request.get_json()
body = request.get_json() for required_field in ("session_id", "init_data"):
for required_field in ("session_id", "init_data"): if not body.get(required_field):
if not body.get(required_field): return jsonify({
return jsonify({ 'message': f'Missing required field "{required_field}" in JSON body'
'status': 400, }), 400
'message': f'Missing required field "{required_field}" in JSON body' cdm = current_app.config["CDM"]
}) session_id = bytes.fromhex(body["session_id"])
cdm = current_app.config["CDM"] init_data = body["init_data"]
session_id = bytes.fromhex(body["session_id"]) if not init_data.startswith("<WRMHEADER"):
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:
license_request = cdm.get_license_challenge( pssh = PlayReadyPSSH(init_data)
session_id=session_id, if pssh.wrm_headers:
wrm_header=init_data init_data = pssh.wrm_headers[0]
) except InvalidPssh as e:
except InvalidSession:
return jsonify({ return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired." 'message': f'Unable to parse base64 PSSH, {e}'
})
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': 'success', 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
'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):
if str(device).lower() == config['default_pr_cdm'].lower(): body = request.get_json()
body = request.get_json() for required_field in ("license_message", "session_id"):
for required_field in ("license_message", "session_id"): if not body.get(required_field):
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"No Cdm session for {device} has been opened yet. No session to use." 'message': f'Missing required field "{required_field}" in JSON body'
})
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': 'Successfully parsed and loaded the Keys from the License message' 'message': f"No Cdm session for {device} has been opened yet. No session to use."
}) })
session_id = bytes.fromhex(body["session_id"])
license_message = body["license_message"]
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):
if str(device).lower() == config['default_pr_cdm'].lower(): body = request.get_json()
body = request.get_json() for required_field in ("session_id",):
for required_field in ("session_id",): if not body.get(required_field):
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'
}) })
try: session_id = bytes.fromhex(body["session_id"])
keys = cdm.get_keys(session_id) cdm = current_app.config["CDM"]
except InvalidSession: if not cdm:
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': 'success', 'message': f"Missing required field '{required_field}' in JSON body."
'data': { })
'keys': keys_json try:
} keys = cdm.get_keys(session_id)
}) except InvalidSession:
return jsonify({
'message': f"Invalid Session ID '{session_id.hex()}', it may have expired."
})
except Exception as e:
return jsonify({
'message': f"Error, {e}"
})
keys_json = [
{
"key_id": key.key_id.hex,
"key": key.key.hex(),
"type": key.key_type.value,
"cipher_type": key.cipher_type.value,
"key_length": key.key_length,
}
for key in keys
]
return jsonify({
'message': 'success',
'data': {
'keys': keys_json
}
})

View File

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

42
routes/upload.py Normal file
View File

@ -0,0 +1,42 @@
from flask import Blueprint, request, jsonify, session
import os
import logging
upload_bp = Blueprint('upload_bp', __name__)
@upload_bp.route('/upload/<cdmtype>', methods=['POST'])
def upload(cdmtype):
try:
username = session.get('username')
if not username:
return jsonify({'message': 'False', 'error': 'No username in session'}), 400
# Validate CDM type
if cdmtype not in ['PR', 'WV']:
return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400
# Set up user directory paths
base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username)
pr_path = os.path.join(base_path, 'PR')
wv_path = os.path.join(base_path, 'WV')
# Create necessary directories if they don't exist
os.makedirs(pr_path, exist_ok=True)
os.makedirs(wv_path, exist_ok=True)
# Get uploaded file
uploaded_file = request.files.get('file')
if not uploaded_file:
return jsonify({'message': 'False', 'error': 'No file provided'}), 400
# Determine correct save path based on cdmtype
filename = uploaded_file.filename
save_path = os.path.join(pr_path if cdmtype == 'PR' else wv_path, filename)
uploaded_file.save(save_path)
return jsonify({'message': 'Success', 'file_saved_to': save_path})
except Exception as e:
logging.exception("Upload failed")
return jsonify({'message': 'False', 'error': 'Server error'}), 500

54
routes/user_changes.py Normal file
View File

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

34
routes/user_info.py Normal file
View File

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