diff --git a/admin-ui/.gitignore b/admin-ui/.gitignore deleted file mode 100644 index 4d26965..0000000 --- a/admin-ui/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -*.db diff --git a/admin-ui/package-lock.json b/admin-ui/package-lock.json deleted file mode 100644 index c7c8bcf..0000000 --- a/admin-ui/package-lock.json +++ /dev/null @@ -1,1251 +0,0 @@ -{ - "name": "jellyfin-lessons-admin-ui", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "jellyfin-lessons-admin-ui", - "version": "1.0.0", - "dependencies": { - "better-sqlite3": "^11.0.0", - "express": "^4.21.0" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} diff --git a/admin-ui/package.json b/admin-ui/package.json deleted file mode 100644 index 7b9c391..0000000 --- a/admin-ui/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "jellyfin-lessons-admin-ui", - "version": "1.0.0", - "description": "Web admin UI for managing Jellyfin Lessons", - "main": "server.js", - "scripts": { - "start": "node server.js" - }, - "dependencies": { - "better-sqlite3": "^11.0.0", - "express": "^4.21.0" - } -} diff --git a/admin-ui/public/index.html b/admin-ui/public/index.html deleted file mode 100644 index b37696b..0000000 --- a/admin-ui/public/index.html +++ /dev/null @@ -1,408 +0,0 @@ - - - - - - Jellyfin Lessons — Admin - - - -
- -
-

Lesson Editor

- -
-

Select a lesson from the sidebar or create a new one.

