diff --git a/.gitignore b/.gitignore index 15161ed..1a2f6fd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ main.spec pyinstallericon.ico icon.ico venv +frontend-dist diff --git a/build.py b/build.py new file mode 100644 index 0000000..250b4b8 --- /dev/null +++ b/build.py @@ -0,0 +1,28 @@ +"""Main file to run the application.""" + +import os +import subprocess +import shutil + + +def build_frontend(): + """Build the frontend.""" + frontend_dir = "cdrm-frontend" + + # Check and run npm commands if needed + if not os.path.exists(f"{frontend_dir}/node_modules"): + subprocess.run(["npm", "install"], cwd=frontend_dir, check=False) + + if not os.path.exists(f"{frontend_dir}/dist"): + subprocess.run(["npm", "run", "build"], cwd=frontend_dir, check=False) + + # Move dist to frontend-dist + if os.path.exists("frontend-dist"): + shutil.rmtree("frontend-dist") + shutil.copytree(f"{frontend_dir}/dist", "frontend-dist") + + print("✅ Build complete. Run the application with 'python main.py'") + + +if __name__ == "__main__": + build_frontend() diff --git a/cdrm-frontend/dist/assets/index-1tbqhIbb.js b/cdrm-frontend/dist/assets/index-1tbqhIbb.js deleted file mode 100644 index 2de63d7..0000000 --- a/cdrm-frontend/dist/assets/index-1tbqhIbb.js +++ /dev/null @@ -1,193 +0,0 @@ -var b4=(A,b)=>()=>(b||A((b={exports:{}}).exports,b),b.exports);var cN=b4((ON,ug)=>{(function(){const b=document.createElement("link").relList;if(b&&b.supports&&b.supports("modulepreload"))return;for(const x of document.querySelectorAll('link[rel="modulepreload"]'))S(x);new MutationObserver(x=>{for(const U of x)if(U.type==="childList")for(const O of U.addedNodes)O.tagName==="LINK"&&O.rel==="modulepreload"&&S(O)}).observe(document,{childList:!0,subtree:!0});function R(x){const U={};return x.integrity&&(U.integrity=x.integrity),x.referrerPolicy&&(U.referrerPolicy=x.referrerPolicy),x.crossOrigin==="use-credentials"?U.credentials="include":x.crossOrigin==="anonymous"?U.credentials="omit":U.credentials="same-origin",U}function S(x){if(x.ep)return;x.ep=!0;const U=R(x);fetch(x.href,U)}})();var I4=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function k4(A){return A&&A.__esModule&&Object.prototype.hasOwnProperty.call(A,"default")?A.default:A}var u2={exports:{}},ng={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var RA;function D4(){if(RA)return ng;RA=1;var A=Symbol.for("react.transitional.element"),b=Symbol.for("react.fragment");function R(S,x,U){var O=null;if(U!==void 0&&(O=""+U),x.key!==void 0&&(O=""+x.key),"key"in x){U={};for(var h in x)h!=="key"&&(U[h]=x[h])}else U=x;return x=U.ref,{$$typeof:A,type:S,key:O,ref:x!==void 0?x:null,props:U}}return ng.Fragment=b,ng.jsx=R,ng.jsxs=R,ng}var MA;function O4(){return MA||(MA=1,u2.exports=D4()),u2.exports}var re=O4(),a2={exports:{}},Gt={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var NA;function R4(){if(NA)return Gt;NA=1;var A=Symbol.for("react.transitional.element"),b=Symbol.for("react.portal"),R=Symbol.for("react.fragment"),S=Symbol.for("react.strict_mode"),x=Symbol.for("react.profiler"),U=Symbol.for("react.consumer"),O=Symbol.for("react.context"),h=Symbol.for("react.forward_ref"),s=Symbol.for("react.suspense"),u=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),v=Symbol.iterator;function N(ae){return ae===null||typeof ae!="object"?null:(ae=v&&ae[v]||ae["@@iterator"],typeof ae=="function"?ae:null)}var Z={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},C=Object.assign,$={};function oe(ae,Oe,et){this.props=ae,this.context=Oe,this.refs=$,this.updater=et||Z}oe.prototype.isReactComponent={},oe.prototype.setState=function(ae,Oe){if(typeof ae!="object"&&typeof ae!="function"&&ae!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,ae,Oe,"setState")},oe.prototype.forceUpdate=function(ae){this.updater.enqueueForceUpdate(this,ae,"forceUpdate")};function me(){}me.prototype=oe.prototype;function Ve(ae,Oe,et){this.props=ae,this.context=Oe,this.refs=$,this.updater=et||Z}var ue=Ve.prototype=new me;ue.constructor=Ve,C(ue,oe.prototype),ue.isPureReactComponent=!0;var Ne=Array.isArray,Re={H:null,A:null,T:null,S:null,V:null},$e=Object.prototype.hasOwnProperty;function it(ae,Oe,et,ze,G,vt){return et=vt.ref,{$$typeof:A,type:ae,key:Oe,ref:et!==void 0?et:null,props:vt}}function Dt(ae,Oe){return it(ae.type,Oe,void 0,void 0,void 0,ae.props)}function _t(ae){return typeof ae=="object"&&ae!==null&&ae.$$typeof===A}function _(ae){var Oe={"=":"=0",":":"=2"};return"$"+ae.replace(/[=:]/g,function(et){return Oe[et]})}var ce=/\/+/g;function he(ae,Oe){return typeof ae=="object"&&ae!==null&&ae.key!=null?_(""+ae.key):Oe.toString(36)}function ot(){}function _e(ae){switch(ae.status){case"fulfilled":return ae.value;case"rejected":throw ae.reason;default:switch(typeof ae.status=="string"?ae.then(ot,ot):(ae.status="pending",ae.then(function(Oe){ae.status==="pending"&&(ae.status="fulfilled",ae.value=Oe)},function(Oe){ae.status==="pending"&&(ae.status="rejected",ae.reason=Oe)})),ae.status){case"fulfilled":return ae.value;case"rejected":throw ae.reason}}throw ae}function De(ae,Oe,et,ze,G){var vt=typeof ae;(vt==="undefined"||vt==="boolean")&&(ae=null);var mt=!1;if(ae===null)mt=!0;else switch(vt){case"bigint":case"string":case"number":mt=!0;break;case"object":switch(ae.$$typeof){case A:case b:mt=!0;break;case p:return mt=ae._init,De(mt(ae._payload),Oe,et,ze,G)}}if(mt)return G=G(ae),mt=ze===""?"."+he(ae,0):ze,Ne(G)?(et="",mt!=null&&(et=mt.replace(ce,"$&/")+"/"),De(G,Oe,et,"",function(pl){return pl})):G!=null&&(_t(G)&&(G=Dt(G,et+(G.key==null||ae&&ae.key===G.key?"":(""+G.key).replace(ce,"$&/")+"/")+mt)),Oe.push(G)),1;mt=0;var Gi=ze===""?".":ze+":";if(Ne(ae))for(var Tn=0;Tn>>1,ae=xe[wn];if(0>>1;wnx(ze,gt))Gx(vt,ze)?(xe[wn]=vt,xe[G]=gt,wn=G):(xe[wn]=ze,xe[et]=gt,wn=et);else if(Gx(vt,gt))xe[wn]=vt,xe[G]=gt,wn=G;else break e}}return Ue}function x(xe,Ue){var gt=xe.sortIndex-Ue.sortIndex;return gt!==0?gt:xe.id-Ue.id}if(A.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var U=performance;A.unstable_now=function(){return U.now()}}else{var O=Date,h=O.now();A.unstable_now=function(){return O.now()-h}}var s=[],u=[],p=1,v=null,N=3,Z=!1,C=!1,$=!1,oe=!1,me=typeof setTimeout=="function"?setTimeout:null,Ve=typeof clearTimeout=="function"?clearTimeout:null,ue=typeof setImmediate<"u"?setImmediate:null;function Ne(xe){for(var Ue=R(u);Ue!==null;){if(Ue.callback===null)S(u);else if(Ue.startTime<=xe)S(u),Ue.sortIndex=Ue.expirationTime,b(s,Ue);else break;Ue=R(u)}}function Re(xe){if($=!1,Ne(xe),!C)if(R(s)!==null)C=!0,$e||($e=!0,he());else{var Ue=R(u);Ue!==null&&De(Re,Ue.startTime-xe)}}var $e=!1,it=-1,Dt=5,_t=-1;function _(){return oe?!0:!(A.unstable_now()-_txe&&_());){var wn=v.callback;if(typeof wn=="function"){v.callback=null,N=v.priorityLevel;var ae=wn(v.expirationTime<=xe);if(xe=A.unstable_now(),typeof ae=="function"){v.callback=ae,Ne(xe),Ue=!0;break t}v===R(s)&&S(s),Ne(xe)}else S(s);v=R(s)}if(v!==null)Ue=!0;else{var Oe=R(u);Oe!==null&&De(Re,Oe.startTime-xe),Ue=!1}}break e}finally{v=null,N=gt,Z=!1}Ue=void 0}}finally{Ue?he():$e=!1}}}var he;if(typeof ue=="function")he=function(){ue(ce)};else if(typeof MessageChannel<"u"){var ot=new MessageChannel,_e=ot.port2;ot.port1.onmessage=ce,he=function(){_e.postMessage(null)}}else he=function(){me(ce,0)};function De(xe,Ue){it=me(function(){xe(A.unstable_now())},Ue)}A.unstable_IdlePriority=5,A.unstable_ImmediatePriority=1,A.unstable_LowPriority=4,A.unstable_NormalPriority=3,A.unstable_Profiling=null,A.unstable_UserBlockingPriority=2,A.unstable_cancelCallback=function(xe){xe.callback=null},A.unstable_forceFrameRate=function(xe){0>xe||125wn?(xe.sortIndex=gt,b(u,xe),R(s)===null&&xe===R(u)&&($?(Ve(it),it=-1):$=!0,De(Re,gt-wn))):(xe.sortIndex=ae,b(s,xe),C||Z||(C=!0,$e||($e=!0,he()))),xe},A.unstable_shouldYield=_,A.unstable_wrapCallback=function(xe){var Ue=N;return function(){var gt=N;N=Ue;try{return xe.apply(this,arguments)}finally{N=gt}}}}(f2)),f2}var _A;function N4(){return _A||(_A=1,c2.exports=M4()),c2.exports}var d2={exports:{}},pr={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var UA;function P4(){if(UA)return pr;UA=1;var A=O2();function b(s){var u="https://react.dev/errors/"+s;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(A)}catch(b){console.error(b)}}return A(),d2.exports=P4(),d2.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var BA;function _4(){if(BA)return ig;BA=1;var A=N4(),b=O2(),R=L4();function S(o){var a="https://react.dev/errors/"+o;if(1ae||(o.current=wn[ae],wn[ae]=null,ae--)}function ze(o,a){ae++,wn[ae]=o.current,o.current=a}var G=Oe(null),vt=Oe(null),mt=Oe(null),Gi=Oe(null);function Tn(o,a){switch(ze(mt,a),ze(vt,o),ze(G,null),a.nodeType){case 9:case 11:o=(o=a.documentElement)&&(o=o.namespaceURI)?Im(o):0;break;default:if(o=a.tagName,a=a.namespaceURI)a=Im(a),o=km(a,o);else switch(o){case"svg":o=1;break;case"math":o=2;break;default:o=0}}et(G),ze(G,o)}function pl(){et(G),et(vt),et(mt)}function Up(o){o.memoizedState!==null&&ze(Gi,o);var a=G.current,d=km(a,o.type);a!==d&&(ze(vt,o),ze(G,d))}function Uf(o){vt.current===o&&(et(G),et(vt)),Gi.current===o&&(et(Gi),pc._currentValue=gt)}var Pe=Object.prototype.hasOwnProperty,Ql=A.unstable_scheduleCallback,os=A.unstable_cancelCallback,bc=A.unstable_shouldYield,Tv=A.unstable_requestPaint,ui=A.unstable_now,Ct=A.unstable_getCurrentPriorityLevel,Jl=A.unstable_ImmediatePriority,yg=A.unstable_UserBlockingPriority,jp=A.unstable_NormalPriority,B2=A.unstable_LowPriority,Bp=A.unstable_IdlePriority,jf=A.log,Mt=A.unstable_setDisableYieldValue,ls=null,yr=null;function Zl(o){if(typeof jf=="function"&&Mt(o),yr&&typeof yr.setStrictMode=="function")try{yr.setStrictMode(ls,o)}catch{}}var nr=Math.clz32?Math.clz32:io,Jn=Math.log,yt=Math.LN2;function io(o){return o>>>=0,o===0?32:31-(Jn(o)/yt|0)|0}var us=256,Bf=4194304;function hl(o){var a=o&42;if(a!==0)return a;switch(o&-o){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return o&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return o&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return o}}function Et(o,a,d){var g=o.pendingLanes;if(g===0)return 0;var E=0,k=o.suspendedLanes,B=o.pingedLanes;o=o.warmLanes;var H=g&134217727;return H!==0?(g=H&~k,g!==0?E=hl(g):(B&=H,B!==0?E=hl(B):d||(d=H&~o,d!==0&&(E=hl(d))))):(H=g&~k,H!==0?E=hl(H):B!==0?E=hl(B):d||(d=g&~o,d!==0&&(E=hl(d)))),E===0?0:a!==0&&a!==E&&(a&k)===0&&(k=E&-E,d=a&-a,k>=d||k===32&&(d&4194048)!==0)?a:E}function Ic(o,a){return(o.pendingLanes&~(o.suspendedLanes&~o.pingedLanes)&a)===0}function xv(o,a){switch(o){case 1:case 2:case 4:case 8:case 64:return a+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return a+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Vp(){var o=us;return us<<=1,(us&4194048)===0&&(us=256),o}function gg(){var o=Bf;return Bf<<=1,(Bf&62914560)===0&&(Bf=4194304),o}function dn(o){for(var a=[],d=0;31>d;d++)a.push(o);return a}function $l(o,a){o.pendingLanes|=a,a!==268435456&&(o.suspendedLanes=0,o.pingedLanes=0,o.warmLanes=0)}function Vf(o,a,d,g,E,k){var B=o.pendingLanes;o.pendingLanes=d,o.suspendedLanes=0,o.pingedLanes=0,o.warmLanes=0,o.expiredLanes&=d,o.entangledLanes&=d,o.errorRecoveryDisabledLanes&=d,o.shellSuspendCounter=0;var H=o.entanglements,ie=o.expirationTimes,we=o.hiddenUpdates;for(d=B&~d;0)":-1E||ie[g]!==we[E]){var F=` -`+ie[g].replace(" at new "," at ");return o.displayName&&F.includes("")&&(F=F.replace("",o.displayName)),F}while(1<=g&&0<=E);break}}}finally{oo=!1,Error.prepareStackTrace=d}return(d=o?o.displayName||o.name:"")?Hr(d):""}function Vn(o){switch(o.tag){case 26:case 27:case 5:return Hr(o.type);case 16:return Hr("Lazy");case 13:return Hr("Suspense");case 19:return Hr("SuspenseList");case 0:case 15:return an(o.type,!1);case 11:return an(o.type.render,!1);case 1:return an(o.type,!0);case 31:return Hr("Activity");default:return""}}function Oo(o){try{var a="";do a+=Vn(o),o=o.return;while(o);return a}catch(d){return` -Error generating stack: `+d.message+` -`+d.stack}}function mr(o){switch(typeof o){case"bigint":case"boolean":case"number":case"string":case"undefined":return o;case"object":return o;default:return""}}function $u(o){var a=o.type;return(o=o.nodeName)&&o.toLowerCase()==="input"&&(a==="checkbox"||a==="radio")}function Ri(o){var a=$u(o)?"checked":"value",d=Object.getOwnPropertyDescriptor(o.constructor.prototype,a),g=""+o[a];if(!o.hasOwnProperty(a)&&typeof d<"u"&&typeof d.get=="function"&&typeof d.set=="function"){var E=d.get,k=d.set;return Object.defineProperty(o,a,{configurable:!0,get:function(){return E.call(this)},set:function(B){g=""+B,k.call(this,B)}}),Object.defineProperty(o,a,{enumerable:d.enumerable}),{getValue:function(){return g},setValue:function(B){g=""+B},stopTracking:function(){o._valueTracker=null,delete o[a]}}}}function ss(o){o._valueTracker||(o._valueTracker=Ri(o))}function ut(o){if(!o)return!1;var a=o._valueTracker;if(!a)return!0;var d=a.getValue(),g="";return o&&(g=$u(o)?o.checked?"true":"false":o.value),o=g,o!==d?(a.setValue(o),!0):!1}function Dn(o){if(o=o||(typeof document<"u"?document:void 0),typeof o>"u")return null;try{return o.activeElement||o.body}catch{return o.body}}var wg=/[\n"\\]/g;function vr(o){return o.replace(wg,function(a){return"\\"+a.charCodeAt(0).toString(16)+" "})}function ea(o,a,d,g,E,k,B,H){o.name="",B!=null&&typeof B!="function"&&typeof B!="symbol"&&typeof B!="boolean"?o.type=B:o.removeAttribute("type"),a!=null?B==="number"?(a===0&&o.value===""||o.value!=a)&&(o.value=""+mr(a)):o.value!==""+mr(a)&&(o.value=""+mr(a)):B!=="submit"&&B!=="reset"||o.removeAttribute("value"),a!=null?zf(o,B,mr(a)):d!=null?zf(o,B,mr(d)):g!=null&&o.removeAttribute("value"),E==null&&k!=null&&(o.defaultChecked=!!k),E!=null&&(o.checked=E&&typeof E!="function"&&typeof E!="symbol"),H!=null&&typeof H!="function"&&typeof H!="symbol"&&typeof H!="boolean"?o.name=""+mr(H):o.removeAttribute("name")}function qf(o,a,d,g,E,k,B,H){if(k!=null&&typeof k!="function"&&typeof k!="symbol"&&typeof k!="boolean"&&(o.type=k),a!=null||d!=null){if(!(k!=="submit"&&k!=="reset"||a!=null))return;d=d!=null?""+mr(d):"",a=a!=null?""+mr(a):d,H||a===o.value||(o.value=a),o.defaultValue=a}g=g??E,g=typeof g!="function"&&typeof g!="symbol"&&!!g,o.checked=H?o.checked:!!g,o.defaultChecked=!!g,B!=null&&typeof B!="function"&&typeof B!="symbol"&&typeof B!="boolean"&&(o.name=B)}function zf(o,a,d){a==="number"&&Dn(o.ownerDocument)===o||o.defaultValue===""+d||(o.defaultValue=""+d)}function ta(o,a,d,g){if(o=o.options,a){a={};for(var E=0;E"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),lu=!1;if(wr)try{var Ro={};Object.defineProperty(Ro,"passive",{get:function(){lu=!0}}),window.addEventListener("test",Ro,Ro),window.removeEventListener("test",Ro,Ro)}catch{lu=!1}var lr=null,gl=null,uu=null;function oa(){if(uu)return uu;var o,a=gl,d=a.length,g,E="value"in lr?lr.value:lr.textContent,k=E.length;for(o=0;o=ys),gs=" ",Rc=!1;function co(o,a){switch(o){case"keyup":return ed.indexOf(a.keyCode)!==-1;case"keydown":return a.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function td(o){return o=o.detail,typeof o=="object"&&"data"in o?o.data:null}var Po=!1;function bg(o,a){switch(o){case"compositionend":return td(a);case"keypress":return a.which!==32?null:(Rc=!0,gs);case"textInput":return o=a.data,o===gs&&Rc?null:o;default:return null}}function Mc(o,a){if(Po)return o==="compositionend"||!hs&&co(o,a)?(o=oa(),uu=gl=lr=null,Po=!1,o):null;switch(o){case"paste":return null;case"keypress":if(!(a.ctrlKey||a.altKey||a.metaKey)||a.ctrlKey&&a.altKey){if(a.char&&1=a)return{node:d,offset:a-o};o=g}e:{for(;d;){if(d.nextSibling){d=d.nextSibling;break e}d=d.parentNode}d=void 0}d=rd(d)}}function Pc(o,a){return o&&a?o===a?!0:o&&o.nodeType===3?!1:a&&a.nodeType===3?Pc(o,a.parentNode):"contains"in o?o.contains(a):o.compareDocumentPosition?!!(o.compareDocumentPosition(a)&16):!1:!1}function od(o){o=o!=null&&o.ownerDocument!=null&&o.ownerDocument.defaultView!=null?o.ownerDocument.defaultView:window;for(var a=Dn(o.document);a instanceof o.HTMLIFrameElement;){try{var d=typeof a.contentWindow.location.href=="string"}catch{d=!1}if(d)o=a.contentWindow;else break;a=Dn(o.document)}return a}function Ts(o){var a=o&&o.nodeName&&o.nodeName.toLowerCase();return a&&(a==="input"&&(o.type==="text"||o.type==="search"||o.type==="tel"||o.type==="url"||o.type==="password")||a==="textarea"||o.contentEditable==="true")}var Ti=wr&&"documentMode"in document&&11>=document.documentMode,xr=null,Mi=null,xi=null,ca=!1;function $p(o,a,d){var g=d.window===d?d.document:d.nodeType===9?d:d.ownerDocument;ca||xr==null||xr!==Dn(g)||(g=xr,"selectionStart"in g&&Ts(g)?g={start:g.selectionStart,end:g.selectionEnd}:(g=(g.ownerDocument&&g.ownerDocument.defaultView||window).getSelection(),g={anchorNode:g.anchorNode,anchorOffset:g.anchorOffset,focusNode:g.focusNode,focusOffset:g.focusOffset}),xi&&ws(xi,g)||(xi=g,g=oc(Mi,"onSelect"),0>=B,E-=B,bl=1<<32-nr(a)+E|d<k?k:8;var B=xe.T,H={};xe.T=H,Nl(o,!1,a,d);try{var ie=E(),we=xe.S;if(we!==null&&we(H,ie),ie!==null&&typeof ie=="object"&&typeof ie.then=="function"){var F=_g(ie,g);Ml(o,a,F,Ui(o))}else Ml(o,a,g,Ui(o))}catch(Ie){Ml(o,a,{then:function(){},status:"rejected",reason:Ie},Ui())}finally{Ue.p=k,xe.T=B}}function Td(){}function xd(o,a,d,g){if(o.tag!==5)throw Error(S(476));var E=Ed(o).queue;Jc(o,E,a,gt,d===null?Td:function(){return Wg(o),d(g)})}function Ed(o){var a=o.memoizedState;if(a!==null)return a;a={memoizedState:gt,baseState:gt,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Rl,lastRenderedState:gt},next:null};var d={};return a.next={memoizedState:d,baseState:d,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Rl,lastRenderedState:d},next:null},o.memoizedState=a,o=o.alternate,o!==null&&(o.memoizedState=a),a}function Wg(o){var a=Ed(o).next.queue;Ml(o,a,{},Ui())}function ba(){return si(pc)}function _i(){return zn().memoizedState}function Cu(){return zn().memoizedState}function js(o){for(var a=o.return;a!==null;){switch(a.tag){case 24:case 3:var d=Ui();o=Pt(d);var g=Ho(a,o,d);g!==null&&(Rr(g,a,d),Kc(g,a,d)),a={cache:vn()},o.payload=a;return}a=a.return}}function Qg(o,a,d){var g=Ui();d={lane:g,revertLane:0,action:d,hasEagerState:!1,eagerState:null,next:null},bu(o)?Jg(a,d):(d=El(o,a,d,g),d!==null&&(Rr(d,o,g),un(d,a,g)))}function bh(o,a,d){var g=Ui();Ml(o,a,d,g)}function Ml(o,a,d,g){var E={lane:g,revertLane:0,action:d,hasEagerState:!1,eagerState:null,next:null};if(bu(o))Jg(a,E);else{var k=o.alternate;if(o.lanes===0&&(k===null||k.lanes===0)&&(k=a.lastRenderedReducer,k!==null))try{var B=a.lastRenderedState,H=k(B,d);if(E.hasEagerState=!0,E.eagerState=H,Xi(H,B))return Ot(o,a,E,0),En===null&&As(),!1}catch{}finally{}if(d=El(o,a,E,g),d!==null)return Rr(d,o,g),un(d,a,g),!0}return!1}function Nl(o,a,d,g){if(g={lane:2,revertLane:Wd(),action:g,hasEagerState:!1,eagerState:null,next:null},bu(o)){if(a)throw Error(S(479))}else a=El(o,d,g,2),a!==null&&Rr(a,o,2)}function bu(o){var a=o.alternate;return o===Lt||a!==null&&a===Lt}function Jg(o,a){xu=Fc=!0;var d=o.pending;d===null?a.next=a:(a.next=d.next,d.next=a),o.pending=a}function un(o,a,d){if((d&4194048)!==0){var g=a.lanes;g&=o.pendingLanes,d|=g,a.lanes=d,eu(o,d)}}var Zc={readContext:si,use:zc,useCallback:qn,useContext:qn,useEffect:qn,useImperativeHandle:qn,useLayoutEffect:qn,useInsertionEffect:qn,useMemo:qn,useReducer:qn,useRef:qn,useState:qn,useDebugValue:qn,useDeferredValue:qn,useTransition:qn,useSyncExternalStore:qn,useId:qn,useHostTransitionStatus:qn,useFormState:qn,useActionState:qn,useOptimistic:qn,useMemoCache:qn,useCacheRefresh:qn},Ih={readContext:si,use:zc,useCallback:function(o,a){return fr().memoizedState=[o,a===void 0?null:a],o},useContext:si,useEffect:Th,useImperativeHandle:function(o,a,d){d=d!=null?d.concat([o]):null,Wc(4194308,4,_s.bind(null,a,o),d)},useLayoutEffect:function(o,a){return Wc(4194308,4,o,a)},useInsertionEffect:function(o,a){Wc(4,2,o,a)},useMemo:function(o,a){var d=fr();a=a===void 0?null:a;var g=o();if(Ko){Zl(!0);try{o()}finally{Zl(!1)}}return d.memoizedState=[g,a],g},useReducer:function(o,a,d){var g=fr();if(d!==void 0){var E=d(a);if(Ko){Zl(!0);try{d(a)}finally{Zl(!1)}}}else E=a;return g.memoizedState=g.baseState=E,o={pending:null,lanes:0,dispatch:null,lastRenderedReducer:o,lastRenderedState:E},g.queue=o,o=o.dispatch=Qg.bind(null,Lt,o),[g.memoizedState,o]},useRef:function(o){var a=fr();return o={current:o},a.memoizedState=o},useState:function(o){o=Aa(o);var a=o.queue,d=bh.bind(null,Lt,a);return a.dispatch=d,[o.memoizedState,d]},useDebugValue:Ah,useDeferredValue:function(o,a){var d=fr();return Us(d,o,a)},useTransition:function(){var o=Aa(!1);return o=Jc.bind(null,Lt,o.queue,!0,!1),fr().memoizedState=o,[!1,o]},useSyncExternalStore:function(o,a,d){var g=Lt,E=fr();if($t){if(d===void 0)throw Error(S(407));d=d()}else{if(d=a(),En===null)throw Error(S(349));(nn&124)!==0||Hg(g,a,d)}E.memoizedState=d;var k={value:d,getSnapshot:a};return E.queue=k,Th(Ps.bind(null,g,k,o),[o]),g.flags|=2048,mo(9,Eu(),yh.bind(null,g,k,d,a),null),d},useId:function(){var o=fr(),a=En.identifierPrefix;if($t){var d=Il,g=bl;d=(g&~(1<<32-nr(g)-1)).toString(32)+d,a="«"+a+"R"+d,d=Ta++,0Tt?(pi=at,at=null):pi=at.sibling;var cn=Te(ye,at,ve[Tt],be);if(cn===null){at===null&&(at=pi);break}o&&at&&cn.alternate===null&&a(ye,at),se=k(cn,se,Tt),qt===null?rt=cn:qt.sibling=cn,qt=cn,at=pi}if(Tt===ve.length)return d(ye,at),$t&&Ar(ye,Tt),rt;if(at===null){for(;TtTt?(pi=at,at=null):pi=at.sibling;var Ku=Te(ye,at,cn.value,be);if(Ku===null){at===null&&(at=pi);break}o&&at&&Ku.alternate===null&&a(ye,at),se=k(Ku,se,Tt),qt===null?rt=Ku:qt.sibling=Ku,qt=Ku,at=pi}if(cn.done)return d(ye,at),$t&&Ar(ye,Tt),rt;if(at===null){for(;!cn.done;Tt++,cn=ve.next())cn=Ie(ye,cn.value,be),cn!==null&&(se=k(cn,se,Tt),qt===null?rt=cn:qt.sibling=cn,qt=cn);return $t&&Ar(ye,Tt),rt}for(at=g(at);!cn.done;Tt++,cn=ve.next())cn=de(at,ye,Tt,cn.value,be),cn!==null&&(o&&cn.alternate!==null&&at.delete(cn.key===null?Tt:cn.key),se=k(cn,se,Tt),qt===null?rt=cn:qt.sibling=cn,qt=cn);return o&&at.forEach(function(Jv){return a(ye,Jv)}),$t&&Ar(ye,Tt),rt}function yn(ye,se,ve,be){if(typeof ve=="object"&&ve!==null&&ve.type===C&&ve.key===null&&(ve=ve.props.children),typeof ve=="object"&&ve!==null){switch(ve.$$typeof){case N:e:{for(var rt=ve.key;se!==null;){if(se.key===rt){if(rt=ve.type,rt===C){if(se.tag===7){d(ye,se.sibling),be=E(se,ve.props.children),be.return=ye,ye=be;break e}}else if(se.elementType===rt||typeof rt=="object"&&rt!==null&&rt.$$typeof===Dt&&$g(rt)===se.type){d(ye,se.sibling),be=E(se,ve.props),$c(be,ve),be.return=ye,ye=be;break e}d(ye,se);break}else a(ye,se);se=se.sibling}ve.type===C?(be=du(ve.props.children,ye.mode,be,ve.key),be.return=ye,ye=be):(be=ud(ve.type,ve.key,ve.props,null,ye.mode,be),$c(be,ve),be.return=ye,ye=be)}return B(ye);case Z:e:{for(rt=ve.key;se!==null;){if(se.key===rt)if(se.tag===4&&se.stateNode.containerInfo===ve.containerInfo&&se.stateNode.implementation===ve.implementation){d(ye,se.sibling),be=E(se,ve.children||[]),be.return=ye,ye=be;break e}else{d(ye,se);break}else a(ye,se);se=se.sibling}be=rh(ve,ye.mode,be),be.return=ye,ye=be}return B(ye);case Dt:return rt=ve._init,ve=rt(ve._payload),yn(ye,se,ve,be)}if(De(ve))return bt(ye,se,ve,be);if(he(ve)){if(rt=he(ve),typeof rt!="function")throw Error(S(150));return ve=rt.call(ve),pt(ye,se,ve,be)}if(typeof ve.then=="function")return yn(ye,se,Ad(ve),be);if(ve.$$typeof===ue)return yn(ye,se,ga(ye,ve),be);Cd(ye,ve)}return typeof ve=="string"&&ve!==""||typeof ve=="number"||typeof ve=="bigint"?(ve=""+ve,se!==null&&se.tag===6?(d(ye,se.sibling),be=E(se,ve),be.return=ye,ye=be):(d(ye,se),be=ih(ve,ye.mode,be),be.return=ye,ye=be),B(ye)):d(ye,se)}return function(ye,se,ve,be){try{Bs=0;var rt=yn(ye,se,ve,be);return kr=null,rt}catch(at){if(at===Cr||at===bn)throw at;var qt=cr(29,at,null,ye.mode);return qt.lanes=be,qt.return=ye,qt}finally{}}}var Vs=em(!0),tm=em(!1),Fr=Oe(null),Go=null;function vo(o){var a=o.alternate;ze(ci,ci.current&1),ze(Fr,o),Go===null&&(a===null||nt.current!==null||a.memoizedState!==null)&&(Go=o)}function nm(o){if(o.tag===22){if(ze(ci,ci.current),ze(Fr,o),Go===null){var a=o.alternate;a!==null&&a.memoizedState!==null&&(Go=o)}}else Fo()}function Fo(){ze(ci,ci.current),ze(Fr,Fr.current)}function Pl(o){et(Fr),Go===o&&(Go=null),et(ci)}var ci=Oe(0);function bd(o){for(var a=o;a!==null;){if(a.tag===13){var d=a.memoizedState;if(d!==null&&(d=d.dehydrated,d===null||d.data==="$?"||gf(d)))return a}else if(a.tag===19&&a.memoizedProps.revealOrder!==void 0){if((a.flags&128)!==0)return a}else if(a.child!==null){a.child.return=a,a=a.child;continue}if(a===o)break;for(;a.sibling===null;){if(a.return===null||a.return===o)return null;a=a.return}a.sibling.return=a.return,a=a.sibling}return null}function kh(o,a,d,g){a=o.memoizedState,d=d(g,a),d=d==null?a:p({},a,d),o.memoizedState=d,o.lanes===0&&(o.updateQueue.baseState=d)}var Dh={enqueueSetState:function(o,a,d){o=o._reactInternals;var g=Ui(),E=Pt(g);E.payload=a,d!=null&&(E.callback=d),a=Ho(o,E,g),a!==null&&(Rr(a,o,g),Kc(a,o,g))},enqueueReplaceState:function(o,a,d){o=o._reactInternals;var g=Ui(),E=Pt(g);E.tag=1,E.payload=a,d!=null&&(E.callback=d),a=Ho(o,E,g),a!==null&&(Rr(a,o,g),Kc(a,o,g))},enqueueForceUpdate:function(o,a){o=o._reactInternals;var d=Ui(),g=Pt(d);g.tag=2,a!=null&&(g.callback=a),a=Ho(o,g,d),a!==null&&(Rr(a,o,d),Kc(a,o,d))}};function ef(o,a,d,g,E,k,B){return o=o.stateNode,typeof o.shouldComponentUpdate=="function"?o.shouldComponentUpdate(g,k,B):a.prototype&&a.prototype.isPureReactComponent?!ws(d,g)||!ws(E,k):!0}function im(o,a,d,g){o=a.state,typeof a.componentWillReceiveProps=="function"&&a.componentWillReceiveProps(d,g),typeof a.UNSAFE_componentWillReceiveProps=="function"&&a.UNSAFE_componentWillReceiveProps(d,g),a.state!==o&&Dh.enqueueReplaceState(a,a.state,null)}function Kn(o,a){var d=a;if("ref"in a){d={};for(var g in a)g!=="ref"&&(d[g]=a[g])}if(o=o.defaultProps){d===a&&(d=p({},d));for(var E in o)d[E]===void 0&&(d[E]=o[E])}return d}var Iu=typeof reportError=="function"?reportError:function(o){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var a=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof o=="object"&&o!==null&&typeof o.message=="string"?String(o.message):String(o),error:o});if(!window.dispatchEvent(a))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",o);return}console.error(o)};function Fe(o){Iu(o)}function Oh(o){console.error(o)}function qo(o){Iu(o)}function tf(o,a){try{var d=o.onUncaughtError;d(a.value,{componentStack:a.stack})}catch(g){setTimeout(function(){throw g})}}function Hs(o,a,d){try{var g=o.onCaughtError;g(d.value,{componentStack:d.stack,errorBoundary:a.tag===1?a.stateNode:null})}catch(E){setTimeout(function(){throw E})}}function nf(o,a,d){return d=Pt(d),d.tag=3,d.payload={element:null},d.callback=function(){tf(o,a)},d}function Id(o){return o=Pt(o),o.tag=3,o}function Rh(o,a,d,g){var E=d.type.getDerivedStateFromError;if(typeof E=="function"){var k=g.value;o.payload=function(){return E(k)},o.callback=function(){Hs(a,d,g)}}var B=d.stateNode;B!==null&&typeof B.componentDidCatch=="function"&&(o.callback=function(){Hs(a,d,g),typeof E!="function"&&(Si===null?Si=new Set([this]):Si.add(this));var H=g.stack;this.componentDidCatch(g.value,{componentStack:H!==null?H:""})})}function Dv(o,a,d,g,E){if(d.flags|=32768,g!==null&&typeof g=="object"&&typeof g.then=="function"){if(a=d.alternate,a!==null&&ks(a,d,E,!0),d=Fr.current,d!==null){switch(d.tag){case 13:return Go===null?Gd():d.alternate===null&&_n===0&&(_n=3),d.flags&=-257,d.flags|=65536,d.lanes=E,g===ch?d.flags|=16384:(a=d.updateQueue,a===null?d.updateQueue=new Set([g]):a.add(g),Wh(o,g,E)),!1;case 22:return d.flags|=65536,g===ch?d.flags|=16384:(a=d.updateQueue,a===null?(a={transitions:null,markerInstances:null,retryQueue:new Set([g])},d.updateQueue=a):(d=a.retryQueue,d===null?a.retryQueue=new Set([g]):d.add(g)),Wh(o,g,E)),!1}throw Error(S(435,d.tag))}return Wh(o,g,E),Gd(),!1}if($t)return a=Fr.current,a!==null?((a.flags&65536)===0&&(a.flags|=256),a.flags|=65536,a.lanes=E,g!==ad&&(o=Error(S(422),{cause:g}),gu(Ni(o,d)))):(g!==ad&&(a=Error(S(423),{cause:g}),gu(Ni(a,d))),o=o.current.alternate,o.flags|=65536,E&=-E,o.lanes|=E,g=Ni(g,d),E=nf(o.stateNode,g,E),wu(o,E),_n!==4&&(_n=2)),!1;var k=Error(S(520),{cause:g});if(k=Ni(k,d),Ys===null?Ys=[k]:Ys.push(k),_n!==4&&(_n=2),a===null)return!0;g=Ni(g,d),d=a;do{switch(d.tag){case 3:return d.flags|=65536,o=E&-E,d.lanes|=o,o=nf(d.stateNode,g,o),wu(d,o),!1;case 1:if(a=d.type,k=d.stateNode,(d.flags&128)===0&&(typeof a.getDerivedStateFromError=="function"||k!==null&&typeof k.componentDidCatch=="function"&&(Si===null||!Si.has(k))))return d.flags|=65536,E&=-E,d.lanes|=E,E=Id(E),Rh(E,o,d,g),wu(d,E),!1}d=d.return}while(d!==null);return!1}var zo=Error(S(461)),ni=!1;function gi(o,a,d,g){a.child=o===null?tm(a,null,d,g):Vs(a,o.child,d,g)}function rf(o,a,d,g,E){d=d.render;var k=a.ref;if("ref"in g){var B={};for(var H in g)H!=="ref"&&(B[H]=g[H])}else B=g;return Bo(a),g=dd(o,a,d,B,k,E),H=pd(),o!==null&&!ni?(xa(o,a,E),Me(o,a,E)):($t&&H&&Bc(a),a.flags|=1,gi(o,a,g,E),a.child)}function Ks(o,a,d,g,E){if(o===null){var k=d.type;return typeof k=="function"&&!nh(k)&&k.defaultProps===void 0&&d.compare===null?(a.tag=15,a.type=k,ku(o,a,k,g,E)):(o=ud(d.type,null,g,a,a.mode,E),o.ref=a.ref,o.return=a,a.child=o)}if(k=o.child,!Ou(o,E)){var B=k.memoizedProps;if(d=d.compare,d=d!==null?d:ws,d(B,g)&&o.ref===a.ref)return Me(o,a,E)}return a.flags|=1,o=Cl(k,g),o.ref=a.ref,o.return=a,a.child=o}function ku(o,a,d,g,E){if(o!==null){var k=o.memoizedProps;if(ws(k,g)&&o.ref===a.ref)if(ni=!1,a.pendingProps=g=k,Ou(o,E))(o.flags&131072)!==0&&(ni=!0);else return a.lanes=o.lanes,Me(o,a,E)}return of(o,a,d,g,E)}function rm(o,a,d){var g=a.pendingProps,E=g.children,k=o!==null?o.memoizedState:null;if(g.mode==="hidden"){if((a.flags&128)!==0){if(g=k!==null?k.baseLanes|d:d,o!==null){for(E=a.child=o.child,k=0;E!==null;)k=k|E.lanes|E.childLanes,E=E.sibling;a.childLanes=k&~g}else a.childLanes=0,a.child=null;return kd(o,a,g,d)}if((d&536870912)!==0)a.memoizedState={baseLanes:0,cachePool:null},o!==null&&Vo(a,k!==null?k.cachePool:null),k!==null?Li(a,k):Ms(),nm(a);else return a.lanes=a.childLanes=536870912,kd(o,a,k!==null?k.baseLanes|d:d,d)}else k!==null?(Vo(a,k.cachePool),Li(a,k),Fo(),a.memoizedState=null):(o!==null&&Vo(a,null),Ms(),Fo());return gi(o,a,E,d),a.child}function kd(o,a,d,g){var E=vu();return E=E===null?null:{parent:ct._currentValue,pool:E},a.memoizedState={baseLanes:d,cachePool:E},o!==null&&Vo(a,null),Ms(),nm(a),o!==null&&ks(o,a,g,!0),null}function Ll(o,a){var d=a.ref;if(d===null)o!==null&&o.ref!==null&&(a.flags|=4194816);else{if(typeof d!="function"&&typeof d!="object")throw Error(S(284));(o===null||o.ref!==d)&&(a.flags|=4194816)}}function of(o,a,d,g,E){return Bo(a),d=dd(o,a,d,g,void 0,E),g=pd(),o!==null&&!ni?(xa(o,a,E),Me(o,a,E)):($t&&g&&Bc(a),a.flags|=1,gi(o,a,d,E),a.child)}function Mh(o,a,d,g,E,k){return Bo(a),a.updateQueue=null,d=Bg(a,g,d,E),jg(o),g=pd(),o!==null&&!ni?(xa(o,a,k),Me(o,a,k)):($t&&g&&Bc(a),a.flags|=1,gi(o,a,d,k),a.child)}function Nh(o,a,d,g,E){if(Bo(a),a.stateNode===null){var k=pa,B=d.contextType;typeof B=="object"&&B!==null&&(k=si(B)),k=new d(g,k),a.memoizedState=k.state!==null&&k.state!==void 0?k.state:null,k.updater=Dh,a.stateNode=k,k._reactInternals=a,k=a.stateNode,k.props=g,k.state=a.memoizedState,k.refs={},Rs(a),B=d.contextType,k.context=typeof B=="object"&&B!==null?si(B):pa,k.state=a.memoizedState,B=d.getDerivedStateFromProps,typeof B=="function"&&(kh(a,d,B,g),k.state=a.memoizedState),typeof d.getDerivedStateFromProps=="function"||typeof k.getSnapshotBeforeUpdate=="function"||typeof k.UNSAFE_componentWillMount!="function"&&typeof k.componentWillMount!="function"||(B=k.state,typeof k.componentWillMount=="function"&&k.componentWillMount(),typeof k.UNSAFE_componentWillMount=="function"&&k.UNSAFE_componentWillMount(),B!==k.state&&Dh.enqueueReplaceState(k,k.state,null),Ir(a,g,k,E),Bt(),k.state=a.memoizedState),typeof k.componentDidMount=="function"&&(a.flags|=4194308),g=!0}else if(o===null){k=a.stateNode;var H=a.memoizedProps,ie=Kn(d,H);k.props=ie;var we=k.context,F=d.contextType;B=pa,typeof F=="object"&&F!==null&&(B=si(F));var Ie=d.getDerivedStateFromProps;F=typeof Ie=="function"||typeof k.getSnapshotBeforeUpdate=="function",H=a.pendingProps!==H,F||typeof k.UNSAFE_componentWillReceiveProps!="function"&&typeof k.componentWillReceiveProps!="function"||(H||we!==B)&&im(a,k,g,B),Dl=!1;var Te=a.memoizedState;k.state=Te,Ir(a,g,k,E),Bt(),we=a.memoizedState,H||Te!==we||Dl?(typeof Ie=="function"&&(kh(a,d,Ie,g),we=a.memoizedState),(ie=Dl||ef(a,d,ie,g,Te,we,B))?(F||typeof k.UNSAFE_componentWillMount!="function"&&typeof k.componentWillMount!="function"||(typeof k.componentWillMount=="function"&&k.componentWillMount(),typeof k.UNSAFE_componentWillMount=="function"&&k.UNSAFE_componentWillMount()),typeof k.componentDidMount=="function"&&(a.flags|=4194308)):(typeof k.componentDidMount=="function"&&(a.flags|=4194308),a.memoizedProps=g,a.memoizedState=we),k.props=g,k.state=we,k.context=B,g=ie):(typeof k.componentDidMount=="function"&&(a.flags|=4194308),g=!1)}else{k=a.stateNode,Su(o,a),B=a.memoizedProps,F=Kn(d,B),k.props=F,Ie=a.pendingProps,Te=k.context,we=d.contextType,ie=pa,typeof we=="object"&&we!==null&&(ie=si(we)),H=d.getDerivedStateFromProps,(we=typeof H=="function"||typeof k.getSnapshotBeforeUpdate=="function")||typeof k.UNSAFE_componentWillReceiveProps!="function"&&typeof k.componentWillReceiveProps!="function"||(B!==Ie||Te!==ie)&&im(a,k,g,ie),Dl=!1,Te=a.memoizedState,k.state=Te,Ir(a,g,k,E),Bt();var de=a.memoizedState;B!==Ie||Te!==de||Dl||o!==null&&o.dependencies!==null&&Ds(o.dependencies)?(typeof H=="function"&&(kh(a,d,H,g),de=a.memoizedState),(F=Dl||ef(a,d,F,g,Te,de,ie)||o!==null&&o.dependencies!==null&&Ds(o.dependencies))?(we||typeof k.UNSAFE_componentWillUpdate!="function"&&typeof k.componentWillUpdate!="function"||(typeof k.componentWillUpdate=="function"&&k.componentWillUpdate(g,de,ie),typeof k.UNSAFE_componentWillUpdate=="function"&&k.UNSAFE_componentWillUpdate(g,de,ie)),typeof k.componentDidUpdate=="function"&&(a.flags|=4),typeof k.getSnapshotBeforeUpdate=="function"&&(a.flags|=1024)):(typeof k.componentDidUpdate!="function"||B===o.memoizedProps&&Te===o.memoizedState||(a.flags|=4),typeof k.getSnapshotBeforeUpdate!="function"||B===o.memoizedProps&&Te===o.memoizedState||(a.flags|=1024),a.memoizedProps=g,a.memoizedState=de),k.props=g,k.state=de,k.context=ie,g=F):(typeof k.componentDidUpdate!="function"||B===o.memoizedProps&&Te===o.memoizedState||(a.flags|=4),typeof k.getSnapshotBeforeUpdate!="function"||B===o.memoizedProps&&Te===o.memoizedState||(a.flags|=1024),g=!1)}return k=g,Ll(o,a),g=(a.flags&128)!==0,k||g?(k=a.stateNode,d=g&&typeof d.getDerivedStateFromError!="function"?null:k.render(),a.flags|=1,o!==null&&g?(a.child=Vs(a,o.child,null,E),a.child=Vs(a,null,d,E)):gi(o,a,d,E),a.memoizedState=k.state,o=a.child):o=Me(o,a,E),o}function Dd(o,a,d,g){return yu(),a.flags|=256,gi(o,a,d,g),a.child}var Ph={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function _l(o){return{baseLanes:o,cachePool:en()}}function Du(o,a,d){return o=o!==null?o.childLanes&~d:0,a&&(o|=Or),o}function Lh(o,a,d){var g=a.pendingProps,E=!1,k=(a.flags&128)!==0,B;if((B=k)||(B=o!==null&&o.memoizedState===null?!1:(ci.current&2)!==0),B&&(E=!0,a.flags&=-129),B=(a.flags&32)!==0,a.flags&=-33,o===null){if($t){if(E?vo(a):Fo(),$t){var H=On,ie;if(ie=H){e:{for(ie=H,H=Wi;ie.nodeType!==8;){if(!H){H=null;break e}if(ie=xo(ie.nextSibling),ie===null){H=null;break e}}H=ie}H!==null?(a.memoizedState={dehydrated:H,treeContext:ya!==null?{id:bl,overflow:Il}:null,retryLane:536870912,hydrationErrors:null},ie=cr(18,null,null,0),ie.stateNode=H,ie.return=a,a.child=ie,Pi=a,On=null,ie=!0):ie=!1}ie||hu(a)}if(H=a.memoizedState,H!==null&&(H=H.dehydrated,H!==null))return gf(H)?a.lanes=32:a.lanes=536870912,null;Pl(a)}return H=g.children,g=g.fallback,E?(Fo(),E=a.mode,H=Gs({mode:"hidden",children:H},E),g=du(g,E,d,null),H.return=a,g.return=a,H.sibling=g,a.child=H,E=a.child,E.memoizedState=_l(d),E.childLanes=Du(o,B,d),a.memoizedState=Ph,g):(vo(a),So(a,H))}if(ie=o.memoizedState,ie!==null&&(H=ie.dehydrated,H!==null)){if(k)a.flags&256?(vo(a),a.flags&=-257,a=Ei(o,a,d)):a.memoizedState!==null?(Fo(),a.child=o.child,a.flags|=128,a=null):(Fo(),E=g.fallback,H=a.mode,g=Gs({mode:"visible",children:g.children},H),E=du(E,H,d,null),E.flags|=2,g.return=a,E.return=a,g.sibling=E,a.child=g,Vs(a,o.child,null,d),g=a.child,g.memoizedState=_l(d),g.childLanes=Du(o,B,d),a.memoizedState=Ph,a=E);else if(vo(a),gf(H)){if(B=H.nextSibling&&H.nextSibling.dataset,B)var we=B.dgst;B=we,g=Error(S(419)),g.stack="",g.digest=B,gu({value:g,source:null,stack:null}),a=Ei(o,a,d)}else if(ni||ks(o,a,d,!1),B=(d&o.childLanes)!==0,ni||B){if(B=En,B!==null&&(g=d&-d,g=(g&42)!==0?1:tu(g),g=(g&(B.suspendedLanes|d))!==0?0:g,g!==0&&g!==ie.retryLane))throw ie.retryLane=g,Al(o,g),Rr(B,o,g),zo;H.data==="$?"||Gd(),a=Ei(o,a,d)}else H.data==="$?"?(a.flags|=192,a.child=o.child,a=null):(o=ie.treeContext,On=xo(H.nextSibling),Pi=a,$t=!0,_o=null,Wi=!1,o!==null&&(Yi[Er++]=bl,Yi[Er++]=Il,Yi[Er++]=ya,bl=o.id,Il=o.overflow,ya=a),a=So(a,g.children),a.flags|=4096);return a}return E?(Fo(),E=g.fallback,H=a.mode,ie=o.child,we=ie.sibling,g=Cl(ie,{mode:"hidden",children:g.children}),g.subtreeFlags=ie.subtreeFlags&65011712,we!==null?E=Cl(we,E):(E=du(E,H,d,null),E.flags|=2),E.return=a,g.return=a,g.sibling=E,a.child=g,g=E,E=a.child,H=o.child.memoizedState,H===null?H=_l(d):(ie=H.cachePool,ie!==null?(we=ct._currentValue,ie=ie.parent!==we?{parent:we,pool:we}:ie):ie=en(),H={baseLanes:H.baseLanes|d,cachePool:ie}),E.memoizedState=H,E.childLanes=Du(o,B,d),a.memoizedState=Ph,g):(vo(a),d=o.child,o=d.sibling,d=Cl(d,{mode:"visible",children:g.children}),d.return=a,d.sibling=null,o!==null&&(B=a.deletions,B===null?(a.deletions=[o],a.flags|=16):B.push(o)),a.child=d,a.memoizedState=null,d)}function So(o,a){return a=Gs({mode:"visible",children:a},o.mode),a.return=o,o.child=a}function Gs(o,a){return o=cr(22,o,null,a),o.lanes=0,o.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},o}function Ei(o,a,d){return Vs(a,o.child,null,d),o=So(a,a.pendingProps.children),o.flags|=2,a.memoizedState=null,o}function St(o,a,d){o.lanes|=a;var g=o.alternate;g!==null&&(g.lanes|=a),ah(o.return,a,d)}function lf(o,a,d,g,E){var k=o.memoizedState;k===null?o.memoizedState={isBackwards:a,rendering:null,renderingStartTime:0,last:g,tail:d,tailMode:E}:(k.isBackwards=a,k.rendering=null,k.renderingStartTime=0,k.last=g,k.tail=d,k.tailMode=E)}function Ai(o,a,d){var g=a.pendingProps,E=g.revealOrder,k=g.tail;if(gi(o,a,g.children,d),g=ci.current,(g&2)!==0)g=g&1|2,a.flags|=128;else{if(o!==null&&(o.flags&128)!==0)e:for(o=a.child;o!==null;){if(o.tag===13)o.memoizedState!==null&&St(o,d,a);else if(o.tag===19)St(o,d,a);else if(o.child!==null){o.child.return=o,o=o.child;continue}if(o===a)break e;for(;o.sibling===null;){if(o.return===null||o.return===a)break e;o=o.return}o.sibling.return=o.return,o=o.sibling}g&=1}switch(ze(ci,g),E){case"forwards":for(d=a.child,E=null;d!==null;)o=d.alternate,o!==null&&bd(o)===null&&(E=d),d=d.sibling;d=E,d===null?(E=a.child,a.child=null):(E=d.sibling,d.sibling=null),lf(a,!1,E,d,k);break;case"backwards":for(d=null,E=a.child,a.child=null;E!==null;){if(o=E.alternate,o!==null&&bd(o)===null){a.child=E;break}o=E.sibling,E.sibling=d,d=E,E=o}lf(a,!0,d,null,k);break;case"together":lf(a,!1,null,null,void 0);break;default:a.memoizedState=null}return a.child}function Me(o,a,d){if(o!==null&&(a.dependencies=o.dependencies),Vl|=a.lanes,(d&a.childLanes)===0)if(o!==null){if(ks(o,a,d,!1),(d&a.childLanes)===0)return null}else return null;if(o!==null&&a.child!==o.child)throw Error(S(153));if(a.child!==null){for(o=a.child,d=Cl(o,o.pendingProps),a.child=d,d.return=a;o.sibling!==null;)o=o.sibling,d=d.sibling=Cl(o,o.pendingProps),d.return=a;d.sibling=null}return a.child}function Ou(o,a){return(o.lanes&a)!==0?!0:(o=o.dependencies,!!(o!==null&&Ds(o)))}function Ke(o,a,d){switch(a.tag){case 3:Tn(a,a.stateNode.containerInfo),jo(a,ct,o.memoizedState.cache),yu();break;case 27:case 5:Up(a);break;case 4:Tn(a,a.stateNode.containerInfo);break;case 10:jo(a,a.type,a.memoizedProps.value);break;case 13:var g=a.memoizedState;if(g!==null)return g.dehydrated!==null?(vo(a),a.flags|=128,null):(d&a.child.childLanes)!==0?Lh(o,a,d):(vo(a),o=Me(o,a,d),o!==null?o.sibling:null);vo(a);break;case 19:var E=(o.flags&128)!==0;if(g=(d&a.childLanes)!==0,g||(ks(o,a,d,!1),g=(d&a.childLanes)!==0),E){if(g)return Ai(o,a,d);a.flags|=128}if(E=a.memoizedState,E!==null&&(E.rendering=null,E.tail=null,E.lastEffect=null),ze(ci,ci.current),g)break;return null;case 22:case 23:return a.lanes=0,rm(o,a,d);case 24:jo(a,ct,o.memoizedState.cache)}return Me(o,a,d)}function Nt(o,a,d){if(o!==null)if(o.memoizedProps!==a.pendingProps)ni=!0;else{if(!Ou(o,d)&&(a.flags&128)===0)return ni=!1,Ke(o,a,d);ni=(o.flags&131072)!==0}else ni=!1,$t&&(a.flags&1048576)!==0&&pu(a,jc,a.index);switch(a.lanes=0,a.tag){case 16:e:{o=a.pendingProps;var g=a.elementType,E=g._init;if(g=E(g._payload),a.type=g,typeof g=="function")nh(g)?(o=Kn(g,o),a.tag=1,a=Nh(null,a,g,o,d)):(a.tag=0,a=of(null,a,g,o,d));else{if(g!=null){if(E=g.$$typeof,E===Ne){a.tag=11,a=rf(null,a,g,o,d);break e}else if(E===it){a.tag=14,a=Ks(null,a,g,o,d);break e}}throw a=_e(g)||g,Error(S(306,a,""))}}return a;case 0:return of(o,a,a.type,a.pendingProps,d);case 1:return g=a.type,E=Kn(g,a.pendingProps),Nh(o,a,g,E,d);case 3:e:{if(Tn(a,a.stateNode.containerInfo),o===null)throw Error(S(387));g=a.pendingProps;var k=a.memoizedState;E=k.element,Su(o,a),Ir(a,g,null,d);var B=a.memoizedState;if(g=B.cache,jo(a,ct,g),g!==k.cache&&sd(a,[ct],d,!0),Bt(),g=B.element,k.isDehydrated)if(k={element:g,isDehydrated:!1,cache:B.cache},a.updateQueue.baseState=k,a.memoizedState=k,a.flags&256){a=Dd(o,a,g,d);break e}else if(g!==E){E=Ni(Error(S(424)),a),gu(E),a=Dd(o,a,g,d);break e}else{switch(o=a.stateNode.containerInfo,o.nodeType){case 9:o=o.body;break;default:o=o.nodeName==="HTML"?o.ownerDocument.body:o}for(On=xo(o.firstChild),Pi=a,$t=!0,_o=null,Wi=!0,d=tm(a,null,g,d),a.child=d;d;)d.flags=d.flags&-3|4096,d=d.sibling}else{if(yu(),g===E){a=Me(o,a,d);break e}gi(o,a,g,d)}a=a.child}return a;case 26:return Ll(o,a),o===null?(d=ay(a.type,null,a.pendingProps,null))?a.memoizedState=d:$t||(d=a.type,o=a.pendingProps,g=$d(mt.current).createElement(d),g[Di]=a,g[ir]=o,bi(g,d,o),Y(g),a.stateNode=g):a.memoizedState=ay(a.type,o.memoizedProps,a.pendingProps,o.memoizedState),null;case 27:return Up(a),o===null&&$t&&(g=a.stateNode=ly(a.type,a.pendingProps,mt.current),Pi=a,Wi=!0,E=On,To(a.type)?(ip=E,On=xo(g.firstChild)):On=E),gi(o,a,a.pendingProps.children,d),Ll(o,a),o===null&&(a.flags|=4194304),a.child;case 5:return o===null&&$t&&((E=g=On)&&(g=Bv(g,a.type,a.pendingProps,Wi),g!==null?(a.stateNode=g,Pi=a,On=xo(g.firstChild),Wi=!1,E=!0):E=!1),E||hu(a)),Up(a),E=a.type,k=a.pendingProps,B=o!==null?o.memoizedProps:null,g=k.children,ry(E,k)?g=null:B!==null&&ry(E,B)&&(a.flags|=32),a.memoizedState!==null&&(E=dd(o,a,Iv,null,null,d),pc._currentValue=E),Ll(o,a),gi(o,a,g,d),a.child;case 6:return o===null&&$t&&((o=d=On)&&(d=Vv(d,a.pendingProps,Wi),d!==null?(a.stateNode=d,Pi=a,On=null,o=!0):o=!1),o||hu(a)),null;case 13:return Lh(o,a,d);case 4:return Tn(a,a.stateNode.containerInfo),g=a.pendingProps,o===null?a.child=Vs(a,null,g,d):gi(o,a,g,d),a.child;case 11:return rf(o,a,a.type,a.pendingProps,d);case 7:return gi(o,a,a.pendingProps,d),a.child;case 8:return gi(o,a,a.pendingProps.children,d),a.child;case 12:return gi(o,a,a.pendingProps.children,d),a.child;case 10:return g=a.pendingProps,jo(a,a.type,g.value),gi(o,a,g.children,d),a.child;case 9:return E=a.type._context,g=a.pendingProps.children,Bo(a),E=si(E),g=g(E),a.flags|=1,gi(o,a,g,d),a.child;case 14:return Ks(o,a,a.type,a.pendingProps,d);case 15:return ku(o,a,a.type,a.pendingProps,d);case 19:return Ai(o,a,d);case 31:return g=a.pendingProps,d=a.mode,g={mode:g.mode,children:g.children},o===null?(d=Gs(g,d),d.ref=a.ref,a.child=d,d.return=a,a=d):(d=Cl(o.child,g),d.ref=a.ref,a.child=d,d.return=a,a=d),a;case 22:return rm(o,a,d);case 24:return Bo(a),g=si(ct),o===null?(E=vu(),E===null&&(E=En,k=vn(),E.pooledCache=k,k.refCount++,k!==null&&(E.pooledCacheLanes|=d),E=k),a.memoizedState={parent:g,cache:E},Rs(a),jo(a,ct,E)):((o.lanes&d)!==0&&(Su(o,a),Ir(a,null,null,d),Bt()),E=o.memoizedState,k=a.memoizedState,E.parent!==g?(E={parent:g,cache:g},a.memoizedState=E,a.lanes===0&&(a.memoizedState=a.updateQueue.baseState=E),jo(a,ct,g)):(g=k.cache,jo(a,ct,g),g!==E.cache&&sd(a,[ct],d,!0))),gi(o,a,a.pendingProps.children,d),a.child;case 29:throw a.pendingProps}throw Error(S(156,a.tag))}function qr(o){o.flags|=4}function om(o,a){if(a.type!=="stylesheet"||(a.state.loading&4)!==0)o.flags&=-16777217;else if(o.flags|=16777216,!fy(a)){if(a=Fr.current,a!==null&&((nn&4194048)===nn?Go!==null:(nn&62914560)!==nn&&(nn&536870912)===0||a!==Go))throw Os=ch,sh;o.flags|=8192}}function Vt(o,a){a!==null&&(o.flags|=4),o.flags&16384&&(a=o.tag!==22?gg():536870912,o.lanes|=a,Na|=a)}function Ia(o,a){if(!$t)switch(o.tailMode){case"hidden":a=o.tail;for(var d=null;a!==null;)a.alternate!==null&&(d=a),a=a.sibling;d===null?o.tail=null:d.sibling=null;break;case"collapsed":d=o.tail;for(var g=null;d!==null;)d.alternate!==null&&(g=d),d=d.sibling;g===null?a||o.tail===null?o.tail=null:o.tail.sibling=null:g.sibling=null}}function Mn(o){var a=o.alternate!==null&&o.alternate.child===o.child,d=0,g=0;if(a)for(var E=o.child;E!==null;)d|=E.lanes|E.childLanes,g|=E.subtreeFlags&65011712,g|=E.flags&65011712,E.return=o,E=E.sibling;else for(E=o.child;E!==null;)d|=E.lanes|E.childLanes,g|=E.subtreeFlags,g|=E.flags,E.return=o,E=E.sibling;return o.subtreeFlags|=g,o.childLanes=d,a}function Ov(o,a,d){var g=a.pendingProps;switch(bs(a),a.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Mn(a),null;case 1:return Mn(a),null;case 3:return d=a.stateNode,g=null,o!==null&&(g=o.memoizedState.cache),a.memoizedState.cache!==g&&(a.flags|=2048),kl(ct),pl(),d.pendingContext&&(d.context=d.pendingContext,d.pendingContext=null),(o===null||o.child===null)&&(Is(a)?qr(a):o===null||o.memoizedState.isDehydrated&&(a.flags&256)===0||(a.flags|=1024,Ng())),Mn(a),null;case 26:return d=a.memoizedState,o===null?(qr(a),d!==null?(Mn(a),om(a,d)):(Mn(a),a.flags&=-16777217)):d?d!==o.memoizedState?(qr(a),Mn(a),om(a,d)):(Mn(a),a.flags&=-16777217):(o.memoizedProps!==g&&qr(a),Mn(a),a.flags&=-16777217),null;case 27:Uf(a),d=mt.current;var E=a.type;if(o!==null&&a.stateNode!=null)o.memoizedProps!==g&&qr(a);else{if(!g){if(a.stateNode===null)throw Error(S(166));return Mn(a),null}o=G.current,Is(a)?oh(a):(o=ly(E,g,d),a.stateNode=o,qr(a))}return Mn(a),null;case 5:if(Uf(a),d=a.type,o!==null&&a.stateNode!=null)o.memoizedProps!==g&&qr(a);else{if(!g){if(a.stateNode===null)throw Error(S(166));return Mn(a),null}if(o=G.current,Is(a))oh(a);else{switch(E=$d(mt.current),o){case 1:o=E.createElementNS("http://www.w3.org/2000/svg",d);break;case 2:o=E.createElementNS("http://www.w3.org/1998/Math/MathML",d);break;default:switch(d){case"svg":o=E.createElementNS("http://www.w3.org/2000/svg",d);break;case"math":o=E.createElementNS("http://www.w3.org/1998/Math/MathML",d);break;case"script":o=E.createElement("div"),o.innerHTML=" - - - -
- - diff --git a/cdrm-frontend/dist/og-api.jpg b/cdrm-frontend/dist/og-api.jpg deleted file mode 100644 index dcfa99c..0000000 Binary files a/cdrm-frontend/dist/og-api.jpg and /dev/null differ diff --git a/cdrm-frontend/dist/og-cache.jpg b/cdrm-frontend/dist/og-cache.jpg deleted file mode 100644 index 0b4715c..0000000 Binary files a/cdrm-frontend/dist/og-cache.jpg and /dev/null differ diff --git a/cdrm-frontend/dist/og-home.jpg b/cdrm-frontend/dist/og-home.jpg deleted file mode 100644 index 295179c..0000000 Binary files a/cdrm-frontend/dist/og-home.jpg and /dev/null differ diff --git a/cdrm-frontend/dist/og-testplayer.jpg b/cdrm-frontend/dist/og-testplayer.jpg deleted file mode 100644 index 24a1b6c..0000000 Binary files a/cdrm-frontend/dist/og-testplayer.jpg and /dev/null differ diff --git a/configs/icon_links.py b/configs/icon_links.py index 34914ae..01662dd 100644 --- a/configs/icon_links.py +++ b/configs/icon_links.py @@ -1,5 +1,5 @@ data = { - 'discord': 'https://discord.cdrm-project.com/', - 'telegram': 'https://telegram.cdrm-project.com/', - 'gitea': 'https://cdm-project.com/tpd94/cdrm-project' -} \ No newline at end of file + "discord": "https://discord.cdrm-project.com/", + "telegram": "https://telegram.cdrm-project.com/", + "gitea": "https://cdm-project.com/tpd94/cdrm-project", +} diff --git a/configs/index_tags.py b/configs/index_tags.py index aed1510..2639ab1 100644 --- a/configs/index_tags.py +++ b/configs/index_tags.py @@ -1,47 +1,47 @@ tags = { - 'index': { - 'description': 'Decrypt Widevine and PlayReady protected content', - 'keywords': 'CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption', - 'opengraph_title': 'CDRM-Project', - 'opengraph_description': 'Self Hosted web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library to decrypt Widevine & Playready content', - 'opengraph_image': 'https://cdrm-project.com/og-home.jpg', - 'opengraph_url': 'https://cdm-project.com/tpd94/cdrm-project', - 'tab_title': 'CDRM-Project', + "index": { + "description": "Decrypt Widevine and PlayReady protected content", + "keywords": "CDRM, Widevine, PlayReady, DRM, Decrypt, CDM, CDM-Project, CDRM-Project, TPD94, Decryption", + "opengraph_title": "CDRM-Project", + "opengraph_description": "Self Hosted web application written in Python/JavaScript utilizing the Flask/Tailwind Framework and ReactJS library to decrypt Widevine & Playready content", + "opengraph_image": "https://cdrm-project.com/og-home.jpg", + "opengraph_url": "https://cdm-project.com/tpd94/cdrm-project", + "tab_title": "CDRM-Project", }, - 'cache': { - 'description': 'Search the cache by KID or PSSH for decryption keys', - 'keywords': 'Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption', - 'opengraph_title': 'Search the Cache', - 'opengraph_description': 'Search the cache by KID or PSSH for decryption keys', - 'opengraph_image': 'https://cdrm-project.com/og-cache.jpg', - 'opengraph_url': 'https://cdrm-project.com/cache', - 'tab_title': 'Cache', + "cache": { + "description": "Search the cache by KID or PSSH for decryption keys", + "keywords": "Cache, Vault, Widevine, PlayReady, DRM, Decryption, CDM, CDRM-Project, CDRM-Project, TPD94, Decryption", + "opengraph_title": "Search the Cache", + "opengraph_description": "Search the cache by KID or PSSH for decryption keys", + "opengraph_image": "https://cdrm-project.com/og-cache.jpg", + "opengraph_url": "https://cdrm-project.com/cache", + "tab_title": "Cache", }, - 'testplayer': { - 'description': 'Shaka Player for testing decryption keys', - 'keywords': 'Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY', - 'opengraph_title': 'Test Player', - 'opengraph_description': 'Shaka Player for testing decryption keys', - 'opengraph_image': 'https://cdrm-project.com/og-testplayer.jpg', - 'opengraph_url': 'https://cdrm-project.com/testplayer', - 'tab_title': 'Test Player', + "testplayer": { + "description": "Shaka Player for testing decryption keys", + "keywords": "Shaka, Player, DRM, CDRM, CDM, CDRM-Project, TPD94, Decryption, CDM-Project, KID, KEY", + "opengraph_title": "Test Player", + "opengraph_description": "Shaka Player for testing decryption keys", + "opengraph_image": "https://cdrm-project.com/og-testplayer.jpg", + "opengraph_url": "https://cdrm-project.com/testplayer", + "tab_title": "Test Player", }, - 'api': { - 'description': 'API documentation for the program "CDRM-Project"', - 'keywords': 'API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault', - 'opengraph_title': 'API', - 'opengraph_description': 'Documentation for the program "CDRM-Project"', - 'opengraph_image': 'https://cdrm-project.com/og-api.jpg', - 'opengraph_url': 'https://cdrm-project.com/api', - 'tab_title': 'API', + "api": { + "description": 'API documentation for the program "CDRM-Project"', + "keywords": "API, python, requests, send, remotecdm, remote, cdm, CDM-Project, CDRM-Project, TPD94, Decryption, DRM, Web, Vault", + "opengraph_title": "API", + "opengraph_description": 'Documentation for the program "CDRM-Project"', + "opengraph_image": "https://cdrm-project.com/og-api.jpg", + "opengraph_url": "https://cdrm-project.com/api", + "tab_title": "API", }, - 'account': { - 'description': 'Account for CDRM-Project', - 'keywords': 'Login, CDRM, CDM, CDRM-Project, register, account', - 'opengraph_title': 'My account', - 'opengraph_description': 'Account for CDRM-Project', - 'opengraph_image': 'https://cdrm-project.com/og-home.jpg', - 'opengraph_url': 'https://cdrm-project.com/account', - 'tab_title': 'My account', - } -} \ No newline at end of file + "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", + }, +} diff --git a/custom_functions/database/cache_to_db_mariadb.py b/custom_functions/database/cache_to_db_mariadb.py index de18fd3..e25eb7b 100644 --- a/custom_functions/database/cache_to_db_mariadb.py +++ b/custom_functions/database/cache_to_db_mariadb.py @@ -4,16 +4,15 @@ import mysql.connector from mysql.connector import Error - def get_db_config(): # Configure your MariaDB connection - with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: + with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) db_config = { - 'host': f'{config["mariadb"]["host"]}', - 'user': f'{config["mariadb"]["user"]}', - 'password': f'{config["mariadb"]["password"]}', - 'database': f'{config["mariadb"]["database"]}' + "host": f'{config["mariadb"]["host"]}', + "user": f'{config["mariadb"]["user"]}', + "password": f'{config["mariadb"]["password"]}', + "database": f'{config["mariadb"]["database"]}', } return db_config @@ -22,7 +21,8 @@ def create_database(): try: with mysql.connector.connect(**get_db_config()) as conn: cursor = conn.cursor() - cursor.execute(''' + cursor.execute( + """ CREATE TABLE IF NOT EXISTS licenses ( SERVICE VARCHAR(255), PSSH TEXT, @@ -33,20 +33,32 @@ def create_database(): Cookies TEXT, Data BLOB ) - ''') + """ + ) conn.commit() except Error as e: print(f"Error: {e}") -def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, headers=None, cookies=None, data=None): + +def cache_to_db( + service=None, + pssh=None, + kid=None, + key=None, + license_url=None, + headers=None, + cookies=None, + data=None, +): try: with mysql.connector.connect(**get_db_config()) as conn: cursor = conn.cursor() - cursor.execute('SELECT 1 FROM licenses WHERE KID = %s', (kid,)) + cursor.execute("SELECT 1 FROM licenses WHERE KID = %s", (kid,)) existing_record = cursor.fetchone() - cursor.execute(''' + cursor.execute( + """ INSERT INTO licenses (SERVICE, PSSH, KID, `Key`, License_URL, Headers, Cookies, Data) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE @@ -57,7 +69,9 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h Headers = VALUES(Headers), Cookies = VALUES(Cookies), Data = VALUES(Data) - ''', (service, pssh, kid, key, license_url, headers, cookies, data)) + """, + (service, pssh, kid, key, license_url, headers, cookies, data), + ) conn.commit() return True if existing_record else False @@ -65,6 +79,7 @@ def cache_to_db(service=None, pssh=None, kid=None, key=None, license_url=None, h print(f"Error: {e}") return False + def search_by_pssh_or_kid(search_filter): results = set() try: @@ -72,54 +87,71 @@ def search_by_pssh_or_kid(search_filter): cursor = conn.cursor() like_filter = f"%{search_filter}%" - cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s', (like_filter,)) + cursor.execute( + "SELECT PSSH, KID, `Key` FROM licenses WHERE PSSH LIKE %s", + (like_filter,), + ) results.update(cursor.fetchall()) - cursor.execute('SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s', (like_filter,)) + cursor.execute( + "SELECT PSSH, KID, `Key` FROM licenses WHERE KID LIKE %s", + (like_filter,), + ) results.update(cursor.fetchall()) - final_results = [{'PSSH': row[0], 'KID': row[1], 'Key': row[2]} for row in results] + final_results = [ + {"PSSH": row[0], "KID": row[1], "Key": row[2]} for row in results + ] return final_results[:20] except Error as e: print(f"Error: {e}") return [] + def get_key_by_kid_and_service(kid, service): try: with mysql.connector.connect(**get_db_config()) as conn: cursor = conn.cursor() - cursor.execute('SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s', (kid, service)) + cursor.execute( + "SELECT `Key` FROM licenses WHERE KID = %s AND SERVICE = %s", + (kid, service), + ) result = cursor.fetchone() return result[0] if result else None except Error as e: print(f"Error: {e}") return None + def get_kid_key_dict(service_name): try: with mysql.connector.connect(**get_db_config()) as conn: cursor = conn.cursor() - cursor.execute('SELECT KID, `Key` FROM licenses WHERE SERVICE = %s', (service_name,)) + cursor.execute( + "SELECT KID, `Key` FROM licenses WHERE SERVICE = %s", (service_name,) + ) return {row[0]: row[1] for row in cursor.fetchall()} except Error as e: print(f"Error: {e}") return {} + def get_unique_services(): try: with mysql.connector.connect(**get_db_config()) as conn: cursor = conn.cursor() - cursor.execute('SELECT DISTINCT SERVICE FROM licenses') + cursor.execute("SELECT DISTINCT SERVICE FROM licenses") return [row[0] for row in cursor.fetchall()] except Error as e: print(f"Error: {e}") return [] + def key_count(): try: with mysql.connector.connect(**get_db_config()) as conn: cursor = conn.cursor() - cursor.execute('SELECT COUNT(KID) FROM licenses') + cursor.execute("SELECT COUNT(KID) FROM licenses") return cursor.fetchone()[0] except Error as e: print(f"Error: {e}") diff --git a/custom_functions/database/cache_to_db_sqlite.py b/custom_functions/database/cache_to_db_sqlite.py index 806e20b..101ba71 100644 --- a/custom_functions/database/cache_to_db_sqlite.py +++ b/custom_functions/database/cache_to_db_sqlite.py @@ -1,11 +1,13 @@ import sqlite3 import os + def create_database(): # Using with statement to manage the connection and cursor - with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: + with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn: cursor = conn.cursor() - cursor.execute(''' + cursor.execute( + """ CREATE TABLE IF NOT EXISTS licenses ( SERVICE TEXT, PSSH TEXT, @@ -16,92 +18,127 @@ def create_database(): Cookies TEXT, Data TEXT ) - ''') + """ + ) -def cache_to_db(service: str = None, pssh: str = None, kid: str = None, key: str = None, license_url: str = None, headers: str = None, cookies: str = None, data: str = None): - with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: + +def cache_to_db( + service: str = None, + pssh: str = None, + kid: str = None, + key: str = None, + license_url: str = None, + headers: str = None, + cookies: str = None, + data: str = None, +): + with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn: cursor = conn.cursor() # Check if the record with the given KID already exists - cursor.execute('''SELECT 1 FROM licenses WHERE KID = ?''', (kid,)) + cursor.execute("""SELECT 1 FROM licenses WHERE KID = ?""", (kid,)) existing_record = cursor.fetchone() # Insert or replace the record - cursor.execute(''' + cursor.execute( + """ INSERT OR REPLACE INTO licenses (SERVICE, PSSH, KID, Key, License_URL, Headers, Cookies, Data) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ''', (service, pssh, kid, key, license_url, headers, cookies, data)) + """, + (service, pssh, kid, key, license_url, headers, cookies, data), + ) # If the record was existing and updated, return True (updated), else return False (added) return True if existing_record else False + def search_by_pssh_or_kid(search_filter): # Using with statement to automatically close the connection - with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: + with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn: cursor = conn.cursor() # Initialize a set to store unique matching records results = set() # Search for records where PSSH contains the search_filter - cursor.execute(''' + cursor.execute( + """ SELECT * FROM licenses WHERE PSSH LIKE ? - ''', ('%' + search_filter + '%',)) + """, + ("%" + search_filter + "%",), + ) rows = cursor.fetchall() for row in rows: results.add((row[1], row[2], row[3])) # (PSSH, KID, Key) # Search for records where KID contains the search_filter - cursor.execute(''' + cursor.execute( + """ SELECT * FROM licenses WHERE KID LIKE ? - ''', ('%' + search_filter + '%',)) + """, + ("%" + search_filter + "%",), + ) rows = cursor.fetchall() for row in rows: results.add((row[1], row[2], row[3])) # (PSSH, KID, Key) # Convert the set of results to a list of dictionaries for output - final_results = [{'PSSH': result[0], 'KID': result[1], 'Key': result[2]} for result in results] + final_results = [ + {"PSSH": result[0], "KID": result[1], "Key": result[2]} + for result in results + ] return final_results[:20] + def get_key_by_kid_and_service(kid, service): # Using 'with' to automatically close the connection when done - with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: + with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn: cursor = conn.cursor() # Query to search by KID and SERVICE - cursor.execute(''' + cursor.execute( + """ SELECT Key FROM licenses WHERE KID = ? AND SERVICE = ? - ''', (kid, service)) + """, + (kid, service), + ) # Fetch the result result = cursor.fetchone() # Check if a result was found - return result[0] if result else None # The 'Key' is the first (and only) column returned in the result + return ( + result[0] if result else None + ) # The 'Key' is the first (and only) column returned in the result + def get_kid_key_dict(service_name): # Using with statement to automatically manage the connection and cursor - with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: + with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn: cursor = conn.cursor() # Query to fetch KID and Key for the selected service - cursor.execute(''' + cursor.execute( + """ SELECT KID, Key FROM licenses WHERE SERVICE = ? - ''', (service_name,)) + """, + (service_name,), + ) # Fetch all results and create the dictionary kid_key_dict = {row[0]: row[1] for row in cursor.fetchall()} return kid_key_dict + def get_unique_services(): # Using with statement to automatically manage the connection and cursor - with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: + with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn: cursor = conn.cursor() # Query to get distinct services from the 'licenses' table - cursor.execute('SELECT DISTINCT SERVICE FROM licenses') + cursor.execute("SELECT DISTINCT SERVICE FROM licenses") # Fetch all results and extract the unique services services = cursor.fetchall() @@ -111,13 +148,14 @@ def get_unique_services(): return unique_services + def key_count(): # Using with statement to automatically manage the connection and cursor - with sqlite3.connect(f'{os.getcwd()}/databases/sql/key_cache.db') as conn: + with sqlite3.connect(f"{os.getcwd()}/databases/sql/key_cache.db") as conn: cursor = conn.cursor() # Count the number of KID entries in the licenses table - cursor.execute('SELECT COUNT(KID) FROM licenses') + cursor.execute("SELECT COUNT(KID) FROM licenses") count = cursor.fetchone()[0] # Fetch the result and get the count return count diff --git a/custom_functions/database/user_db.py b/custom_functions/database/user_db.py index d668bf6..7895567 100644 --- a/custom_functions/database/user_db.py +++ b/custom_functions/database/user_db.py @@ -4,27 +4,32 @@ import bcrypt def create_user_database(): - os.makedirs(f'{os.getcwd()}/databases/sql', exist_ok=True) + os.makedirs(f"{os.getcwd()}/databases/sql", exist_ok=True) - with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: + with sqlite3.connect(f"{os.getcwd()}/databases/sql/users.db") as conn: cursor = conn.cursor() - cursor.execute(''' + cursor.execute( + """ CREATE TABLE IF NOT EXISTS user_info ( Username TEXT PRIMARY KEY, Password TEXT, Styled_Username TEXT, API_Key TEXT ) - ''') + """ + ) def add_user(username, password, api_key): - hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + hashed_pw = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) - with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: + with sqlite3.connect(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)) + cursor.execute( + "INSERT INTO user_info (Username, Password, Styled_Username, API_Key) VALUES (?, ?, ?, ?)", + (username.lower(), hashed_pw, username, api_key), + ) conn.commit() return True except sqlite3.IntegrityError: @@ -32,24 +37,29 @@ def add_user(username, password, api_key): def verify_user(username, password): - with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: + 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(),)) + 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) + 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: + 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(),)) + cursor.execute( + "SELECT API_Key FROM user_info WHERE Username = ?", (username.lower(),) + ) result = cursor.fetchone() if result: @@ -57,30 +67,42 @@ def fetch_api_key(username): 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()) + new_hashed_pw = bcrypt.hashpw(new_password.encode("utf-8"), bcrypt.gensalt()) # Update the password in the database - with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: + with sqlite3.connect(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())) + 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: + 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())) + 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: + 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(),)) + cursor.execute( + "SELECT Styled_Username FROM user_info WHERE Username = ?", + (username.lower(),), + ) result = cursor.fetchone() if result: @@ -88,13 +110,14 @@ def fetch_styled_username(username): else: return None + def fetch_username_by_api_key(api_key): - with sqlite3.connect(f'{os.getcwd()}/databases/sql/users.db') as conn: + 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,)) + 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 \ No newline at end of file + return None # If no user is found for the API key diff --git a/custom_functions/decrypt/api_decrypt.py b/custom_functions/decrypt/api_decrypt.py index d756dcd..c6eac25 100644 --- a/custom_functions/decrypt/api_decrypt.py +++ b/custom_functions/decrypt/api_decrypt.py @@ -13,19 +13,23 @@ import yaml from urllib.parse import urlparse - - - def find_license_key(data, keywords=None): if keywords is None: - keywords = ["license", "licenseData", "widevine2License"] # Default list of keywords to search for + keywords = [ + "license", + "licenseData", + "widevine2License", + ] # Default list of keywords to search for # If the data is a dictionary, check each key if isinstance(data, dict): for key, value in data.items(): - if any(keyword in key.lower() for keyword in - keywords): # Check if any keyword is in the key (case-insensitive) - return value.replace("-", "+").replace("_", "/") # Return the value immediately when found + if any( + keyword in key.lower() for keyword in keywords + ): # Check if any keyword is in the key (case-insensitive) + return value.replace("-", "+").replace( + "_", "/" + ) # Return the value immediately when found # Recursively check if the value is a dictionary or list if isinstance(value, (dict, list)): result = find_license_key(value, keywords) # Recursively search @@ -44,21 +48,32 @@ def find_license_key(data, keywords=None): def find_license_challenge(data, keywords=None, new_value=None): if keywords is None: - keywords = ["license", "licenseData", "widevine2License", "licenseRequest"] # Default list of keywords to search for + keywords = [ + "license", + "licenseData", + "widevine2License", + "licenseRequest", + ] # Default list of keywords to search for # If the data is a dictionary, check each key if isinstance(data, dict): for key, value in data.items(): - if any(keyword in key.lower() for keyword in keywords): # Check if any keyword is in the key (case-insensitive) + if any( + keyword in key.lower() for keyword in keywords + ): # Check if any keyword is in the key (case-insensitive) data[key] = new_value # Modify the value in-place # Recursively check if the value is a dictionary or list elif isinstance(value, (dict, list)): - find_license_challenge(value, keywords, new_value) # Recursively modify in place + find_license_challenge( + value, keywords, new_value + ) # Recursively modify in place # If the data is a list, iterate through each item elif isinstance(data, list): for i, item in enumerate(data): - result = find_license_challenge(item, keywords, new_value) # Recursively modify in place + result = find_license_challenge( + item, keywords, new_value + ) # Recursively modify in place return data # Return the modified original data (no new structure is created) @@ -68,11 +83,12 @@ def is_base64(string): # Try decoding the string decoded_data = base64.b64decode(string) # Check if the decoded data, when re-encoded, matches the original string - return base64.b64encode(decoded_data).decode('utf-8') == string + return base64.b64encode(decoded_data).decode("utf-8") == string except Exception: # If decoding or encoding fails, it's not Base64 return False + def is_url_and_split(input_str): parsed = urlparse(input_str) @@ -84,82 +100,99 @@ def is_url_and_split(input_str): else: return False, None, None -def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, headers: str = None, cookies: str = None, json_data: str = None, 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: + +def api_decrypt( + pssh: str = None, + license_url: str = None, + proxy: str = None, + headers: str = None, + cookies: str = None, + json_data: str = None, + device: str = "public", + username: str = None, +): + print(f"Using device {device} for user {username}") + with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) - if config['database_type'].lower() == 'sqlite': + if config["database_type"].lower() == "sqlite": from custom_functions.database.cache_to_db_sqlite import cache_to_db - elif config['database_type'].lower() == 'mariadb': + elif config["database_type"].lower() == "mariadb": from custom_functions.database.cache_to_db_mariadb import cache_to_db if pssh is None: - return { - 'status': 'error', - 'message': 'No PSSH provided' - } + return {"status": "error", "message": "No PSSH provided"} try: if "".encode("utf-16-le") in base64.b64decode(pssh): # PR try: pr_pssh = playreadyPSSH(pssh) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred processing PSSH\n\n{error}' + "status": "error", + "message": f"An error occurred processing PSSH\n\n{error}", } try: - if device == 'public': + if device == "public": base_name = config["default_pr_cdm"] if not base_name.endswith(".prd"): base_name += ".prd" - prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/PR/{base_name}') + 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}') + 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' + "status": "error", + "message": "No default .prd file found", } else: base_name = device if not base_name.endswith(".prd"): base_name += ".prd" - prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}') + prd_files = glob.glob( + f"{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}" + ) else: - prd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}') + prd_files = glob.glob( + f"{os.getcwd()}/configs/CDMs/{username}/PR/{base_name}" + ) if prd_files: pr_device = playreadyDevice.load(prd_files[0]) else: return { - 'status': 'error', - 'message': f'{base_name} does not exist' + "status": "error", + "message": f"{base_name} does not exist", } except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred location PlayReady CDM file\n\n{error}' + "status": "error", + "message": f"An error occurred location PlayReady CDM file\n\n{error}", } try: pr_cdm = playreadyCdm.from_device(pr_device) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred loading PlayReady CDM\n\n{error}' + "status": "error", + "message": f"An error occurred loading PlayReady CDM\n\n{error}", } try: pr_session_id = pr_cdm.open() except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred opening a CDM session\n\n{error}' + "status": "error", + "message": f"An error occurred opening a CDM session\n\n{error}", } try: - pr_challenge = pr_cdm.get_license_challenge(pr_session_id, pr_pssh.wrm_headers[0]) + pr_challenge = pr_cdm.get_license_challenge( + pr_session_id, pr_pssh.wrm_headers[0] + ) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting license challenge\n\n{error}' + "status": "error", + "message": f"An error occurred getting license challenge\n\n{error}", } try: if headers: @@ -168,8 +201,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea format_headers = None except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting headers\n\n{error}' + "status": "error", + "message": f"An error occurred getting headers\n\n{error}", } try: if cookies: @@ -178,8 +211,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea format_cookies = None except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting cookies\n\n{error}' + "status": "error", + "message": f"An error occurred getting cookies\n\n{error}", } try: if json_data and not is_base64(json_data): @@ -188,19 +221,19 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea format_json_data = None except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting json_data\n\n{error}' + "status": "error", + "message": f"An error occurred getting json_data\n\n{error}", } licence = None proxies = None if proxy is not None: is_url, protocol, fqdn = is_url_and_split(proxy) if is_url: - proxies = {'http': proxy, 'https': proxy} + proxies = {"http": proxy, "https": proxy} else: return { - 'status': 'error', - 'message': f'Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port' + "status": "error", + "message": f"Your proxy is invalid, please put it in the format of http(s)://fqdn.tld:port", } try: licence = requests.post( @@ -209,132 +242,133 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea proxies=proxies, cookies=format_cookies, json=format_json_data if format_json_data is not None else None, - data=pr_challenge if format_json_data is None else None + data=pr_challenge if format_json_data is None else None, ) except requests.exceptions.ConnectionError as error: return { - 'status': 'error', - 'message': f'An error occurred sending license challenge through your proxy\n\n{error}' + "status": "error", + "message": f"An error occurred sending license challenge through your proxy\n\n{error}", } except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}' + "status": "error", + "message": f"An error occurred sending license reqeust\n\n{error}\n\n{licence.content}", } try: pr_cdm.parse_license(pr_session_id, licence.text) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}' + "status": "error", + "message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}", } returned_keys = "" try: keys = list(pr_cdm.get_keys(pr_session_id)) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting keys\n\n{error}' + "status": "error", + "message": f"An error occurred getting keys\n\n{error}", } try: for index, key in enumerate(keys): - if key.key_type != 'SIGNING': - cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, - data=pr_challenge if json_data is None else json_data, kid=key.key_id.hex, - key=key.key.hex()) + if key.key_type != "SIGNING": + cache_to_db( + pssh=pssh, + license_url=license_url, + headers=headers, + cookies=cookies, + data=pr_challenge if json_data is None else json_data, + kid=key.key_id.hex, + key=key.key.hex(), + ) if index != len(keys) - 1: returned_keys += f"{key.key_id.hex}:{key.key.hex()}\n" else: returned_keys += f"{key.key_id.hex}:{key.key.hex()}" except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred formatting keys\n\n{error}' + "status": "error", + "message": f"An error occurred formatting keys\n\n{error}", } try: pr_cdm.close(pr_session_id) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred closing session\n\n{error}' + "status": "error", + "message": f"An error occurred closing session\n\n{error}", } try: - return { - 'status': 'success', - 'message': returned_keys - } + return {"status": "success", "message": returned_keys} except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting returned_keys\n\n{error}' + "status": "error", + "message": f"An error occurred getting returned_keys\n\n{error}", } except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred processing PSSH\n\n{error}' + "status": "error", + "message": f"An error occurred processing PSSH\n\n{error}", } else: try: wv_pssh = widevinePSSH(pssh) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred processing PSSH\n\n{error}' + "status": "error", + "message": f"An error occurred processing PSSH\n\n{error}", } try: - if device == 'public': + if device == "public": base_name = config["default_wv_cdm"] if not base_name.endswith(".wvd"): base_name += ".wvd" - wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/WV/{base_name}') + 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}') + 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' - } + return {"status": "error", "message": "No default .wvd file found"} else: base_name = device if not base_name.endswith(".wvd"): base_name += ".wvd" - wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}') + wvd_files = glob.glob( + f"{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}" + ) else: - wvd_files = glob.glob(f'{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}') + wvd_files = glob.glob( + f"{os.getcwd()}/configs/CDMs/{username}/WV/{base_name}" + ) if wvd_files: wv_device = widevineDevice.load(wvd_files[0]) else: - return { - 'status': 'error', - 'message': f'{base_name} does not exist' - } + return {"status": "error", "message": f"{base_name} does not exist"} except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred location Widevine CDM file\n\n{error}' + "status": "error", + "message": f"An error occurred location Widevine CDM file\n\n{error}", } try: wv_cdm = widevineCdm.from_device(wv_device) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred loading Widevine CDM\n\n{error}' + "status": "error", + "message": f"An error occurred loading Widevine CDM\n\n{error}", } try: wv_session_id = wv_cdm.open() except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred opening a CDM session\n\n{error}' + "status": "error", + "message": f"An error occurred opening a CDM session\n\n{error}", } try: wv_challenge = wv_cdm.get_license_challenge(wv_session_id, wv_pssh) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting license challenge\n\n{error}' + "status": "error", + "message": f"An error occurred getting license challenge\n\n{error}", } try: if headers: @@ -343,8 +377,8 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea format_headers = None except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting headers\n\n{error}' + "status": "error", + "message": f"An error occurred getting headers\n\n{error}", } try: if cookies: @@ -353,26 +387,29 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea format_cookies = None except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting cookies\n\n{error}' + "status": "error", + "message": f"An error occurred getting cookies\n\n{error}", } try: if json_data and not is_base64(json_data): format_json_data = ast.literal_eval(json_data) - format_json_data = find_license_challenge(data=format_json_data, new_value=base64.b64encode(wv_challenge).decode()) + format_json_data = find_license_challenge( + data=format_json_data, + new_value=base64.b64encode(wv_challenge).decode(), + ) else: format_json_data = None except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting json_data\n\n{error}' + "status": "error", + "message": f"An error occurred getting json_data\n\n{error}", } licence = None proxies = None if proxy is not None: is_url, protocol, fqdn = is_url_and_split(proxy) if is_url: - proxies = {'http': proxy, 'https': proxy} + proxies = {"http": proxy, "https": proxy} try: licence = requests.post( url=license_url, @@ -380,17 +417,17 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea proxies=proxies, cookies=format_cookies, json=format_json_data if format_json_data is not None else None, - data=wv_challenge if format_json_data is None else None + data=wv_challenge if format_json_data is None else None, ) except requests.exceptions.ConnectionError as error: return { - 'status': 'error', - 'message': f'An error occurred sending license challenge through your proxy\n\n{error}' + "status": "error", + "message": f"An error occurred sending license challenge through your proxy\n\n{error}", } except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred sending license reqeust\n\n{error}\n\n{licence.content}' + "status": "error", + "message": f"An error occurred sending license reqeust\n\n{error}\n\n{licence.content}", } try: wv_cdm.parse_license(wv_session_id, licence.content) @@ -401,44 +438,49 @@ def api_decrypt(pssh:str = None, license_url: str = None, proxy: str = None, hea wv_cdm.parse_license(wv_session_id, license_value) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred parsing license content\n\n{error}\n\n{licence.content}' + "status": "error", + "message": f"An error occurred parsing license content\n\n{error}\n\n{licence.content}", } returned_keys = "" try: keys = list(wv_cdm.get_keys(wv_session_id)) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting keys\n\n{error}' + "status": "error", + "message": f"An error occurred getting keys\n\n{error}", } try: for index, key in enumerate(keys): - if key.type != 'SIGNING': - cache_to_db(pssh=pssh, license_url=license_url, headers=headers, cookies=cookies, data=wv_challenge if json_data is None else json_data, kid=key.kid.hex, key=key.key.hex()) + if key.type != "SIGNING": + cache_to_db( + pssh=pssh, + license_url=license_url, + headers=headers, + cookies=cookies, + data=wv_challenge if json_data is None else json_data, + kid=key.kid.hex, + key=key.key.hex(), + ) if index != len(keys) - 1: returned_keys += f"{key.kid.hex}:{key.key.hex()}\n" else: returned_keys += f"{key.kid.hex}:{key.key.hex()}" except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred formatting keys\n\n{error}' + "status": "error", + "message": f"An error occurred formatting keys\n\n{error}", } try: - wv_cdm.close(wv_session_id) + wv_cdm.close(wv_session_id) except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred closing session\n\n{error}' + "status": "error", + "message": f"An error occurred closing session\n\n{error}", } try: - return { - 'status': 'success', - 'message': returned_keys - } + return {"status": "success", "message": returned_keys} except Exception as error: return { - 'status': 'error', - 'message': f'An error occurred getting returned_keys\n\n{error}' - } \ No newline at end of file + "status": "error", + "message": f"An error occurred getting returned_keys\n\n{error}", + } diff --git a/custom_functions/prechecks/cdm_checks.py b/custom_functions/prechecks/cdm_checks.py index 1ff81ca..e4ef54b 100644 --- a/custom_functions/prechecks/cdm_checks.py +++ b/custom_functions/prechecks/cdm_checks.py @@ -3,66 +3,86 @@ import yaml import requests - def check_for_wvd_cdm(): - 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) - if config['default_wv_cdm'] == '': - answer = ' ' - while answer[0].upper() != 'Y' and answer[0].upper() != 'N': - answer = input('No default Widevine CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ') - if answer[0].upper() == 'Y': - response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd') + if config["default_wv_cdm"] == "": + answer = " " + while answer[0].upper() != "Y" and answer[0].upper() != "N": + answer = input( + "No default Widevine CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: " + ) + if answer[0].upper() == "Y": + response = requests.get( + url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Widevine/L3/public.wvd" + ) if response.status_code == 200: - with open(f'{os.getcwd()}/configs/CDMs/WV/public.wvd', 'wb') as file: + with open(f"{os.getcwd()}/configs/CDMs/WV/public.wvd", "wb") as file: file.write(response.content) - config['default_wv_cdm'] = 'public' - with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file: + config["default_wv_cdm"] = "public" + with open(f"{os.getcwd()}/configs/config.yaml", "w") as file: yaml.dump(config, file) print("Successfully downloaded Widevine CDM") else: - exit(f"Download failed, please try again or place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml") - if answer[0].upper() == 'N': - exit(f"Place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml") + exit( + f"Download failed, please try again or place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml" + ) + if answer[0].upper() == "N": + exit( + f"Place a .wvd file in {os.getcwd()}/configs/CDMs/WV and specify the name in {os.getcwd()}/configs/config.yaml" + ) else: base_name = config["default_wv_cdm"] if not base_name.endswith(".wvd"): base_name += ".wvd" - if os.path.exists(f'{os.getcwd()}/configs/CDMs/WV/{base_name}'): + if os.path.exists(f"{os.getcwd()}/configs/CDMs/WV/{base_name}"): return else: - exit(f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV") + exit( + f"Widevine CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV" + ) + def check_for_prd_cdm(): - with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: + with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) - if config['default_pr_cdm'] == '': - answer = ' ' - while answer[0].upper() != 'Y' and answer[0].upper() != 'N': - answer = input('No default PlayReady CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: ') - if answer[0].upper() == 'Y': - response = requests.get(url='https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd') + if config["default_pr_cdm"] == "": + answer = " " + while answer[0].upper() != "Y" and answer[0].upper() != "N": + answer = input( + "No default PlayReady CDM specified, would you like to download one from The CDM Project? (Y)es/(N)o: " + ) + if answer[0].upper() == "Y": + response = requests.get( + url="https://cdm-project.com/CDRM-Team/CDMs/raw/branch/main/Playready/SL2000/public.prd" + ) if response.status_code == 200: - with open(f'{os.getcwd()}/configs/CDMs/PR/public.prd', 'wb') as file: + with open(f"{os.getcwd()}/configs/CDMs/PR/public.prd", "wb") as file: file.write(response.content) - config['default_pr_cdm'] = 'public' - with open(f'{os.getcwd()}/configs/config.yaml', 'w') as file: + config["default_pr_cdm"] = "public" + with open(f"{os.getcwd()}/configs/config.yaml", "w") as file: yaml.dump(config, file) print("Successfully downloaded PlayReady CDM") else: - exit(f"Download failed, please try again or place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml") - if answer[0].upper() == 'N': - exit(f"Place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml") + exit( + f"Download failed, please try again or place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml" + ) + if answer[0].upper() == "N": + exit( + f"Place a .prd file in {os.getcwd()}/configs/CDMs/PR and specify the name in {os.getcwd()}/configs/config.yaml" + ) else: base_name = config["default_pr_cdm"] if not base_name.endswith(".prd"): base_name += ".prd" - if os.path.exists(f'{os.getcwd()}/configs/CDMs/PR/{base_name}'): + if os.path.exists(f"{os.getcwd()}/configs/CDMs/PR/{base_name}"): return else: - exit(f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV") + exit( + f"PlayReady CDM {base_name} does not exist in {os.getcwd()}/configs/CDMs/WV" + ) def check_for_cdms(): check_for_wvd_cdm() - check_for_prd_cdm() \ No newline at end of file + check_for_prd_cdm() diff --git a/custom_functions/prechecks/config_file_checks.py b/custom_functions/prechecks/config_file_checks.py index 5ca2e58..a3f8c39 100644 --- a/custom_functions/prechecks/config_file_checks.py +++ b/custom_functions/prechecks/config_file_checks.py @@ -1,7 +1,8 @@ import os + def check_for_config_file(): - if os.path.exists(f'{os.getcwd()}/configs/config.yaml'): + if os.path.exists(f"{os.getcwd()}/configs/config.yaml"): return else: default_config = """\ @@ -21,6 +22,6 @@ remote_cdm_secret: '' # port: '' # database: '' """ - with open(f'{os.getcwd()}/configs/config.yaml', 'w') as f: + with open(f"{os.getcwd()}/configs/config.yaml", "w") as f: f.write(default_config) - return \ No newline at end of file + return diff --git a/custom_functions/prechecks/database_checks.py b/custom_functions/prechecks/database_checks.py index 764bc08..6827d46 100644 --- a/custom_functions/prechecks/database_checks.py +++ b/custom_functions/prechecks/database_checks.py @@ -1,37 +1,44 @@ import os import yaml + def check_for_sqlite_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) - if os.path.exists(f'{os.getcwd()}/databases/key_cache.db'): + if os.path.exists(f"{os.getcwd()}/databases/key_cache.db"): return else: - if config['database_type'].lower() != 'mariadb': + if config["database_type"].lower() != "mariadb": from custom_functions.database.cache_to_db_sqlite import create_database + create_database() return else: return + def check_for_user_database(): - if os.path.exists(f'{os.getcwd()}/databases/users.db'): + if os.path.exists(f"{os.getcwd()}/databases/users.db"): return else: from custom_functions.database.user_db import create_user_database + create_user_database() + def check_for_mariadb_database(): - with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: + with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) - if config['database_type'].lower() == 'mariadb': + if config["database_type"].lower() == "mariadb": from custom_functions.database.cache_to_db_mariadb import create_database + create_database() return else: return + def check_for_sql_database(): check_for_sqlite_database() check_for_mariadb_database() - check_for_user_database() \ No newline at end of file + check_for_user_database() diff --git a/custom_functions/prechecks/folder_checks.py b/custom_functions/prechecks/folder_checks.py index 142670d..09e5147 100644 --- a/custom_functions/prechecks/folder_checks.py +++ b/custom_functions/prechecks/folder_checks.py @@ -1,44 +1,50 @@ import os + def check_for_config_folder(): - if os.path.isdir(f'{os.getcwd()}/configs'): + if os.path.isdir(f"{os.getcwd()}/configs"): return else: - os.mkdir(f'{os.getcwd()}/configs') + os.mkdir(f"{os.getcwd()}/configs") return + def check_for_database_folder(): - if os.path.isdir(f'{os.getcwd()}/databases'): + if os.path.isdir(f"{os.getcwd()}/databases"): return else: - os.mkdir(f'{os.getcwd()}/databases') - os.mkdir(f'{os.getcwd()}/databases/sql') + os.mkdir(f"{os.getcwd()}/databases") + os.mkdir(f"{os.getcwd()}/databases/sql") return + def check_for_cdm_folder(): - if os.path.isdir(f'{os.getcwd()}/configs/CDMs'): + if os.path.isdir(f"{os.getcwd()}/configs/CDMs"): return else: - os.mkdir(f'{os.getcwd()}/configs/CDMs') + os.mkdir(f"{os.getcwd()}/configs/CDMs") return + def check_for_wv_cdm_folder(): - if os.path.isdir(f'{os.getcwd()}/configs/CDMs/WV'): + if os.path.isdir(f"{os.getcwd()}/configs/CDMs/WV"): return else: - os.mkdir(f'{os.getcwd()}/configs/CDMs/WV') + os.mkdir(f"{os.getcwd()}/configs/CDMs/WV") return + def check_for_cdm_pr_folder(): - if os.path.isdir(f'{os.getcwd()}/configs/CDMs/PR'): + if os.path.isdir(f"{os.getcwd()}/configs/CDMs/PR"): return else: - os.mkdir(f'{os.getcwd()}/configs/CDMs/PR') + os.mkdir(f"{os.getcwd()}/configs/CDMs/PR") return + def folder_checks(): check_for_config_folder() check_for_database_folder() check_for_cdm_folder() check_for_wv_cdm_folder() - check_for_cdm_pr_folder() \ No newline at end of file + check_for_cdm_pr_folder() diff --git a/custom_functions/prechecks/precheck.py b/custom_functions/prechecks/precheck.py index 304a98f..23519df 100644 --- a/custom_functions/prechecks/precheck.py +++ b/custom_functions/prechecks/precheck.py @@ -3,9 +3,10 @@ from custom_functions.prechecks.config_file_checks import check_for_config_file from custom_functions.prechecks.database_checks import check_for_sql_database from custom_functions.prechecks.cdm_checks import check_for_cdms + def run_precheck(): folder_checks() check_for_config_file() check_for_cdms() check_for_sql_database() - return \ No newline at end of file + return diff --git a/custom_functions/prechecks/python_checks.py b/custom_functions/prechecks/python_checks.py index 4230ecd..0cf1fce 100644 --- a/custom_functions/prechecks/python_checks.py +++ b/custom_functions/prechecks/python_checks.py @@ -3,6 +3,7 @@ import os import subprocess import venv + def version_check(): major_version = sys.version_info.major minor_version = sys.version_info.minor @@ -15,20 +16,29 @@ def version_check(): else: exit("Python 2 detected, Python version 3.12 or higher is required") + def pip_check(): try: import pip + return except ImportError: exit("Pip is not installed") + def venv_check(): # Check if we're already inside a virtual environment - if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix): + if hasattr(sys, "real_prefix") or ( + hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix + ): return - venv_path = os.path.join(os.getcwd(), 'cdrm-venv') - venv_python = os.path.join(venv_path, 'bin', 'python') if os.name != 'nt' else os.path.join(venv_path, 'Scripts', 'python.exe') + venv_path = os.path.join(os.getcwd(), "cdrm-venv") + venv_python = ( + os.path.join(venv_path, "bin", "python") + if os.name != "nt" + else os.path.join(venv_path, "Scripts", "python.exe") + ) # If venv already exists, restart script using its Python if os.path.exists(venv_path): @@ -36,14 +46,14 @@ def venv_check(): sys.exit() # Ask user for permission to create a virtual environment - answer = '' - while not answer or answer[0].upper() not in {'Y', 'N'}: + answer = "" + while not answer or answer[0].upper() not in {"Y", "N"}: answer = input( - 'Program is not running from a venv. To maintain compatibility and dependencies, this program must be run from one.\n' - 'Would you like me to create one for you? (Y/N): ' + "Program is not running from a venv. To maintain compatibility and dependencies, this program must be run from one.\n" + "Would you like me to create one for you? (Y/N): " ) - if answer[0].upper() == 'Y': + if answer[0].upper() == "Y": print("Creating virtual environment...") venv.create(venv_path, with_pip=True) subprocess.call([venv_python] + sys.argv) @@ -61,25 +71,33 @@ def requirements_check(): import flask_cors import yaml import mysql.connector + return except ImportError: while True: - user_input = input("Missing packages. Do you want to install them? (Y/N): ").strip().upper() - if user_input == 'Y': + user_input = ( + input("Missing packages. Do you want to install them? (Y/N): ") + .strip() + .upper() + ) + if user_input == "Y": print("Installing packages from requirements.txt...") - subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) + subprocess.check_call( + [sys.executable, "-m", "pip", "install", "-r", "requirements.txt"] + ) print("Installation complete.") break - elif user_input == 'N': + elif user_input == "N": print("Dependencies required, please install them and run again.") sys.exit() else: print("Invalid input. Please enter 'Y' to install or 'N' to exit.") + def run_python_checks(): - if getattr(sys, 'frozen', False): # Check if running from PyInstaller + if getattr(sys, "frozen", False): # Check if running from PyInstaller return version_check() pip_check() venv_check() - requirements_check() \ No newline at end of file + requirements_check() diff --git a/custom_functions/user_checks/device_allowed.py b/custom_functions/user_checks/device_allowed.py index 3d0d51c..ad3a5fc 100644 --- a/custom_functions/user_checks/device_allowed.py +++ b/custom_functions/user_checks/device_allowed.py @@ -1,12 +1,17 @@ import os import glob + def user_allowed_to_use_device(device, username): - base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', 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'))] + pr_files = [ + os.path.basename(f) for f in glob.glob(os.path.join(base_path, "PR", "*.prd")) + ] + wv_files = [ + os.path.basename(f) for f in glob.glob(os.path.join(base_path, "WV", "*.wvd")) + ] # Combine all filenames all_files = pr_files + wv_files @@ -14,4 +19,4 @@ def user_allowed_to_use_device(device, username): # Check if filename matches directly or by adding extensions possible_names = {device, f"{device}.prd", f"{device}.wvd"} - return any(name in all_files for name in possible_names) \ No newline at end of file + return any(name in all_files for name in possible_names) diff --git a/main.py b/main.py index ede50e1..2500283 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ from custom_functions.prechecks.python_checks import run_python_checks + run_python_checks() from custom_functions.prechecks.precheck import run_precheck + run_precheck() from flask import Flask from flask_cors import CORS @@ -15,10 +17,11 @@ from routes.login import login_bp from routes.user_changes import user_change_bp import os import yaml + app = Flask(__name__) -with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: +with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) -app.secret_key = config['secret_key_flask'] +app.secret_key = config["secret_key_flask"] CORS(app) @@ -33,5 +36,5 @@ app.register_blueprint(remotecdm_wv_bp) app.register_blueprint(remotecdm_pr_bp) app.register_blueprint(user_change_bp) -if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0') \ No newline at end of file +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..350fa3e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.black] +line-length = 88 +target-version = ['py38'] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | cdrm-frontend +)/ +''' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e2fe1b6..c7674ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ protobuf~=4.25.6 PyYAML mysql-connector-python bcrypt +black diff --git a/routes/api.py b/routes/api.py index 2d76a96..13a302f 100644 --- a/routes/api.py +++ b/routes/api.py @@ -13,101 +13,126 @@ import tempfile import time from configs.icon_links import data as icon_data -api_bp = Blueprint('api', __name__) -with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: +api_bp = Blueprint("api", __name__) +with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) -if config['database_type'].lower() != 'mariadb': - from custom_functions.database.cache_to_db_sqlite import search_by_pssh_or_kid, cache_to_db, \ - get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count -elif config['database_type'].lower() == 'mariadb': - from custom_functions.database.cache_to_db_mariadb import search_by_pssh_or_kid, cache_to_db, \ - get_key_by_kid_and_service, get_unique_services, get_kid_key_dict, key_count +if config["database_type"].lower() != "mariadb": + from custom_functions.database.cache_to_db_sqlite import ( + search_by_pssh_or_kid, + cache_to_db, + get_key_by_kid_and_service, + get_unique_services, + get_kid_key_dict, + key_count, + ) +elif config["database_type"].lower() == "mariadb": + from custom_functions.database.cache_to_db_mariadb import ( + search_by_pssh_or_kid, + cache_to_db, + get_key_by_kid_and_service, + get_unique_services, + get_kid_key_dict, + key_count, + ) + def get_db_config(): # Configure your MariaDB connection - with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: + with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) db_config = { - 'host': f'{config["mariadb"]["host"]}', - 'user': f'{config["mariadb"]["user"]}', - 'password': f'{config["mariadb"]["password"]}', - 'database': f'{config["mariadb"]["database"]}' + "host": f'{config["mariadb"]["host"]}', + "user": f'{config["mariadb"]["user"]}', + "password": f'{config["mariadb"]["password"]}', + "database": f'{config["mariadb"]["database"]}', } return db_config -@api_bp.route('/api/cache/search', methods=['POST']) + +@api_bp.route("/api/cache/search", methods=["POST"]) def get_data(): - search_argument = json.loads(request.data)['input'] + search_argument = json.loads(request.data)["input"] results = search_by_pssh_or_kid(search_filter=search_argument) return jsonify(results) -@api_bp.route('/api/cache//', methods=['GET']) + +@api_bp.route("/api/cache//", methods=["GET"]) def get_single_key_service(service, kid): result = get_key_by_kid_and_service(kid=kid, service=service) - return jsonify({ - 'code': 0, - 'content_key': result, - }) + return jsonify( + { + "code": 0, + "content_key": result, + } + ) -@api_bp.route('/api/cache/', methods=['GET']) + +@api_bp.route("/api/cache/", methods=["GET"]) def get_multiple_key_service(service): result = get_kid_key_dict(service_name=service) pages = math.ceil(len(result) / 10) - return jsonify({ - 'code': 0, - 'content_keys': result, - 'pages': pages - }) + return jsonify({"code": 0, "content_keys": result, "pages": pages}) -@api_bp.route('/api/cache//', methods=['POST']) + +@api_bp.route("/api/cache//", methods=["POST"]) def add_single_key_service(service, kid): body = request.get_json() - content_key = body['content_key'] + content_key = body["content_key"] result = cache_to_db(service=service, kid=kid, key=content_key) if result: - return jsonify({ - 'code': 0, - 'updated': True, - }) + return jsonify( + { + "code": 0, + "updated": True, + } + ) elif result is False: - return jsonify({ - 'code': 0, - 'updated': True, - }) + return jsonify( + { + "code": 0, + "updated": True, + } + ) -@api_bp.route('/api/cache/', methods=['POST']) + +@api_bp.route("/api/cache/", methods=["POST"]) def add_multiple_key_service(service): body = request.get_json() keys_added = 0 keys_updated = 0 - for kid, key in body['content_keys'].items(): + for kid, key in body["content_keys"].items(): result = cache_to_db(service=service, kid=kid, key=key) if result is True: keys_updated += 1 elif result is False: keys_added += 1 - return jsonify({ - 'code': 0, - 'added': str(keys_added), - 'updated': str(keys_updated), - }) + return jsonify( + { + "code": 0, + "added": str(keys_added), + "updated": str(keys_updated), + } + ) -@api_bp.route('/api/cache', methods=['POST']) + +@api_bp.route("/api/cache", methods=["POST"]) def unique_service(): services = get_unique_services() - return jsonify({ - 'code': 0, - 'service_list': services, - }) + return jsonify( + { + "code": 0, + "service_list": services, + } + ) -@api_bp.route('/api/cache/download', methods=['GET']) +@api_bp.route("/api/cache/download", methods=["GET"]) def download_database(): - if config['database_type'].lower() != 'mariadb': - original_database_path = f'{os.getcwd()}/databases/sql/key_cache.db' + if config["database_type"].lower() != "mariadb": + original_database_path = f"{os.getcwd()}/databases/sql/key_cache.db" # Make a copy of the original database (without locking the original) - modified_database_path = f'{os.getcwd()}/databases/sql/key_cache_modified.db' + modified_database_path = f"{os.getcwd()}/databases/sql/key_cache_modified.db" # Using shutil.copy2 to preserve metadata (timestamps, etc.) shutil.copy2(original_database_path, modified_database_path) @@ -117,34 +142,40 @@ def download_database(): cursor = conn.cursor() # Update all rows to remove Headers and Cookies (set them to NULL or empty strings) - cursor.execute(''' + cursor.execute( + """ UPDATE licenses SET Headers = NULL, Cookies = NULL - ''') + """ + ) # No need for explicit commit, it's done automatically with the 'with' block # The connection will automatically be committed and closed when the block ends # Send the modified database as an attachment - return send_file(modified_database_path, as_attachment=True, download_name='key_cache.db') - if config['database_type'].lower() == 'mariadb': + return send_file( + modified_database_path, as_attachment=True, download_name="key_cache.db" + ) + if config["database_type"].lower() == "mariadb": try: # Connect to MariaDB conn = mysql.connector.connect(**get_db_config()) cursor = conn.cursor() # Update sensitive data (this updates the live DB, you may want to duplicate rows instead) - cursor.execute(''' + cursor.execute( + """ UPDATE licenses SET Headers = NULL, Cookies = NULL - ''') + """ + ) conn.commit() # Now export the table - cursor.execute('SELECT * FROM licenses') + cursor.execute("SELECT * FROM licenses") rows = cursor.fetchall() column_names = [desc[0] for desc in cursor.description] @@ -152,116 +183,135 @@ def download_database(): output = StringIO() output.write(f"-- Dump of `licenses` table\n") for row in rows: - values = ', '.join(f"'{str(v).replace('\'', '\\\'')}'" if v is not None else 'NULL' for v in row) - output.write(f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n") + values = ", ".join( + f"'{str(v).replace('\'', '\\\'')}'" if v is not None else "NULL" + for v in row + ) + output.write( + f"INSERT INTO licenses ({', '.join(column_names)}) VALUES ({values});\n" + ) # Write to a temp file for download temp_dir = tempfile.gettempdir() - temp_path = os.path.join(temp_dir, 'key_cache.sql') - with open(temp_path, 'w', encoding='utf-8') as f: + temp_path = os.path.join(temp_dir, "key_cache.sql") + with open(temp_path, "w", encoding="utf-8") as f: f.write(output.getvalue()) - return send_file(temp_path, as_attachment=True, download_name='licenses_dump.sql') + return send_file( + temp_path, as_attachment=True, download_name="licenses_dump.sql" + ) except mysql.connector.Error as err: return {"error": str(err)}, 500 -_keycount_cache = { - 'count': None, - 'timestamp': 0 -} -@api_bp.route('/api/cache/keycount', methods=['GET']) +_keycount_cache = {"count": None, "timestamp": 0} + + +@api_bp.route("/api/cache/keycount", methods=["GET"]) def get_count(): now = time.time() - if now - _keycount_cache['timestamp'] > 10 or _keycount_cache['count'] is None: - _keycount_cache['count'] = key_count() - _keycount_cache['timestamp'] = now - return jsonify({ - 'count': _keycount_cache['count'] - }) + if now - _keycount_cache["timestamp"] > 10 or _keycount_cache["count"] is None: + _keycount_cache["count"] = key_count() + _keycount_cache["timestamp"] = now + return jsonify({"count": _keycount_cache["count"]}) -@api_bp.route('/api/decrypt', methods=['POST']) + +@api_bp.route("/api/decrypt", methods=["POST"]) def decrypt_data(): api_request_data = json.loads(request.data) - if 'pssh' in api_request_data: - if api_request_data['pssh'] == '': + if "pssh" in api_request_data: + if api_request_data["pssh"] == "": api_request_pssh = None else: - api_request_pssh = api_request_data['pssh'] + api_request_pssh = api_request_data["pssh"] else: api_request_pssh = None - if 'licurl' in api_request_data: - if api_request_data['licurl'] == '': + if "licurl" in api_request_data: + if api_request_data["licurl"] == "": api_request_licurl = None else: - api_request_licurl = api_request_data['licurl'] + api_request_licurl = api_request_data["licurl"] else: api_request_licurl = None - if 'proxy' in api_request_data: - if api_request_data['proxy'] == '': + if "proxy" in api_request_data: + if api_request_data["proxy"] == "": api_request_proxy = None else: - api_request_proxy = api_request_data['proxy'] + api_request_proxy = api_request_data["proxy"] else: api_request_proxy = None - if 'headers' in api_request_data: - if api_request_data['headers'] == '': + if "headers" in api_request_data: + if api_request_data["headers"] == "": api_request_headers = None else: - api_request_headers = api_request_data['headers'] + api_request_headers = api_request_data["headers"] else: api_request_headers = None - if 'cookies' in api_request_data: - if api_request_data['cookies'] == '': + if "cookies" in api_request_data: + if api_request_data["cookies"] == "": api_request_cookies = None else: - api_request_cookies = api_request_data['cookies'] + api_request_cookies = api_request_data["cookies"] else: api_request_cookies = None - if 'data' in api_request_data: - if api_request_data['data'] == '': + if "data" in api_request_data: + if api_request_data["data"] == "": api_request_data_func = None else: - api_request_data_func = api_request_data['data'] - else: api_request_data_func = None - if 'device' in api_request_data: - if api_request_data['device'] == 'default' or api_request_data['device'] == 'CDRM-Project Public Widevine CDM' or api_request_data['device'] == 'CDRM-Project Public PlayReady CDM': - api_request_device = 'public' - else: - api_request_device = api_request_data['device'] + api_request_data_func = api_request_data["data"] else: - api_request_device = 'public' + api_request_data_func = None + if "device" in api_request_data: + if ( + api_request_data["device"] == "default" + or api_request_data["device"] == "CDRM-Project Public Widevine CDM" + or api_request_data["device"] == "CDRM-Project Public PlayReady CDM" + ): + api_request_device = "public" + else: + api_request_device = api_request_data["device"] + else: + api_request_device = "public" username = None - if api_request_device != 'public': - username = session.get('username') + if api_request_device != "public": + username = session.get("username") if not username: - return jsonify({'message': 'Not logged in, not allowed'}), 400 + return jsonify({"message": "Not logged in, not allowed"}), 400 if user_allowed_to_use_device(device=api_request_device, username=username): api_request_device = api_request_device else: - return jsonify({'message': f'Not authorized / Not found'}), 403 - result = api_decrypt(pssh=api_request_pssh, proxy=api_request_proxy, license_url=api_request_licurl, headers=api_request_headers, cookies=api_request_cookies, json_data=api_request_data_func, device=api_request_device, username=username) - if result['status'] == 'success': - return jsonify({ - 'status': 'success', - 'message': result['message'] - }) + return jsonify({"message": f"Not authorized / Not found"}), 403 + result = api_decrypt( + pssh=api_request_pssh, + proxy=api_request_proxy, + license_url=api_request_licurl, + headers=api_request_headers, + cookies=api_request_cookies, + json_data=api_request_data_func, + device=api_request_device, + username=username, + ) + if result["status"] == "success": + return jsonify({"status": "success", "message": result["message"]}) else: - return jsonify({ - 'status': 'fail', - 'message': result['message'] - }) + return jsonify({"status": "fail", "message": result["message"]}) -@api_bp.route('/api/links', methods=['GET']) + +@api_bp.route("/api/links", methods=["GET"]) def get_links(): - return jsonify({ - 'discord': icon_data['discord'], - 'telegram': icon_data['telegram'], - 'gitea': icon_data['gitea'], - }) + return jsonify( + { + "discord": icon_data["discord"], + "telegram": icon_data["telegram"], + "gitea": icon_data["gitea"], + } + ) -@api_bp.route('/api/extension', methods=['POST']) + +@api_bp.route("/api/extension", methods=["POST"]) def verify_extension(): - return jsonify({ - 'status': True, - }) \ No newline at end of file + return jsonify( + { + "status": True, + } + ) diff --git a/routes/login.py b/routes/login.py index 0fc3f73..129e463 100644 --- a/routes/login.py +++ b/routes/login.py @@ -2,36 +2,44 @@ from flask import Blueprint, request, jsonify, session from custom_functions.database.user_db import verify_user login_bp = Blueprint( - 'login_bp', + "login_bp", __name__, ) -@login_bp.route('/login', methods=['POST']) + +@login_bp.route("/login", methods=["POST"]) def login(): - if request.method == 'POST': + if request.method == "POST": data = request.get_json() - for required_field in ['username', 'password']: + for required_field in ["username", "password"]: if required_field not in data: - return jsonify({'error': f'Missing required field: {required_field}'}), 400 + return ( + jsonify({"error": f"Missing required field: {required_field}"}), + 400, + ) - if verify_user(data['username'], data['password']): - session['username'] = data['username'].lower() # Stored securely in a signed cookie - return jsonify({'message': 'Successfully logged in!'}) + 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 + return jsonify({"error": "Invalid username or password!"}), 401 -@login_bp.route('/login/status', methods=['POST']) + +@login_bp.route("/login/status", methods=["POST"]) def login_status(): try: - username = session.get('username') + username = session.get("username") if username: - return jsonify({'message': 'True'}) + return jsonify({"message": "True"}) else: - return jsonify({'message': 'False'}) + return jsonify({"message": "False"}) except: - return jsonify({'message': 'False'}) + return jsonify({"message": "False"}) -@login_bp.route('/logout', methods=['POST']) + +@login_bp.route("/logout", methods=["POST"]) def logout(): - session.pop('username', None) - return jsonify({'message': 'Successfully logged out!'}) \ No newline at end of file + session.pop("username", None) + return jsonify({"message": "Successfully logged out!"}) diff --git a/routes/react.py b/routes/react.py index 6da08f6..e4296c4 100644 --- a/routes/react.py +++ b/routes/react.py @@ -3,31 +3,32 @@ import os from flask import Blueprint, send_from_directory, request, render_template from configs import index_tags -if getattr(sys, 'frozen', False): # Running as a bundled app +if getattr(sys, "frozen", False): # Running as a bundled app base_path = sys._MEIPASS else: # Running in a normal Python environment base_path = os.path.abspath(".") -static_folder = os.path.join(base_path, 'cdrm-frontend', 'dist') +static_folder = os.path.join(base_path, "frontend-dist") react_bp = Blueprint( - 'react_bp', + "react_bp", __name__, static_folder=static_folder, - static_url_path='/', - template_folder=static_folder + static_url_path="/", + template_folder=static_folder, ) -@react_bp.route('/', methods=['GET']) -@react_bp.route('/', methods=["GET"]) -@react_bp.route('/', methods=["GET"]) -def index(path=''): - if request.method == 'GET': + +@react_bp.route("/", methods=["GET"]) +@react_bp.route("/", methods=["GET"]) +@react_bp.route("/", methods=["GET"]) +def index(path=""): + if request.method == "GET": file_path = os.path.join(react_bp.static_folder, path) if path != "" and os.path.exists(file_path): return send_from_directory(react_bp.static_folder, path) - elif path.lower() in ['', 'cache', 'api', 'testplayer', 'account']: - data = index_tags.tags.get(path.lower(), index_tags.tags['index']) - return render_template('index.html', data=data) + elif path.lower() in ["", "cache", "api", "testplayer", "account"]: + data = index_tags.tags.get(path.lower(), index_tags.tags["index"]) + return render_template("index.html", data=data) else: - return send_from_directory(react_bp.static_folder, 'index.html') + return send_from_directory(react_bp.static_folder, "index.html") diff --git a/routes/register.py b/routes/register.py index cbbe283..ee6d53a 100644 --- a/routes/register.py +++ b/routes/register.py @@ -3,40 +3,44 @@ from flask import Blueprint, request, jsonify from custom_functions.database.user_db import add_user import uuid -register_bp = Blueprint('register_bp', __name__) +register_bp = Blueprint("register_bp", __name__) -USERNAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$') -PASSWORD_REGEX = re.compile(r'^\S+$') +USERNAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$") +PASSWORD_REGEX = re.compile(r"^\S+$") -@register_bp.route('/register', methods=['POST']) + +@register_bp.route("/register", methods=["POST"]) def register(): - if request.method != 'POST': - return jsonify({'error': 'Method not supported'}), 405 + if request.method != "POST": + return jsonify({"error": "Method not supported"}), 405 data = request.get_json() # Check required fields - for required_field in ['username', 'password']: + for required_field in ["username", "password"]: if required_field not in data: - return jsonify({'error': f'Missing required field: {required_field}'}), 400 + return jsonify({"error": f"Missing required field: {required_field}"}), 400 - username = data['username'] - password = data['password'] + username = data["username"] + 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 + return ( + jsonify( + { + "error": "Invalid username. Only letters, numbers, hyphens, and underscores are allowed." + } + ), + 400, + ) if not PASSWORD_REGEX.fullmatch(password): - return jsonify({ - 'error': 'Invalid password. Spaces are not allowed.' - }), 400 + return jsonify({"error": "Invalid password. Spaces are not allowed."}), 400 # Attempt to add user if add_user(username, password, api_key): - return jsonify({'message': 'User successfully registered!'}), 201 + return jsonify({"message": "User successfully registered!"}), 201 else: - return jsonify({'error': 'User already exists!'}), 409 + return jsonify({"error": "User already exists!"}), 409 diff --git a/routes/remote_device_pr.py b/routes/remote_device_pr.py index d960add..1a6e4af 100644 --- a/routes/remote_device_pr.py +++ b/routes/remote_device_pr.py @@ -6,136 +6,195 @@ import yaml from pyplayready.device import Device as PlayReadyDevice from pyplayready.cdm import Cdm as PlayReadyCDM from pyplayready import PSSH as PlayReadyPSSH -from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh) +from pyplayready.exceptions import ( + InvalidSession, + 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 - - -remotecdm_pr_bp = Blueprint('remotecdm_pr', __name__) -with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: +remotecdm_pr_bp = Blueprint("remotecdm_pr", __name__) +with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) -@remotecdm_pr_bp.route('/remotecdm/playready', methods=['GET', 'HEAD']) + +@remotecdm_pr_bp.route("/remotecdm/playready", methods=["GET", "HEAD"]) def remote_cdm_playready(): - if request.method == 'GET': - return jsonify({ - 'message': 'OK' - }) - if request.method == 'HEAD': + if request.method == "GET": + return jsonify({"message": "OK"}) + if request.method == "HEAD": response = Response(status=200) - response.headers['Server'] = 'playready serve' + response.headers["Server"] = "playready serve" return response -@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo', methods=['GET']) +@remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo", methods=["GET"]) def remote_cdm_playready_deviceinfo(): base_name = config["default_pr_cdm"] if not base_name.endswith(".prd"): - full_file_name = (base_name + ".prd") - device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{full_file_name}') + full_file_name = base_name + ".prd" + device = PlayReadyDevice.load(f"{os.getcwd()}/configs/CDMs/PR/{full_file_name}") cdm = PlayReadyCDM.from_device(device) - return jsonify({ - 'security_level': cdm.security_level, - 'host': f'{config["fqdn"]}/remotecdm/playready', - 'secret': f'{config["remote_cdm_secret"]}', - 'device_name': Path(base_name).stem - }) + return jsonify( + { + "security_level": cdm.security_level, + "host": f'{config["fqdn"]}/remotecdm/playready', + "secret": f'{config["remote_cdm_secret"]}', + "device_name": Path(base_name).stem, + } + ) -@remotecdm_pr_bp.route('/remotecdm/playready/deviceinfo/', methods=['GET']) + +@remotecdm_pr_bp.route("/remotecdm/playready/deviceinfo/", 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'] + 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}') + 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//open', methods=['GET']) -def remote_cdm_playready_open(device): - if str(device).lower() == config['default_pr_cdm'].lower(): - pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd') - cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device) - session_id = cdm.open() - return jsonify({ - 'message': 'Success', - 'data': { - 'session_id': session_id.hex(), - 'device': { - 'security_level': cdm.security_level - } + return jsonify( + { + "security_level": cdm.security_level, + "host": f'{config["fqdn"]}/remotecdm/widevine', + "secret": f"{api_key}", + "device_name": Path(base_name).stem, } - }) - if request.headers['X-Secret-Key'] and str(device).lower() != config['default_pr_cdm'].lower(): - api_key = request.headers['X-Secret-Key'] + ) + + +@remotecdm_pr_bp.route("/remotecdm/playready//open", methods=["GET"]) +def remote_cdm_playready_open(device): + if str(device).lower() == config["default_pr_cdm"].lower(): + pr_device = PlayReadyDevice.load( + f'{os.getcwd()}/configs/CDMs/PR/{config["default_pr_cdm"]}.prd' + ) + cdm = current_app.config["CDM"] = PlayReadyCDM.from_device(pr_device) + session_id = cdm.open() + return jsonify( + { + "message": "Success", + "data": { + "session_id": session_id.hex(), + "device": {"security_level": cdm.security_level}, + }, + } + ) + if ( + request.headers["X-Secret-Key"] + and str(device).lower() != config["default_pr_cdm"].lower() + ): + api_key = request.headers["X-Secret-Key"] user = fetch_username_by_api_key(api_key=api_key) if user: if user_allowed_to_use_device(device=device, username=user): - pr_device = PlayReadyDevice.load(f'{os.getcwd()}/configs/CDMs/{user}/PR/{device}.prd') - cdm = current_app.config['CDM'] = PlayReadyCDM.from_device(pr_device) + 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 - } + 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 + 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 + 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 + return ( + jsonify( + { + "message": f"Device '{device}' is not found or you are not authorized to use it.", + } + ), + 403, + ) -@remotecdm_pr_bp.route('/remotecdm/playready//close/', methods=['GET']) + +@remotecdm_pr_bp.route( + "/remotecdm/playready//close/", methods=["GET"] +) def remote_cdm_playready_close(device, session_id): try: session_id = bytes.fromhex(session_id) cdm = current_app.config["CDM"] if not cdm: - return jsonify({ - 'message': f'No CDM for "{device}" has been opened yet. No session to close' - }), 400 + return ( + jsonify( + { + "message": f'No CDM for "{device}" has been opened yet. No session to close' + } + ), + 400, + ) try: cdm.close(session_id) except InvalidSession: - return jsonify({ - 'message': f'Invalid session ID "{session_id.hex()}", it may have expired' - }), 400 - return jsonify({ - 'message': f'Successfully closed Session "{session_id.hex()}".', - }), 200 + return ( + jsonify( + { + "message": f'Invalid session ID "{session_id.hex()}", it may have expired' + } + ), + 400, + ) + return ( + jsonify( + { + "message": f'Successfully closed Session "{session_id.hex()}".', + } + ), + 200, + ) except Exception as e: - return jsonify({ - 'message': f'Failed to close Session "{session_id.hex()}".' - }), 400 + return ( + jsonify({"message": f'Failed to close Session "{session_id.hex()}".'}), + 400, + ) -@remotecdm_pr_bp.route('/remotecdm/playready//get_license_challenge', methods=['POST']) + +@remotecdm_pr_bp.route( + "/remotecdm/playready//get_license_challenge", methods=["POST"] +) def remote_cdm_playready_get_license_challenge(device): body = request.get_json() for required_field in ("session_id", "init_data"): if not body.get(required_field): - return jsonify({ - 'message': f'Missing required field "{required_field}" in JSON body' - }), 400 + return ( + jsonify( + { + "message": f'Missing required field "{required_field}" in JSON body' + } + ), + 400, + ) cdm = current_app.config["CDM"] session_id = bytes.fromhex(body["session_id"]) init_data = body["init_data"] @@ -145,42 +204,37 @@ def remote_cdm_playready_get_license_challenge(device): if pssh.wrm_headers: init_data = pssh.wrm_headers[0] except InvalidPssh as e: - return jsonify({ - 'message': f'Unable to parse base64 PSSH, {e}' - }) + return jsonify({"message": f"Unable to parse base64 PSSH, {e}"}) try: license_request = cdm.get_license_challenge( - session_id=session_id, - wrm_header=init_data + session_id=session_id, wrm_header=init_data ) except InvalidSession: - return jsonify({ - 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired." - }) + return jsonify( + { + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + } + ) except Exception as e: - return jsonify({ - 'message': f'Error, {e}' - }) - return jsonify({ - 'message': 'success', - 'data': { - 'challenge': license_request - } - }) + return jsonify({"message": f"Error, {e}"}) + return jsonify({"message": "success", "data": {"challenge": license_request}}) -@remotecdm_pr_bp.route('/remotecdm/playready//parse_license', methods=['POST']) + +@remotecdm_pr_bp.route("/remotecdm/playready//parse_license", methods=["POST"]) def remote_cdm_playready_parse_license(device): body = request.get_json() for required_field in ("license_message", "session_id"): if not body.get(required_field): - return jsonify({ - 'message': f'Missing required field "{required_field}" in JSON body' - }) + return jsonify( + {"message": f'Missing required field "{required_field}" in JSON body'} + ) cdm = current_app.config["CDM"] if not cdm: - return jsonify({ - 'message': f"No Cdm session for {device} has been opened yet. No session to use." - }) + return jsonify( + { + "message": f"No Cdm session for {device} has been opened yet. No session to use." + } + ) session_id = bytes.fromhex(body["session_id"]) license_message = body["license_message"] if is_base64(license_message): @@ -188,45 +242,44 @@ def remote_cdm_playready_parse_license(device): try: cdm.parse_license(session_id, license_message) except InvalidSession: - return jsonify({ - 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired." - }) + return jsonify( + { + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + } + ) except InvalidLicense as e: - return jsonify({ - 'message': f"Invalid License, {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' - }) + 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//get_keys', methods=['POST']) + +@remotecdm_pr_bp.route("/remotecdm/playready//get_keys", methods=["POST"]) def remote_cdm_playready_get_keys(device): body = request.get_json() for required_field in ("session_id",): if not body.get(required_field): - return jsonify({ - 'message': f'Missing required field "{required_field}" in JSON body' - }) + 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({ - 'message': f"Missing required field '{required_field}' in JSON body." - }) + return jsonify( + {"message": f"Missing required field '{required_field}' in JSON body."} + ) try: keys = cdm.get_keys(session_id) except InvalidSession: - return jsonify({ - 'message': f"Invalid Session ID '{session_id.hex()}', it may have expired." - }) + return jsonify( + { + "message": f"Invalid Session ID '{session_id.hex()}', it may have expired." + } + ) except Exception as e: - return jsonify({ - 'message': f"Error, {e}" - }) + return jsonify({"message": f"Error, {e}"}) keys_json = [ { "key_id": key.key_id.hex, @@ -237,9 +290,4 @@ def remote_cdm_playready_get_keys(device): } for key in keys ] - return jsonify({ - 'message': 'success', - 'data': { - 'keys': keys_json - } - }) \ No newline at end of file + return jsonify({"message": "success", "data": {"keys": keys_json}}) diff --git a/routes/remote_device_wv.py b/routes/remote_device_wv.py index 8ff18df..977e780 100644 --- a/routes/remote_device_wv.py +++ b/routes/remote_device_wv.py @@ -7,381 +7,575 @@ from pywidevine.pssh import PSSH as widevinePSSH from pywidevine import __version__ from pywidevine.cdm import Cdm as widevineCDM from pywidevine.device import Device as widevineDevice -from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType, - InvalidSession, SignatureMismatch, TooManySessions) +from pywidevine.exceptions import ( + InvalidContext, + InvalidInitData, + InvalidLicenseMessage, + InvalidLicenseType, + InvalidSession, + SignatureMismatch, + TooManySessions, +) import yaml from custom_functions.database.user_db import fetch_api_key, fetch_username_by_api_key from custom_functions.user_checks.device_allowed import user_allowed_to_use_device from pathlib import Path -remotecdm_wv_bp = Blueprint('remotecdm_wv', __name__) -with open(f'{os.getcwd()}/configs/config.yaml', 'r') as file: +remotecdm_wv_bp = Blueprint("remotecdm_wv", __name__) +with open(f"{os.getcwd()}/configs/config.yaml", "r") as file: config = yaml.safe_load(file) -@remotecdm_wv_bp.route('/remotecdm/widevine', methods=['GET', 'HEAD']) + +@remotecdm_wv_bp.route("/remotecdm/widevine", methods=["GET", "HEAD"]) def remote_cdm_widevine(): - if request.method == 'GET': - return jsonify({ - 'status': 200, - 'message': f"{config['fqdn'].upper()} Remote Widevine CDM." - }) - if request.method == 'HEAD': + if request.method == "GET": + return jsonify( + {"status": 200, "message": f"{config['fqdn'].upper()} Remote Widevine CDM."} + ) + if request.method == "HEAD": response = Response(status=200) - response.headers['Server'] = f'https://github.com/devine-dl/pywidevine serve v{__version__}' + response.headers["Server"] = ( + f"https://github.com/devine-dl/pywidevine serve v{__version__}" + ) return response -@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo', methods=['GET']) + +@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo", methods=["GET"]) def remote_cdm_widevine_deviceinfo(): - if request.method == 'GET': + if request.method == "GET": base_name = config["default_wv_cdm"] if not base_name.endswith(".wvd"): - base_name = (base_name + ".wvd") - device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{base_name}') + base_name = base_name + ".wvd" + device = widevineDevice.load(f"{os.getcwd()}/configs/CDMs/WV/{base_name}") cdm = widevineCDM.from_device(device) - return jsonify({ - 'device_type': cdm.device_type.name, - 'system_id': cdm.system_id, - 'security_level': cdm.security_level, - 'host': f'{config["fqdn"]}/remotecdm/widevine', - 'secret': f'{config["remote_cdm_secret"]}', - 'device_name': Path(base_name).stem - }) + return jsonify( + { + "device_type": cdm.device_type.name, + "system_id": cdm.system_id, + "security_level": cdm.security_level, + "host": f'{config["fqdn"]}/remotecdm/widevine', + "secret": f'{config["remote_cdm_secret"]}', + "device_name": Path(base_name).stem, + } + ) -@remotecdm_wv_bp.route('/remotecdm/widevine/deviceinfo/', methods=['GET']) + +@remotecdm_wv_bp.route("/remotecdm/widevine/deviceinfo/", 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'] + 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}') + 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 - }) + 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//open', methods=['GET']) + +@remotecdm_wv_bp.route("/remotecdm/widevine//open", methods=["GET"]) def remote_cdm_widevine_open(device): - if str(device).lower() == config['default_wv_cdm'].lower(): - wv_device = widevineDevice.load(f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd') + if str(device).lower() == config["default_wv_cdm"].lower(): + wv_device = widevineDevice.load( + f'{os.getcwd()}/configs/CDMs/WV/{config["default_wv_cdm"]}.wvd' + ) cdm = current_app.config["CDM"] = widevineCDM.from_device(wv_device) session_id = cdm.open() - return jsonify({ - 'status': 200, - 'message': 'Success', - 'data': { - 'session_id': session_id.hex(), - 'device': { - 'system_id': cdm.system_id, - 'security_level': cdm.security_level, + return ( + jsonify( + { + "status": 200, + "message": "Success", + "data": { + "session_id": session_id.hex(), + "device": { + "system_id": cdm.system_id, + "security_level": cdm.security_level, + }, + }, } - } - }), 200 - if request.headers['X-Secret-Key'] and str(device).lower() != config['default_wv_cdm'].lower(): - api_key = request.headers['X-Secret-Key'] + ), + 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') + 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, + return ( + jsonify( + { + "status": 200, + "message": "Success", + "data": { + "session_id": session_id.hex(), + "device": { + "system_id": cdm.system_id, + "security_level": cdm.security_level, + }, + }, } - } - }), 200 + ), + 200, + ) else: - return jsonify({ - 'message': f"Device '{device}' is not found or you are not authorized to use it.", - 'status': 403 - }), 403 + 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 + 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 + return ( + jsonify( + { + "message": f"Device '{device}' is not found or you are not authorized to use it.", + "status": 403, + } + ), + 403, + ) -@remotecdm_wv_bp.route('/remotecdm/widevine//close/', methods=['GET']) +@remotecdm_wv_bp.route( + "/remotecdm/widevine//close/", methods=["GET"] +) def remote_cdm_widevine_close(device, session_id): - session_id = bytes.fromhex(session_id) - cdm = current_app.config["CDM"] - if not cdm: - return jsonify({ - 'status': 400, - 'message': f'No CDM for "{device}" has been opened yet. No session to close' - }), 400 - try: - cdm.close(session_id) - except InvalidSession: - return jsonify({ - 'status': 400, - 'message': f'Invalid session ID "{session_id.hex()}", it may have expired' - }), 400 - return jsonify({ - 'status': 200, - 'message': f'Successfully closed Session "{session_id.hex()}".', - }), 200 + session_id = bytes.fromhex(session_id) + cdm = current_app.config["CDM"] + if not cdm: + return ( + jsonify( + { + "status": 400, + "message": f'No CDM for "{device}" has been opened yet. No session to close', + } + ), + 400, + ) + try: + cdm.close(session_id) + except InvalidSession: + return ( + jsonify( + { + "status": 400, + "message": f'Invalid session ID "{session_id.hex()}", it may have expired', + } + ), + 400, + ) + return ( + jsonify( + { + "status": 200, + "message": f'Successfully closed Session "{session_id.hex()}".', + } + ), + 200, + ) -@remotecdm_wv_bp.route('/remotecdm/widevine//set_service_certificate', methods=['POST']) + +@remotecdm_wv_bp.route( + "/remotecdm/widevine//set_service_certificate", methods=["POST"] +) def remote_cdm_widevine_set_service_certificate(device): body = request.get_json() for required_field in ("session_id", "certificate"): if required_field == "certificate": - has_field = required_field in body # it needs the key, but can be empty/null + has_field = ( + required_field in body + ) # it needs the key, but can be empty/null else: has_field = body.get(required_field) if not has_field: - return jsonify({ - 'status': 400, - 'message': f'Missing required field "{required_field}" in JSON body' - }), 400 + return ( + jsonify( + { + "status": 400, + "message": f'Missing required field "{required_field}" in JSON body', + } + ), + 400, + ) session_id = bytes.fromhex(body["session_id"]) cdm = current_app.config["CDM"] if not cdm: - return jsonify({ - 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }), 400 + return ( + jsonify( + { + "status": 400, + "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 + 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 + return ( + jsonify( + {"status": 400, "message": f"Invalid Service Certificate, {error}"} + ), + 400, + ) except SignatureMismatch: - return jsonify({ - 'status': 400, - 'message': 'Signature Validation failed on the Service Certificate, rejecting' - }), 400 - return jsonify({ - 'status': 200, - 'message': f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.", - 'data': { - 'provider_id': provider_id, - } - }), 200 + return ( + 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//get_service_certificate', methods=['POST']) + +@remotecdm_wv_bp.route( + "/remotecdm/widevine//get_service_certificate", methods=["POST"] +) def remote_cdm_widevine_get_service_certificate(device): body = request.get_json() for required_field in ("session_id",): if not body.get(required_field): - return jsonify({ - 'status': 400, - 'message': f'Missing required field "{required_field}" in JSON body' - }), 400 + return ( + jsonify( + { + "status": 400, + "message": f'Missing required field "{required_field}" in JSON body', + } + ), + 400, + ) session_id = bytes.fromhex(body["session_id"]) cdm = current_app.config["CDM"] if not cdm: - return jsonify({ - 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }), 400 + return ( + jsonify( + { + "status": 400, + "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 + 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() + service_certificate_b64 = base64.b64encode( + service_certificate.SerializeToString() + ).decode() else: service_certificate_b64 = None - return jsonify({ - 'status': 200, - 'message': 'Successfully got the Service Certificate', - 'data': { - 'service_certificate': service_certificate_b64, - } - }), 200 + return ( + jsonify( + { + "status": 200, + "message": "Successfully got the Service Certificate", + "data": { + "service_certificate": service_certificate_b64, + }, + } + ), + 200, + ) -@remotecdm_wv_bp.route('/remotecdm/widevine//get_license_challenge/', methods=['POST']) + +@remotecdm_wv_bp.route( + "/remotecdm/widevine//get_license_challenge/", + methods=["POST"], +) def remote_cdm_widevine_get_license_challenge(device, license_type): body = request.get_json() for required_field in ("session_id", "init_data"): if not body.get(required_field): - return jsonify({ - 'status': 400, - 'message': f'Missing required field "{required_field}" in JSON body' - }), 400 + return ( + jsonify( + { + "status": 400, + "message": f'Missing required field "{required_field}" in JSON body', + } + ), + 400, + ) session_id = bytes.fromhex(body["session_id"]) privacy_mode = body.get("privacy_mode", True) cdm = current_app.config["CDM"] if not cdm: - return jsonify({ - 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }), 400 + return ( + jsonify( + { + "status": 400, + "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 + return ( + jsonify( + { + "status": 403, + "message": "No Service Certificate set but Privacy Mode is Enforced.", + } + ), + 403, + ) - current_app.config['pssh'] = body['init_data'] - init_data = widevinePSSH(body['init_data']) + current_app.config["pssh"] = body["init_data"] + init_data = widevinePSSH(body["init_data"]) try: license_request = cdm.get_license_challenge( session_id=session_id, pssh=init_data, license_type=license_type, - privacy_mode=privacy_mode + privacy_mode=privacy_mode, ) except InvalidSession: - return jsonify({ - 'status': 400, - 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' - }), 400 + return ( + 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 + 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 + 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//parse_license', methods=['POST']) +@remotecdm_wv_bp.route("/remotecdm/widevine//parse_license", methods=["POST"]) def remote_cdm_widevine_parse_license(device): body = request.get_json() for required_field in ("session_id", "license_message"): if not body.get(required_field): - return jsonify({ - 'status': 400, - 'message': f'Missing required field "{required_field}" in JSON body' - }), 400 + return ( + jsonify( + { + "status": 400, + "message": f'Missing required field "{required_field}" in JSON body', + } + ), + 400, + ) session_id = bytes.fromhex(body["session_id"]) cdm = current_app.config["CDM"] if not cdm: - return jsonify({ - 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }), 400 + return ( + jsonify( + { + "status": 400, + "message": f'No CDM session for "{device}" has been opened yet. No session to use', + } + ), + 400, + ) try: - cdm.parse_license(session_id, body['license_message']) + cdm.parse_license(session_id, body["license_message"]) except InvalidLicenseMessage as error: - return jsonify({ - 'status': 400, - 'message': f'Invalid License Message, {error}' - }), 400 + 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 + 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 + 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 + 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//get_keys/', methods=['POST']) + +@remotecdm_wv_bp.route( + "/remotecdm/widevine//get_keys/", methods=["POST"] +) def remote_cdm_widevine_get_keys(device, key_type): body = request.get_json() for required_field in ("session_id",): if not body.get(required_field): - return jsonify({ - 'status': 400, - 'message': f'Missing required field "{required_field}" in JSON body' - }), 400 + return ( + jsonify( + { + "status": 400, + "message": f'Missing required field "{required_field}" in JSON body', + } + ), + 400, + ) session_id = bytes.fromhex(body["session_id"]) key_type: Optional[str] = key_type - if key_type == 'ALL': + if key_type == "ALL": key_type = None cdm = current_app.config["CDM"] if not cdm: - return jsonify({ - 'status': 400, - 'message': f'No CDM session for "{device}" has been opened yet. No session to use' - }), 400 + return ( + jsonify( + { + "status": 400, + "message": f'No CDM session for "{device}" has been opened yet. No session to use', + } + ), + 400, + ) try: keys = cdm.get_keys(session_id, key_type) except InvalidSession: - return jsonify({ - 'status': 400, - 'message': f'Invalid Session ID "{session_id.hex()}", it may have expired' - }), 400 + 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 + 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 + "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': + if config["database_type"].lower() != "mariadb": from custom_functions.database.cache_to_db_sqlite import cache_to_db - elif config['database_type'].lower() == 'mariadb': + elif config["database_type"].lower() == "mariadb": from custom_functions.database.cache_to_db_mariadb import cache_to_db - if entry['type'] != 'SIGNING': - cache_to_db(pssh=str(current_app.config['pssh']), kid=entry['key_id'], key=entry['key']) + if entry["type"] != "SIGNING": + cache_to_db( + pssh=str(current_app.config["pssh"]), + kid=entry["key_id"], + key=entry["key"], + ) - return jsonify({ - 'status': 200, - 'message': 'Success', - 'data': { - 'keys': keys_json - } - }), 200 \ No newline at end of file + return ( + jsonify({"status": 200, "message": "Success", "data": {"keys": keys_json}}), + 200, + ) diff --git a/routes/upload.py b/routes/upload.py index 507b724..9a38c44 100644 --- a/routes/upload.py +++ b/routes/upload.py @@ -2,41 +2,41 @@ from flask import Blueprint, request, jsonify, session import os import logging -upload_bp = Blueprint('upload_bp', __name__) +upload_bp = Blueprint("upload_bp", __name__) -@upload_bp.route('/upload/', methods=['POST']) +@upload_bp.route("/upload/", methods=["POST"]) def upload(cdmtype): try: - username = session.get('username') + username = session.get("username") if not username: - return jsonify({'message': 'False', 'error': 'No username in session'}), 400 + return jsonify({"message": "False", "error": "No username in session"}), 400 # Validate CDM type - if cdmtype not in ['PR', 'WV']: - return jsonify({'message': 'False', 'error': 'Invalid CDM type'}), 400 + if cdmtype not in ["PR", "WV"]: + return jsonify({"message": "False", "error": "Invalid CDM type"}), 400 # Set up user directory paths - base_path = os.path.join(os.getcwd(), 'configs', 'CDMs', username) - pr_path = os.path.join(base_path, 'PR') - wv_path = os.path.join(base_path, 'WV') + base_path = os.path.join(os.getcwd(), "configs", "CDMs", username) + pr_path = os.path.join(base_path, "PR") + wv_path = os.path.join(base_path, "WV") # Create necessary directories if they don't exist os.makedirs(pr_path, exist_ok=True) os.makedirs(wv_path, exist_ok=True) # Get uploaded file - uploaded_file = request.files.get('file') + uploaded_file = request.files.get("file") if not uploaded_file: - return jsonify({'message': 'False', 'error': 'No file provided'}), 400 + return jsonify({"message": "False", "error": "No file provided"}), 400 # Determine correct save path based on cdmtype filename = uploaded_file.filename - save_path = os.path.join(pr_path if cdmtype == 'PR' else wv_path, filename) + 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}) + 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 + return jsonify({"message": "False", "error": "Server error"}), 500 diff --git a/routes/user_changes.py b/routes/user_changes.py index ec4014c..f8b37ad 100644 --- a/routes/user_changes.py +++ b/routes/user_changes.py @@ -2,53 +2,60 @@ import re from flask import Blueprint, request, jsonify, session from custom_functions.database.user_db import change_password, change_api_key -user_change_bp = Blueprint('user_change_bp', __name__) +user_change_bp = Blueprint("user_change_bp", __name__) # Define allowed characters regex (no spaces allowed) PASSWORD_REGEX = re.compile(r'^[A-Za-z0-9!@#$%^&*()_+\-=\[\]{};\'":\\|,.<>\/?`~]+$') -@user_change_bp.route('/user/change_password', methods=['POST']) + +@user_change_bp.route("/user/change_password", methods=["POST"]) def change_password_route(): - username = session.get('username') + username = session.get("username") if not username: - return jsonify({'message': 'False'}), 400 + return jsonify({"message": "False"}), 400 try: data = request.get_json() - new_password = data.get('new_password', '') + new_password = data.get("new_password", "") if not PASSWORD_REGEX.match(new_password): - return jsonify({'message': 'Invalid password format'}), 400 + return jsonify({"message": "Invalid password format"}), 400 change_password(username=username, new_password=new_password) - return jsonify({'message': 'True'}), 200 + return jsonify({"message": "True"}), 200 except Exception as e: - return jsonify({'message': 'False'}), 400 + return jsonify({"message": "False"}), 400 -@user_change_bp.route('/user/change_api_key', methods=['POST']) +@user_change_bp.route("/user/change_api_key", methods=["POST"]) def change_api_key_route(): # Ensure the user is logged in by checking session for 'username' - username = session.get('username') + username = session.get("username") if not username: - return jsonify({'message': 'False', 'error': 'User not logged in'}), 400 + return jsonify({"message": "False", "error": "User not logged in"}), 400 # Get the new API key from the request body - new_api_key = request.json.get('new_api_key') + new_api_key = request.json.get("new_api_key") if not new_api_key: - return jsonify({'message': 'False', 'error': 'New API key not provided'}), 400 + return jsonify({"message": "False", "error": "New API key not provided"}), 400 try: # Call the function to update the API key in the database success = change_api_key(username=username, new_api_key=new_api_key) if success: - return jsonify({'message': 'True', 'success': 'API key changed successfully'}), 200 + return ( + jsonify({"message": "True", "success": "API key changed successfully"}), + 200, + ) else: - return jsonify({'message': 'False', 'error': 'Failed to change API key'}), 500 + return ( + jsonify({"message": "False", "error": "Failed to change API key"}), + 500, + ) except Exception as e: # Catch any unexpected errors and return a response - return jsonify({'message': 'False', 'error': str(e)}), 500 \ No newline at end of file + return jsonify({"message": "False", "error": str(e)}), 500 diff --git a/routes/user_info.py b/routes/user_info.py index c5e7c71..bddd83e 100644 --- a/routes/user_info.py +++ b/routes/user_info.py @@ -2,33 +2,46 @@ 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 +from custom_functions.database.user_db import ( + fetch_api_key, + fetch_styled_username, + fetch_username_by_api_key, +) -user_info_bp = Blueprint('user_info_bp', __name__) +user_info_bp = Blueprint("user_info_bp", __name__) -@user_info_bp.route('/userinfo', methods=['POST']) + +@user_info_bp.route("/userinfo", methods=["POST"]) def user_info(): - username = session.get('username') + username = session.get("username") if not username: try: headers = request.headers - api_key = headers['Api-Key'] + api_key = headers["Api-Key"] username = fetch_username_by_api_key(api_key) except: - return jsonify({'message': 'False'}), 400 + 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'))] + 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) - }) + 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 + return jsonify({"message": "False"}), 500