-
-
-
- - - - diff --git a/admin-ui/server.js b/admin-ui/server.js deleted file mode 100644 index 14ba129..0000000 --- a/admin-ui/server.js +++ /dev/null @@ -1,175 +0,0 @@ -const express = require('express'); -const Database = require('better-sqlite3'); -const path = require('path'); -const fs = require('fs'); -const os = require('os'); -const { execSync } = require('child_process'); - -const app = express(); -const PORT = 3099; - -// DB path shared with plugin -const DB_PATH = path.join(os.homedir(), '.local', 'share', 'jellyfin-lessons', 'lessons.db'); - -// Ensure DB directory exists -fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }); - -const db = new Database(DB_PATH); - -// UUID helper -function generateId() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { - const r = Math.random() * 16 | 0; - return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); - }); -} - -// Initialize schema — must match plugin Data/LessonStore.cs -const now = () => new Date().toISOString(); -db.exec(` - CREATE TABLE IF NOT EXISTS lessons ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - grade INTEGER NOT NULL, - subject TEXT NOT NULL, - lesson_date TEXT NOT NULL, - description TEXT, - source_path TEXT NOT NULL, - compiled_html TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS lesson_media ( - lesson_id TEXT NOT NULL, - jellyfin_item_id TEXT NOT NULL, - sort_order INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (lesson_id, jellyfin_item_id), - FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE - ); -`); - -// Middleware -app.use(express.json({ limit: '10mb' })); -app.use(express.static(path.join(__dirname, 'public'))); - -// CORS for local dev -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type'); - if (req.method === 'OPTIONS') return res.sendStatus(200); - next(); -}); - -// GET /api/lessons — list all lessons -app.get('/api/lessons', (req, res) => { - try { - const lessons = db.prepare('SELECT * FROM lessons ORDER BY updated_at DESC').all(); - res.json(lessons); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// GET /api/lessons/:id — single lesson with media items -app.get('/api/lessons/:id', (req, res) => { - try { - const lesson = db.prepare('SELECT * FROM lessons WHERE id = ?').get(req.params.id); - if (!lesson) return res.status(404).json({ error: 'Lesson not found' }); - const mediaItems = db.prepare('SELECT * FROM lesson_media WHERE lesson_id = ? ORDER BY sort_order').all(req.params.id); - res.json({ ...lesson, mediaItems }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// POST /api/lessons — create or update lesson -app.post('/api/lessons', (req, res) => { - try { - const { id, title, grade, subject, lessonDate, description, sourcePath, compiledHtml, mediaItems } = req.body; - - if (!title) return res.status(400).json({ error: 'Title is required' }); - - const upsertLesson = db.transaction(() => { - let lessonId = id || generateId(); - const ts = now(); - const existing = id ? db.prepare('SELECT created_at FROM lessons WHERE id = ?').get(id) : null; - const createdAt = existing ? existing.created_at : ts; - - db.prepare(` - INSERT INTO lessons (id, title, grade, subject, lesson_date, description, source_path, compiled_html, created_at, updated_at) - VALUES (@id, @title, @grade, @subject, @lesson_date, @description, @source_path, @compiled_html, @created_at, @updated_at) - ON CONFLICT(id) DO UPDATE SET - title = excluded.title, grade = excluded.grade, subject = excluded.subject, - lesson_date = excluded.lesson_date, description = excluded.description, - source_path = excluded.source_path, compiled_html = excluded.compiled_html, - updated_at = excluded.updated_at - `).run({ - id: lessonId, title, grade: grade || 0, - subject: subject || '', lesson_date: lessonDate || new Date().toISOString().split('T')[0], - description: description || null, source_path: sourcePath || '', - compiled_html: compiledHtml || null, created_at: createdAt, updated_at: ts - }); - - // Replace media items - db.prepare('DELETE FROM lesson_media WHERE lesson_id = ?').run(lessonId); - if (mediaItems && Array.isArray(mediaItems)) { - const insertMedia = db.prepare('INSERT INTO lesson_media (lesson_id, jellyfin_item_id, sort_order) VALUES (?, ?, ?)'); - for (const item of mediaItems) { - insertMedia.run(lessonId, item.jellyfinItemId, item.sortOrder || 0); - } - } - - return lessonId; - }); - - const lessonId = upsertLesson(); - const lesson = db.prepare('SELECT * FROM lessons WHERE id = ?').get(lessonId); - const media = db.prepare('SELECT * FROM lesson_media WHERE lesson_id = ? ORDER BY sort_order').all(lessonId); - res.json({ ...lesson, mediaItems: media }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// DELETE /api/lessons/:id -app.delete('/api/lessons/:id', (req, res) => { - try { - const result = db.prepare('DELETE FROM lessons WHERE id = ?').run(req.params.id); - if (result.changes === 0) return res.status(404).json({ error: 'Lesson not found' }); - res.json({ deleted: true }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// POST /api/compile — compile Typst to HTML -app.post('/api/compile', (req, res) => { - try { - const { typst } = req.body; - if (!typst) return res.status(400).json({ error: 'typst source is required' }); - - // Write to temp file - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'typst-')); - const srcFile = path.join(tmpDir, 'lesson.typ'); - const outFile = path.join(tmpDir, 'lesson.html'); - - fs.writeFileSync(srcFile, typst); - - try { - execSync(`typst compile "${srcFile}" "${outFile}"`, { timeout: 10000, encoding: 'utf8' }); - const html = fs.readFileSync(outFile, 'utf8'); - res.json({ html }); - } finally { - // Cleanup - fs.rmSync(tmpDir, { recursive: true, force: true }); - } - } catch (err) { - res.status(500).json({ error: err.stderr || err.message }); - } -}); - -app.listen(PORT, () => { - console.log(`Jellyfin Lessons Admin UI running at http://localhost:${PORT}`); -}); diff --git a/flake.nix b/flake.nix index 114ed09..2645a68 100644 --- a/flake.nix +++ b/flake.nix @@ -68,50 +68,5 @@ ) { }; } ); - - nixosModules.default = - { config, lib, pkgs, ... }: - let - cfg = config.services.jellyfin-lessons; - in - { - options.services.jellyfin-lessons = { - enable = lib.mkEnableOption "Jellyfin Lessons plugin"; - dataDir = lib.mkOption { - type = lib.types.str; - default = "/var/lib/jellyfin-lessons"; - description = "Directory for lessons database"; - }; - adminPort = lib.mkOption { - type = lib.types.port; - default = 3099; - description = "Port for admin UI"; - }; - }; - - config = lib.mkIf cfg.enable { - # Copy plugin DLL to Jellyfin plugins directory on jellyfin preStart - systemd.services.jellyfin = { - preStart = let - pluginPkg = self.packages.${pkgs.system}.default; - in '' - mkdir -p /var/lib/jellyfin/plugins/Lessons - cp -f ${pluginPkg}/lib/jellyfin-lessons/*.dll /var/lib/jellyfin/plugins/Lessons/ - ''; - }; - - # Admin UI as a user systemd service - systemd.user.services.jellyfin-lessons-admin = { - description = "Jellyfin Lessons Admin UI"; - after = [ "network.target" ]; - wantedBy = [ "default.target" ]; - serviceConfig = { - ExecStart = "${pkgs.nodejs}/bin/node ${self}/admin-ui/server.js"; - Restart = "on-failure"; - Environment = "PORT=${toString cfg.adminPort}"; - }; - }; - }; - }; }; } diff --git a/plugin/Api/LessonsController.cs b/plugin/Api/LessonsController.cs deleted file mode 100644 index d2f53c8..0000000 --- a/plugin/Api/LessonsController.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Jellyfin.Plugin.Lessons.Data; - -namespace Jellyfin.Plugin.Lessons.Api; - -[ApiController] -[Route("Plugins/Lessons")] -public class LessonsController : ControllerBase -{ - private readonly LessonStore _store; - - public LessonsController(LessonStore store) - { - _store = store; - } - - [HttpGet("List")] - public IActionResult List() - { - var lessons = _store.GetAllLessons(); - - var summaries = lessons.Select(l => new - { - id = l.Id, - title = l.Title, - grade = l.Grade, - subject = l.Subject, - lessonDate = l.LessonDate, - description = l.Description - }); - - return Ok(summaries); - } - - [HttpGet("Detail")] - public IActionResult Detail([FromQuery] string id) - { - if (string.IsNullOrEmpty(id)) - return BadRequest("Lesson id is required."); - - var lesson = _store.GetLessonById(id); - if (lesson == null) - return NotFound(); - - return Ok(new - { - id = lesson.Id, - title = lesson.Title, - grade = lesson.Grade, - subject = lesson.Subject, - lessonDate = lesson.LessonDate, - description = lesson.Description, - sourcePath = lesson.SourcePath, - compiledHtml = lesson.CompiledHtml, - createdAt = lesson.CreatedAt, - updatedAt = lesson.UpdatedAt, - mediaItems = lesson.MediaItems.Select(m => new - { - lessonId = m.LessonId, - jellyfinItemId = m.JellyfinItemId, - sortOrder = m.SortOrder - }) - }); - } -} diff --git a/plugin/Configuration/PluginConfiguration.cs b/plugin/Configuration/PluginConfiguration.cs deleted file mode 100644 index e9eb9b6..0000000 --- a/plugin/Configuration/PluginConfiguration.cs +++ /dev/null @@ -1,7 +0,0 @@ -using MediaBrowser.Model.Plugins; - -namespace Jellyfin.Plugin.Lessons.Configuration; - -public class PluginConfiguration : BasePluginConfiguration -{ -} diff --git a/plugin/Configuration/lessonsPage.html b/plugin/Configuration/lessonsPage.html deleted file mode 100644 index c226509..0000000 --- a/plugin/Configuration/lessonsPage.html +++ /dev/null @@ -1,460 +0,0 @@ - - - - - - Lessons - - - -
-

📚 Lessons

-
Loading lessons…
-
- - - - diff --git a/plugin/Data/LessonStore.cs b/plugin/Data/LessonStore.cs index af90313..09888b9 100644 --- a/plugin/Data/LessonStore.cs +++ b/plugin/Data/LessonStore.cs @@ -1,33 +1,41 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; using Microsoft.Data.Sqlite; -using Jellyfin.Plugin.Lessons.Data.Models; +using JellyfinLessons.Data.Models; -namespace Jellyfin.Plugin.Lessons.Data; +namespace JellyfinLessons.Data; +/// +/// SQLite-backed data store for lessons and their media associations. +/// public class LessonStore { private readonly string _connectionString; + /// + /// Creates the data store, expanding ~ in the dbPath to the user's home directory. + /// public LessonStore(string dbPath) { - if (dbPath.StartsWith("~")) - dbPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + dbPath[1..]; - - var dir = Path.GetDirectoryName(dbPath); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) - Directory.CreateDirectory(dir); + if (dbPath.StartsWith("~/") || dbPath == "~") + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + dbPath = Path.Combine(home, dbPath.TrimStart('~', '/')); + } _connectionString = $"Data Source={dbPath}"; - CreateTables(); } private void CreateTables() { - using var conn = new SqliteConnection(_connectionString); - conn.Open(); + using var connection = new SqliteConnection(_connectionString); + connection.Open(); - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" + using var command = connection.CreateCommand(); + command.CommandText = @" CREATE TABLE IF NOT EXISTS lessons ( id TEXT PRIMARY KEY, title TEXT NOT NULL, @@ -40,157 +48,150 @@ public class LessonStore created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS lesson_media ( lesson_id TEXT NOT NULL, jellyfin_item_id TEXT NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (lesson_id, jellyfin_item_id), FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE - );"; - cmd.ExecuteNonQuery(); + ); + "; + command.ExecuteNonQuery(); + + using var pragmaCmd = connection.CreateCommand(); + pragmaCmd.CommandText = "PRAGMA foreign_keys = ON;"; + pragmaCmd.ExecuteNonQuery(); } + /// + /// Returns all lessons (without media). + /// public List GetAllLessons() { var lessons = new List(); - - using var conn = new SqliteConnection(_connectionString); - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT * FROM lessons ORDER BY lesson_date DESC, subject, title"; - - using var reader = cmd.ExecuteReader(); + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT * FROM lessons ORDER BY lesson_date DESC, title ASC"; + using var reader = command.ExecuteReader(); while (reader.Read()) - lessons.Add(ReadLesson(reader)); - + lessons.Add(MapLesson(reader)); return lessons; } + /// + /// Returns a lesson by its id, or null if not found. + /// public Lesson? GetLessonById(string id) { - using var conn = new SqliteConnection(_connectionString); - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT * FROM lessons WHERE id = @id"; - cmd.Parameters.AddWithValue("@id", id); - - using var reader = cmd.ExecuteReader(); - if (!reader.Read()) - return null; - - var lesson = ReadLesson(reader); - lesson.MediaItems = GetMediaForLesson(lesson.Id); - return lesson; + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT * FROM lessons WHERE id = @id"; + command.Parameters.AddWithValue("@id", id); + using var reader = command.ExecuteReader(); + if (reader.Read()) + { + var lesson = MapLesson(reader); + lesson.MediaItems = GetMediaForLesson(id); + return lesson; + } + return null; } + /// + /// Returns all lessons for a given grade level. + /// public List GetLessonsByGrade(int grade) { var lessons = new List(); - - using var conn = new SqliteConnection(_connectionString); - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT * FROM lessons WHERE grade = @grade ORDER BY lesson_date DESC, subject, title"; - cmd.Parameters.AddWithValue("@grade", grade); - - using var reader = cmd.ExecuteReader(); + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT * FROM lessons WHERE grade = @grade ORDER BY lesson_date DESC, title ASC"; + command.Parameters.AddWithValue("@grade", grade); + using var reader = command.ExecuteReader(); while (reader.Read()) - lessons.Add(ReadLesson(reader)); - + lessons.Add(MapLesson(reader)); return lessons; } - public void UpsertLesson(Lesson lesson, List mediaItems) + /// + /// Inserts or updates a lesson and its associated media entries. + /// + public void UpsertLesson(Lesson lesson, List media) { - using var conn = new SqliteConnection(_connectionString); - conn.Open(); + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + using var transaction = connection.BeginTransaction(); - using var tx = conn.BeginTransaction(); + using var command = connection.CreateCommand(); + command.CommandText = @" + INSERT INTO lessons (id, title, grade, subject, lesson_date, description, source_path, compiled_html, created_at, updated_at) + VALUES (@id, @title, @grade, @subject, @lessonDate, @description, @sourcePath, @compiledHtml, @createdAt, @updatedAt) + ON CONFLICT(id) DO UPDATE SET + title = excluded.title, + grade = excluded.grade, + subject = excluded.subject, + lesson_date = excluded.lesson_date, + description = excluded.description, + source_path = excluded.source_path, + compiled_html = excluded.compiled_html, + updated_at = excluded.updated_at; + "; + command.Parameters.AddWithValue("@id", lesson.Id); + command.Parameters.AddWithValue("@title", lesson.Title); + command.Parameters.AddWithValue("@grade", lesson.Grade); + command.Parameters.AddWithValue("@subject", lesson.Subject); + command.Parameters.AddWithValue("@lessonDate", lesson.LessonDate); + command.Parameters.AddWithValue("@description", (object?)lesson.Description ?? DBNull.Value); + command.Parameters.AddWithValue("@sourcePath", lesson.SourcePath); + command.Parameters.AddWithValue("@compiledHtml", (object?)lesson.CompiledHtml ?? DBNull.Value); + command.Parameters.AddWithValue("@createdAt", lesson.CreatedAt); + command.Parameters.AddWithValue("@updatedAt", lesson.UpdatedAt); + command.ExecuteNonQuery(); - var now = DateTime.UtcNow.ToString("o"); + using var deleteMediaCmd = connection.CreateCommand(); + deleteMediaCmd.CommandText = "DELETE FROM lesson_media WHERE lesson_id = @lessonId"; + deleteMediaCmd.Parameters.AddWithValue("@lessonId", lesson.Id); + deleteMediaCmd.ExecuteNonQuery(); - if (string.IsNullOrEmpty(lesson.Id)) - lesson.Id = Guid.NewGuid().ToString(); - - lesson.CreatedAt = string.IsNullOrEmpty(lesson.CreatedAt) ? now : lesson.CreatedAt; - lesson.UpdatedAt = now; - - using (var cmd = conn.CreateCommand()) + foreach (var item in media) { - cmd.CommandText = @" - INSERT INTO lessons (id, title, grade, subject, lesson_date, description, source_path, compiled_html, created_at, updated_at) - VALUES (@id, @title, @grade, @subject, @lesson_date, @description, @source_path, @compiled_html, @created_at, @updated_at) - ON CONFLICT(id) DO UPDATE SET - title = excluded.title, - grade = excluded.grade, - subject = excluded.subject, - lesson_date = excluded.lesson_date, - description = excluded.description, - source_path = excluded.source_path, - compiled_html = excluded.compiled_html, - updated_at = excluded.updated_at"; - cmd.Parameters.AddWithValue("@id", lesson.Id); - cmd.Parameters.AddWithValue("@title", lesson.Title); - cmd.Parameters.AddWithValue("@grade", lesson.Grade); - cmd.Parameters.AddWithValue("@subject", lesson.Subject); - cmd.Parameters.AddWithValue("@lesson_date", lesson.LessonDate); - cmd.Parameters.AddWithValue("@description", (object?)lesson.Description ?? DBNull.Value); - cmd.Parameters.AddWithValue("@source_path", lesson.SourcePath); - cmd.Parameters.AddWithValue("@compiled_html", (object?)lesson.CompiledHtml ?? DBNull.Value); - cmd.Parameters.AddWithValue("@created_at", lesson.CreatedAt); - cmd.Parameters.AddWithValue("@updated_at", lesson.UpdatedAt); - cmd.ExecuteNonQuery(); + using var insertMediaCmd = connection.CreateCommand(); + insertMediaCmd.CommandText = "INSERT INTO lesson_media (lesson_id, jellyfin_item_id, sort_order) VALUES (@lessonId, @jellyfinItemId, @sortOrder)"; + insertMediaCmd.Parameters.AddWithValue("@lessonId", lesson.Id); + insertMediaCmd.Parameters.AddWithValue("@jellyfinItemId", item.JellyfinItemId); + insertMediaCmd.Parameters.AddWithValue("@sortOrder", item.SortOrder); + insertMediaCmd.ExecuteNonQuery(); } - // Replace media items - using (var delCmd = conn.CreateCommand()) - { - delCmd.CommandText = "DELETE FROM lesson_media WHERE lesson_id = @lesson_id"; - delCmd.Parameters.AddWithValue("@lesson_id", lesson.Id); - delCmd.ExecuteNonQuery(); - } - - foreach (var m in mediaItems) - { - using var insCmd = conn.CreateCommand(); - insCmd.CommandText = @" - INSERT INTO lesson_media (lesson_id, jellyfin_item_id, sort_order) - VALUES (@lesson_id, @jellyfin_item_id, @sort_order)"; - insCmd.Parameters.AddWithValue("@lesson_id", lesson.Id); - insCmd.Parameters.AddWithValue("@jellyfin_item_id", m.JellyfinItemId); - insCmd.Parameters.AddWithValue("@sort_order", m.SortOrder); - insCmd.ExecuteNonQuery(); - } - - tx.Commit(); + transaction.Commit(); } + /// + /// Deletes a lesson and its associated media entries (cascaded by FK if enabled). + /// public void DeleteLesson(string id) { - using var conn = new SqliteConnection(_connectionString); - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = "DELETE FROM lessons WHERE id = @id"; - cmd.Parameters.AddWithValue("@id", id); - cmd.ExecuteNonQuery(); + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = "DELETE FROM lessons WHERE id = @id"; + command.Parameters.AddWithValue("@id", id); + command.ExecuteNonQuery(); } private List GetMediaForLesson(string lessonId) { var items = new List(); - - using var conn = new SqliteConnection(_connectionString); - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT * FROM lesson_media WHERE lesson_id = @id ORDER BY sort_order"; - cmd.Parameters.AddWithValue("@id", lessonId); - - using var reader = cmd.ExecuteReader(); + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT * FROM lesson_media WHERE lesson_id = @lessonId ORDER BY sort_order ASC"; + command.Parameters.AddWithValue("@lessonId", lessonId); + using var reader = command.ExecuteReader(); while (reader.Read()) { items.Add(new LessonMedia @@ -200,11 +201,10 @@ public class LessonStore SortOrder = reader.GetInt32(2) }); } - return items; } - private static Lesson ReadLesson(SqliteDataReader reader) + private static Lesson MapLesson(SqliteDataReader reader) { return new Lesson { @@ -220,4 +220,4 @@ public class LessonStore UpdatedAt = reader.GetString(9) }; } -} +} \ No newline at end of file diff --git a/plugin/Data/Models/Lesson.cs b/plugin/Data/Models/Lesson.cs index ee5ee64..d902501 100644 --- a/plugin/Data/Models/Lesson.cs +++ b/plugin/Data/Models/Lesson.cs @@ -1,17 +1,65 @@ -namespace Jellyfin.Plugin.Lessons.Data.Models; +#nullable enable +using System.Collections.Generic; +namespace JellyfinLessons.Data.Models; + +/// +/// Represents a homeschool lesson plan. +/// public class Lesson { + /// + /// Unique identifier (Guid). + /// public string Id { get; set; } = string.Empty; + + /// + /// Lesson title. + /// public string Title { get; set; } = string.Empty; + + /// + /// Target grade level. + /// public int Grade { get; set; } + + /// + /// Subject area (e.g. Math, Science, History). + /// public string Subject { get; set; } = string.Empty; + + /// + /// Date the lesson covers (ISO 8601 format). + /// public string LessonDate { get; set; } = string.Empty; + + /// + /// Optional description of the lesson. + /// public string? Description { get; set; } + + /// + /// Path to the source markdown or plan file. + /// public string SourcePath { get; set; } = string.Empty; + + /// + /// Pre-compiled HTML rendering of the lesson. + /// public string? CompiledHtml { get; set; } + + /// + /// ISO 8601 creation timestamp. + /// public string CreatedAt { get; set; } = string.Empty; + + /// + /// ISO 8601 last-updated timestamp. + /// public string UpdatedAt { get; set; } = string.Empty; + /// + /// Associated media items (lazy-loaded). + /// public List MediaItems { get; set; } = new(); -} +} \ No newline at end of file diff --git a/plugin/Data/Models/LessonMedia.cs b/plugin/Data/Models/LessonMedia.cs index 35d88f6..1f30402 100644 --- a/plugin/Data/Models/LessonMedia.cs +++ b/plugin/Data/Models/LessonMedia.cs @@ -1,8 +1,22 @@ -namespace Jellyfin.Plugin.Lessons.Data.Models; +namespace JellyfinLessons.Data.Models; +/// +/// Join model linking lessons to Jellyfin media items. +/// public class LessonMedia { + /// + /// Foreign key to the lesson. + /// public string LessonId { get; set; } = string.Empty; + + /// + /// Identifier of the Jellyfin media item. + /// public string JellyfinItemId { get; set; } = string.Empty; + + /// + /// Display order within the lesson. + /// public int SortOrder { get; set; } -} +} \ No newline at end of file diff --git a/plugin/Jellyfin.Plugin.Lessons.csproj b/plugin/Jellyfin.Plugin.Lessons.csproj index 5682323..faba00e 100644 --- a/plugin/Jellyfin.Plugin.Lessons.csproj +++ b/plugin/Jellyfin.Plugin.Lessons.csproj @@ -3,8 +3,6 @@ net9.0 JellyfinLessons jellyfin-lessons - enable - enable @@ -18,12 +16,4 @@ runtime - - - - - - - - diff --git a/plugin/Plugin.cs b/plugin/Plugin.cs deleted file mode 100644 index 52c4269..0000000 --- a/plugin/Plugin.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Plugins; -using MediaBrowser.Model.Serialization; -using Jellyfin.Plugin.Lessons.Configuration; - -namespace Jellyfin.Plugin.Lessons; - -public class Plugin : BasePlugin, IHasWebPages -{ - public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) - : base(appPaths, xmlSerializer) - { - } - - public override string Name => "Lessons"; - - public override Guid Id => Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); - - public override string Description => "Homeschool lesson plans with embedded media"; - - public IEnumerable GetPages() - { - return new[] - { - new PluginPageInfo - { - Name = Name, - DisplayName = "Lessons", - EmbeddedResourcePath = GetType().Namespace + ".Configuration.lessonsPage.html", - EnableInMainMenu = true, - MenuSection = "plugins", - MenuIcon = "book" - } - }; - } -} diff --git a/plugin/ServiceRegistrator.cs b/plugin/ServiceRegistrator.cs deleted file mode 100644 index 378341e..0000000 --- a/plugin/ServiceRegistrator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Jellyfin.Plugin.Lessons.Data; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.DependencyInjection; - -namespace Jellyfin.Plugin.Lessons; - -public class ServiceRegistrator : IPluginServiceRegistrator -{ - public void RegisterServices(IServiceCollection services, IServerApplicationHost appHost) - { - services.AddSingleton(sp => - new LessonStore("~/.local/share/jellyfin-lessons/lessons.db")); - } -} diff --git a/templates/example-lesson.typ b/templates/example-lesson.typ deleted file mode 100644 index 44122e7..0000000 --- a/templates/example-lesson.typ +++ /dev/null @@ -1,83 +0,0 @@ -// example-lesson.typ — Complete example demonstrating the lesson template -// ============================================================ -// Frontmatter: -// title: God's Creation: The Water Cycle -// grade: 3 -// subject: Science -// date: 2026-01-15 -// media: jellyfin://uuid-water-cycle, jellyfin://uuid-cloud-formation -// description: Learn how God designed water to move through His creation — -// evaporation, condensation, and precipitation working together to sustain life. - -#import "lesson.typ": lesson-template, media-ref - -#show: doc => lesson-template( - doc, - title: "God's Creation: The Water Cycle", - grade: "3", - subject: "Science", - date: "2026-01-15", - media: ( - ("uuid-water-cycle", "The Water Cycle Explained"), - ("uuid-cloud-formation", "Cloud Formation and Weather"), - ), - description: "Learn how God designed water to move through His creation — evaporation, condensation, and precipitation working together to sustain life on Earth.", -) - -= What Is the Water Cycle? - -Water is one of God's most amazing creations! The water cycle is how water moves -all around our planet. It never runs out — it just keeps going around and around. -There are four main steps: evaporation, condensation, precipitation, and collection. - -== Evaporation - -When the sun shines on oceans, lakes, and rivers, the water gets warm and turns -into a gas called *water vapor*. This gas rises up into the sky. You can't see it, -but it's there! Evaporation happens every day, especially on warm, sunny days. - -== Condensation - -When the water vapor goes high up where it's cold, it turns back into tiny water -droplets. These droplets come together to make clouds. Have you ever looked at -clouds and seen shapes? Those shapes are made of millions of tiny water droplets -sticking together! - -== Precipitation - -When the clouds get too heavy with water droplets, the water falls back down to -Earth. This is called *precipitation*. It can be: - -- Rain — liquid water falling from the sky -- Snow — frozen crystals that fall in cold weather -- Sleet — tiny ice pellets that bounce when they land -- Hail — hard balls of ice that form in big storms - -God sends precipitation to water the plants and fill the rivers, lakes, and oceans. - -== Collection - -The water that falls onto the ground collects in rivers, lakes, and oceans. Some -of it soaks into the ground and becomes *groundwater* — water stored underground -that plants can reach with their roots. Then the sun warms the water, and the -whole cycle starts all over again! - -= Why Is the Water Cycle Important? - -God designed the water cycle to do important work: - -- Give plants the water they need to grow -- Provide fresh water for animals and people to drink -- Keep our rivers and lakes full of water -- Help control the temperature of our whole planet -- Clean the water naturally as it moves through the cycle - -*"He causes the vapors to ascend from the ends of the earth; He makes lightning -for the rain; He brings the wind out of His treasuries."* — Psalm 135:7 - -= Think About It - -1. What happens to water when the sun shines on a lake? -2. Why do clouds form high up in the sky? -3. What are the four types of precipitation? -4. How does the water cycle show God's care for His creation? diff --git a/templates/lesson.typ b/templates/lesson.typ deleted file mode 100644 index 68fe2fc..0000000 --- a/templates/lesson.typ +++ /dev/null @@ -1,90 +0,0 @@ -// lesson.typ — Typst template for homeschool lessons -// ============================================================ -// This template produces clean, structured lesson documents. -// Designed to compile to HTML for rendering inside Jellyfin. -// -// Frontmatter convention (documented as comments at top of each lesson): -// // title: The Lesson Title -// // grade: 3 -// // subject: Science -// // date: 2026-01-15 -// // media: jellyfin://uuid-1, jellyfin://uuid-2 -// // description: Short description of the lesson -// -// Usage: -// #import "lesson.typ": lesson-template, media-ref -// #show: doc => lesson-template( -// doc, -// title: "The Water Cycle", -// grade: "3", -// subject: "Science", -// date: "2026-01-15", -// media: (), -// description: "Learn about evaporation and condensation.", -// ) - -// Render a single Jellyfin media reference. -// Produces a element in HTML output -// that the Jellyfin plugin can parse and link to the player. -#let media-ref(id, title) = { - raw(lang: "jellyfin-media", id + "|" + title, block: false) -} - -// Render a list of media references as a formatted section. -#let media-section(media-items) = { - if media-items.len() > 0 { - heading(level: 2)[Media] - for item in media-items { - list.item(media-ref(item.at(0), item.at(1))) - } - } -} - -// The main lesson template. -// - body: The lesson content (passed via show rule) -// - title: Lesson title (string) -// - grade: Grade level (string, e.g. "3") -// - subject: Subject name (string) -// - date: Lesson date in YYYY-MM-DD format -// - media: Array of (id, title) pairs for linked Jellyfin media -// - description: Short description of the lesson -#let lesson-template( - body, - title: "Untitled Lesson", - grade: none, - subject: none, - date: none, - media: (), - description: none, -) = { - // Document metadata for PDF output - set document(title: title) - - // Title — prominent heading - heading(level: 1, title) - - // Metadata line: Grade X — Subject — Date - let meta-parts = () - if grade != none { meta-parts.push([Grade ] + grade) } - if subject != none { meta-parts.push(subject) } - if date != none { meta-parts.push(date) } - if meta-parts.len() > 0 { - let sep = [ — ] - let meta-text = meta-parts.first() - for i in range(1, meta-parts.len()) { - meta-text += sep + meta-parts.at(i) - } - par(text(style: "italic", size: 0.9em, meta-text)) - } - - // Description paragraph - if description != none { - par([*Description:* ] + description) - } - - // Lesson body content (renders user-authored sections, paragraphs, lists, etc.) - body - - // Media references section — listed with identifiable jellyfin-media codes - media-section(media) -}