From 50a5b79d7048f62e852c9a95454c0be00abd3d0a Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:42:54 +0000 Subject: [PATCH 01/15] feat(downloads): use taglib-wasm to set tags taglib supports multiple media formats beyond what was previously supported, this would enable transcoding to other formats without needing to write additional metadata libraries. --- bun.lock | 453 +++++++++--------- js/api.js | 3 + js/app.js | 3 + js/downloads.js | 4 + js/metadata.js | 1191 ++++++----------------------------------------- js/taglib.js | 29 ++ js/utils.js | 3 + package.json | 5 +- vite.config.js | 1 + 9 files changed, 391 insertions(+), 1301 deletions(-) create mode 100644 js/taglib.js diff --git a/bun.lock b/bun.lock index 5046b04..c498d44 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,6 @@ { "lockfileVersion": 1, - "configVersion": 0, + "configVersion": 1, "workspaces": { "": { "name": "monochrome", @@ -18,9 +18,11 @@ "jose": "^6.1.3", "npm": "^11.11.0", "pocketbase": "^0.26.8", + "taglib-wasm": "^0.9.0", }, "devDependencies": { "@neutralinojs/neu": "^11.7.0", + "@types/node": "^25.3.3", "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", "globals": "^17.4.0", @@ -30,6 +32,7 @@ "stylelint": "^16.26.1", "stylelint-config-standard": "^39.0.1", "stylelint-config-standard-scss": "^16.0.0", + "typescript": "^5.9.3", "vite": "^7.3.1", "vite-plugin-neutralino": "^1.0.3", "vite-plugin-pwa": "^1.2.0", @@ -43,39 +46,39 @@ "packages": { "@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.6", "", { "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA=="], - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], - "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], - "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ=="], + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw=="], - "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.5", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "debug": "^4.4.1", "lodash.debounce": "^4.0.8", "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg=="], + "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA=="], - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], @@ -85,11 +88,11 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2" } }, "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g=="], + "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ=="], - "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q=="], @@ -99,47 +102,47 @@ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw=="], - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw=="], + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g=="], "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], - "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg=="], + "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw=="], - "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="], + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw=="], "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], - "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q=="], + "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.29.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w=="], - "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.27.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA=="], + "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g=="], "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg=="], - "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g=="], + "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw=="], - "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="], + "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw=="], - "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.3", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg=="], + "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ=="], - "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="], + "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-replace-supers": "^7.28.6", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q=="], - "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw=="], + "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/template": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ=="], "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="], - "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw=="], + "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg=="], "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q=="], - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ=="], + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw=="], "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A=="], - "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ=="], + "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="], - "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw=="], + "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw=="], "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ=="], @@ -147,55 +150,55 @@ "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.27.1", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ=="], - "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q=="], + "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw=="], "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA=="], - "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA=="], + "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A=="], "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ=="], "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA=="], - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], - "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.28.5", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew=="], + "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.29.0", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.29.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ=="], "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w=="], - "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng=="], + "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.0", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ=="], "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ=="], - "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="], + "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg=="], - "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw=="], + "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w=="], - "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew=="], + "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.28.6", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA=="], "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng=="], - "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="], + "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ=="], - "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="], + "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w=="], "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="], - "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA=="], + "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.28.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg=="], - "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ=="], + "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA=="], "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ=="], - "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.28.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA=="], + "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog=="], - "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA=="], + "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg=="], "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw=="], "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ=="], - "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q=="], + "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA=="], "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g=="], @@ -205,27 +208,27 @@ "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg=="], - "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q=="], + "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A=="], "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw=="], - "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.27.1", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw=="], + "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.28.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q=="], - "@babel/preset-env": ["@babel/preset-env@7.28.5", "", { "dependencies": { "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", "babel-plugin-polyfill-regenerator": "^0.6.5", "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg=="], + "@babel/preset-env": ["@babel/preset-env@7.29.0", "", { "dependencies": { "@babel/compat-data": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.28.6", "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.29.0", "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.6", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-class-static-block": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-computed-properties": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.6", "@babel/plugin-transform-exponentiation-operator": "^7.28.6", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.28.6", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.28.6", "@babel/plugin-transform-modules-systemjs": "^7.29.0", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-numeric-separator": "^7.28.6", "@babel/plugin-transform-object-rest-spread": "^7.28.6", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.28.6", "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.29.0", "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.28.6", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.28.6", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs3": "^0.14.0", "babel-plugin-polyfill-regenerator": "^0.6.6", "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w=="], "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], - "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], - "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], - "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@cacheable/memory": ["@cacheable/memory@2.0.7", "", { "dependencies": { "@cacheable/utils": "^2.3.3", "@keyv/bigmap": "^1.3.0", "hookified": "^1.14.0", "keyv": "^5.5.5" } }, "sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A=="], + "@cacheable/memory": ["@cacheable/memory@2.0.8", "", { "dependencies": { "@cacheable/utils": "^2.4.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw=="], - "@cacheable/utils": ["@cacheable/utils@2.3.3", "", { "dependencies": { "hashery": "^1.3.0", "keyv": "^5.5.5" } }, "sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A=="], + "@cacheable/utils": ["@cacheable/utils@2.4.0", "", { "dependencies": { "hashery": "^1.5.0", "keyv": "^5.6.0" } }, "sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ=="], "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260301.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+kJvwociLrvy1JV9BAvoSVsMEIYD982CpFmo/yMEvBwxDIjltYsLTE8DLi0mCkGsQ8Ygidv2fD9wavzXeiY7OQ=="], @@ -241,7 +244,7 @@ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], - "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.23", "", {}, "sha512-YEmgyklR6l/oKUltidNVYdjSmLSW88vMsKx0pmiS3r71s8ZZRpd8A0Yf0U+6p/RzElmMnPBv27hNWjDQMSZRtQ=="], + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.0", "", {}, "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA=="], "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], @@ -255,69 +258,69 @@ "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], @@ -389,11 +392,7 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], - - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], @@ -409,10 +408,12 @@ "@kawarp/core": ["@kawarp/core@1.1.1", "", {}, "sha512-hnJ0CQQAa6o4HPoUE6Tkn6/cqzpA/tRPNDTNqVeoY9rozL37KweAzbypmdrYTBOdyJRR9MvETyxy4hlpenIa/w=="], - "@keyv/bigmap": ["@keyv/bigmap@1.3.0", "", { "dependencies": { "hashery": "^1.2.0", "hookified": "^1.13.0" }, "peerDependencies": { "keyv": "^5.5.4" } }, "sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg=="], + "@keyv/bigmap": ["@keyv/bigmap@1.3.1", "", { "dependencies": { "hashery": "^1.4.0", "hookified": "^1.15.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ=="], "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], + "@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="], + "@neutralinojs/lib": ["@neutralinojs/lib@6.5.0", "", { "optionalDependencies": { "@rollup/rollup-darwin-x64": "*", "@rollup/rollup-linux-x64-gnu": "*" } }, "sha512-ECgYh+CXAfMR1JVTvDw/kHhjL6LzNNcjk8Va1DZUSBkUwROqFTQ7zseFeuFtwGvutqvlWiwpGmU3s11rg/bdvA=="], "@neutralinojs/neu": ["@neutralinojs/neu@11.7.0", "", { "dependencies": { "@electron/asar": "^3.0.3", "chalk": "^4.1.0", "chokidar": "^4.0.3", "commander": "^7.2.0", "configstore": "^5.0.1", "edit-json-file": "^1.6.2", "follow-redirects": "^1.13.1", "fs-extra": "^9.0.1", "pe-library": "^1.0.1", "png2icons": "^2.0.1", "postject": "1.0.0-alpha.6", "recursive-readdir": "^2.2.2", "resedit": "^2.0.2", "spawn-command": "^1.0.0", "tcp-port-used": "^1.0.2", "uuid": "^8.3.2", "websocket": "^1.0.35", "zip-lib": "^1.0.4" }, "bin": { "neu": "bin/neu.js" } }, "sha512-fUqvR70a+BpKI9mrD92ldZkVC24Rs8XL/9m7zmOCLgCRys3yuWy7vEsxpHzKMzqTiQJkTYIsLmcR8VMzNIjuSw=="], @@ -431,63 +432,63 @@ "@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" }, "optionalPeers": ["@types/babel__core"] }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="], - "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" } }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="], + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="], "@rollup/plugin-replace": ["@rollup/plugin-replace@2.4.2", "", { "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0" } }, "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg=="], - "@rollup/plugin-terser": ["@rollup/plugin-terser@0.4.4", "", { "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" } }, "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A=="], + "@rollup/plugin-terser": ["@rollup/plugin-terser@0.4.4", "", { "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A=="], "@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], @@ -519,17 +520,19 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -555,17 +558,17 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.14", "", { "dependencies": { "@babel/compat-data": "^7.27.7", "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg=="], + "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.16", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw=="], - "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.13.0", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A=="], + "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.14.1", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.7", "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw=="], - "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], + "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.7", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.7" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA=="], "babel-runtime": ["babel-runtime@6.26.0", "", { "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } }, "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g=="], "balanced-match": ["balanced-match@2.0.0", "", {}, "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": "dist/cli.js" }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="], @@ -579,7 +582,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], @@ -591,7 +594,7 @@ "butterchurn-presets": ["butterchurn-presets@2.4.7", "", { "dependencies": { "babel-runtime": "^6.26.0", "ecma-proposal-math-extensions": "0.0.2", "lodash": "^4.17.4" } }, "sha512-4MdM8ripz/VfH1BCldrIKdAc/1ryJFBDvqlyow6Ivo1frwj0H3duzvSMFC7/wIjAjxb1QpwVHVqGqS9uAFKhpg=="], - "cacheable": ["cacheable@2.3.1", "", { "dependencies": { "@cacheable/memory": "^2.0.6", "@cacheable/utils": "^2.3.2", "hookified": "^1.14.0", "keyv": "^5.5.5", "qified": "^0.5.3" } }, "sha512-yr+FSHWn1ZUou5LkULX/S+jhfgfnLbuKQjE40tyEd4fxGZVMbBL5ifno0J0OauykS8UiCSgHi+DV/YD+rjFxFg=="], + "cacheable": ["cacheable@2.3.3", "", { "dependencies": { "@cacheable/memory": "^2.0.8", "@cacheable/utils": "^2.4.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.6.0" } }, "sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -601,7 +604,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -633,19 +636,19 @@ "core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="], - "core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], + "core-js-compat": ["core-js-compat@3.48.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q=="], - "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], + "cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], - "css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="], + "css-functions-list": ["css-functions-list@3.3.3", "", {}, "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg=="], - "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - "cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], @@ -677,15 +680,13 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "ecma-proposal-math-extensions": ["ecma-proposal-math-extensions@0.0.2", "", {}, "sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q=="], "edit-json-file": ["edit-json-file@1.8.1", "", { "dependencies": { "find-value": "^1.0.12", "iterate-object": "^1.3.4", "r-json": "^1.2.10", "set-value": "^4.1.0", "w-json": "^1.3.10" } }, "sha512-x8L381+GwqxQejPipwrUZIyAg5gDQ9tLVwiETOspgXiaQztLsrOm7luBW5+Pe31aNezuzDY79YyzF+7viCRPXA=="], - "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": "bin/cli.js" }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -713,7 +714,7 @@ "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": "bin/esbuild" }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -721,7 +722,7 @@ "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], - "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "bin/cli.js" }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], + "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], @@ -759,11 +760,11 @@ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -773,7 +774,7 @@ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "flatted": ["flatted@3.3.4", "", {}, "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA=="], "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], @@ -807,7 +808,7 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -839,17 +840,17 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hashery": ["hashery@1.4.0", "", { "dependencies": { "hookified": "^1.14.0" } }, "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ=="], + "hashery": ["hashery@1.5.0", "", { "dependencies": { "hookified": "^1.14.0" } }, "sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q=="], "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hookified": ["hookified@1.15.0", "", {}, "sha512-51w+ZZGt7Zw5q7rM3nC4t3aLn/xvKDETsXqMczndvwyVQhAHfUmUuFBRFcos8Iyebtk7OAE9dL26wFNzZVVOkw=="], + "hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="], "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], "html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="], - "htmlhint": ["htmlhint@1.9.1", "", { "dependencies": { "async": "3.2.6", "chalk": "4.1.2", "commander": "11.1.0", "glob": "^9.0.0", "is-glob": "^4.0.3", "node-sarif-builder": "^3.4.0", "strip-json-comments": "3.1.1", "xml": "1.0.1" }, "bin": { "htmlhint": "bin/htmlhint" } }, "sha512-nbnzHfgc7x3h7B83Y4fVFHDSMIjI9pepSgFhIYVE0Sj/Ulxd32loDgk+/79kjIhi4dvoIq6WlOMqbkaUQZF7lQ=="], + "htmlhint": ["htmlhint@1.9.2", "", { "dependencies": { "async": "3.2.6", "chalk": "4.1.2", "commander": "11.1.0", "glob": "^13.0.6", "is-glob": "^4.0.3", "node-sarif-builder": "3.2.0", "strip-json-comments": "3.1.1", "xml": "1.0.1" }, "bin": { "htmlhint": "bin/htmlhint" } }, "sha512-PweWSPA1Pb+AVFIOSpIGu5KhLdmtk/uf/0CpjvrDf6XUWmdTyqUljlylwSxQ0AWLvPGcBxK2n8uISsI4lCOkBQ=="], "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], @@ -959,17 +960,17 @@ "iterate-object": ["iterate-object@1.3.5", "", {}, "sha512-eL23u8oFooYTq6TtJKjp2RYjZnCkUYQvC0T/6fJfWykXJ3quvdDdzKZ3CEjy8b3JGOvLTjDYMEMIp5243R906A=="], - "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], + "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], - "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": "bin/cli.js" }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "jose": ["jose@6.2.0", "", {}, "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], @@ -983,7 +984,7 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], @@ -1011,7 +1012,7 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], @@ -1019,7 +1020,7 @@ "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], @@ -1029,7 +1030,7 @@ "mathml-tag-names": ["mathml-tag-names@2.1.3", "", {}, "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg=="], - "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], "meow": ["meow@13.2.0", "", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], @@ -1039,13 +1040,13 @@ "miniflare": ["miniflare@4.20260301.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260301.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -1053,9 +1054,9 @@ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], - "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], + "node-sarif-builder": ["node-sarif-builder@3.2.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-kVIOdynrF2CRodHZeP/97Rh1syTUHBNiw17hUCIVhlhEsWlfJm19MuO56s4MdKbr22xWx6mzMnNAgXzVlIYM9Q=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -1095,7 +1096,7 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], @@ -1113,7 +1114,7 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "postcss-media-query-parser": ["postcss-media-query-parser@0.2.3", "", {}, "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig=="], @@ -1137,7 +1138,7 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qified": ["qified@0.5.3", "", { "dependencies": { "hookified": "^1.13.0" } }, "sha512-kXuQdQTB6oN3KhI6V4acnBSZx8D2I4xzZvn9+wFLLFCoBNQY/sFnCW6c43OL7pOQ2HvGV4lnWIXNmgfp7cTWhQ=="], + "qified": ["qified@0.6.0", "", { "dependencies": { "hookified": "^1.14.0" } }, "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1163,19 +1164,19 @@ "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], - "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": "bin/parser" }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], + "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resedit": ["resedit@2.0.3", "", { "dependencies": { "pe-library": "^1.0.1" } }, "sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA=="], - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -1221,7 +1222,7 @@ "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], - "smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="], + "smob": ["smob@1.6.1", "", {}, "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -1237,8 +1238,6 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], @@ -1251,21 +1250,19 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-comments": ["strip-comments@2.0.1", "", {}, "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "stylelint": ["stylelint@16.26.1", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-syntax-patches-for-csstree": "^1.0.19", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.2.1", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^11.1.1", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.5", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.37.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.6", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": "bin/stylelint.mjs" }, "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw=="], + "stylelint": ["stylelint@16.26.1", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-syntax-patches-for-csstree": "^1.0.19", "@csstools/css-tokenizer": "^3.0.4", "@csstools/media-query-list-parser": "^4.0.3", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.2.1", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^11.1.1", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.5", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.37.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.6", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw=="], "stylelint-config-recommended": ["stylelint-config-recommended@17.0.0", "", { "peerDependencies": { "stylelint": "^16.23.0" } }, "sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA=="], - "stylelint-config-recommended-scss": ["stylelint-config-recommended-scss@16.0.2", "", { "dependencies": { "postcss-scss": "^4.0.9", "stylelint-config-recommended": "^17.0.0", "stylelint-scss": "^6.12.1" }, "peerDependencies": { "postcss": "^8.3.3", "stylelint": "^16.24.0" } }, "sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ=="], + "stylelint-config-recommended-scss": ["stylelint-config-recommended-scss@16.0.2", "", { "dependencies": { "postcss-scss": "^4.0.9", "stylelint-config-recommended": "^17.0.0", "stylelint-scss": "^6.12.1" }, "peerDependencies": { "postcss": "^8.3.3", "stylelint": "^16.24.0" }, "optionalPeers": ["postcss"] }, "sha512-aUTHhPPWCvFyWaxtckJlCPaXTDFsp4pKO8evXNCsW9OwsaUWyMd6jvcUhSmfGWPrTddvzNqK4rS/UuSLcbVGdQ=="], "stylelint-config-standard": ["stylelint-config-standard@39.0.1", "", { "dependencies": { "stylelint-config-recommended": "^17.0.0" }, "peerDependencies": { "stylelint": "^16.23.0" } }, "sha512-b7Fja59EYHRNOTa3aXiuWnhUWXFU2Nfg6h61bLfAb5GS5fX3LMUD0U5t4S8N/4tpHQg3Acs2UVPR9jy2l1g/3A=="], - "stylelint-config-standard-scss": ["stylelint-config-standard-scss@16.0.0", "", { "dependencies": { "stylelint-config-recommended-scss": "^16.0.1", "stylelint-config-standard": "^39.0.0" }, "peerDependencies": { "postcss": "^8.3.3", "stylelint": "^16.23.1" } }, "sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA=="], + "stylelint-config-standard-scss": ["stylelint-config-standard-scss@16.0.0", "", { "dependencies": { "stylelint-config-recommended-scss": "^16.0.1", "stylelint-config-standard": "^39.0.0" }, "peerDependencies": { "postcss": "^8.3.3", "stylelint": "^16.23.1" }, "optionalPeers": ["postcss"] }, "sha512-/FHECLUu+med/e6OaPFpprG86ShC4SYT7Tzb2PTVdDjJsehhFBOioSlWqYFqJxmGPIwO3AMBxNo+kY3dxrbczA=="], "stylelint-scss": ["stylelint-scss@6.14.0", "", { "dependencies": { "css-tree": "^3.0.1", "is-plain-object": "^5.0.0", "known-css-properties": "^0.37.0", "mdn-data": "^2.25.0", "postcss-media-query-parser": "^0.2.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "stylelint": "^16.8.2" } }, "sha512-ZKmHMZolxeuYsnB+PCYrTpFce0/QWX9i9gh0hPXzp73WjuIMqUpzdQaBCrKoLWh6XtCFSaNDErkMPqdjy1/8aA=="], @@ -1279,13 +1276,15 @@ "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], + "taglib-wasm": ["taglib-wasm@0.9.0", "", { "dependencies": { "@msgpack/msgpack": "^3.1.3" }, "peerDependencies": { "typescript": ">=4.5.0" } }, "sha512-E6Z/rGT6vE+9HuRnklSJNvEBdq+VyVVrXvMJ3o7/4oY3tsBwLYp949SgmkTUSegTgDzBjguTN74XVeEKtPVSOA=="], + "tcp-port-used": ["tcp-port-used@1.0.2", "", { "dependencies": { "debug": "4.3.1", "is2": "^2.0.6" } }, "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA=="], "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], "tempy": ["tempy@0.6.0", "", { "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw=="], - "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": "bin/terser" }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], + "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], @@ -1311,12 +1310,16 @@ "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="], - "ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": "script/cli.js" }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], @@ -1331,7 +1334,7 @@ "upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -1345,13 +1348,13 @@ "vite-plugin-neutralino": ["vite-plugin-neutralino@1.0.3", "", {}, "sha512-E/PSTCp7m7efk7fa4eE12WQ+5XGNP72gkhOv61X4i0n+NcnrwKtJgEgeCDGUXeLp4ngTfU/CP+EF55jmZs/0Jw=="], - "vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="], + "vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="], "w-json": ["w-json@1.3.11", "", {}, "sha512-Xa8vTinB5XBIYZlcN8YyHpE625pBU6k+lvCetTQM+FKxRtLJxAY9zUVZbRqCqkMeEGbQpKvGUzwh4wZKGem+ag=="], "websocket": ["websocket@1.0.35", "", { "dependencies": { "bufferutil": "^4.0.1", "debug": "^2.2.0", "es5-ext": "^0.10.63", "typedarray-to-buffer": "^3.1.5", "utf-8-validate": "^5.0.2", "yaeti": "^0.0.6" } }, "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -1359,7 +1362,7 @@ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -1397,10 +1400,6 @@ "workerd": ["workerd@1.20260301.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260301.1", "@cloudflare/workerd-darwin-arm64": "1.20260301.1", "@cloudflare/workerd-linux-64": "1.20260301.1", "@cloudflare/workerd-linux-arm64": "1.20260301.1", "@cloudflare/workerd-windows-64": "1.20260301.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oterQ1IFd3h7PjCfT4znSFOkJCvNQ6YMOyZ40YsnO3nrSpgB4TbJVYWFOnyJAw71/RQuupfVqZZWKvsy8GO3fw=="], - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], @@ -1427,25 +1426,25 @@ "zip-lib": ["zip-lib@1.2.1", "", { "dependencies": { "yauzl": "^3.2.0", "yazl": "^3.3.1" } }, "sha512-7MT4fvAoEJPxB/TILqapSpZlJQk5gH29NzhU4ySWXRauXh290KnHK/QkOggL9jmN92H31fv+lkbFIaFKb78ySg=="], - "@apideck/better-ajv-errors/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@apideck/better-ajv-errors/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@cacheable/memory/keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="], + "@cacheable/memory/keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], - "@cacheable/utils/keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="], + "@cacheable/utils/keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], @@ -1455,37 +1454,33 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@keyv/bigmap/keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="], + "@keyv/bigmap/keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], - "@rollup/plugin-babel/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], + "@rollup/plugin-babel/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="], - "@rollup/plugin-node-resolve/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + "@rollup/plugin-node-resolve/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], - "@rollup/plugin-replace/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], + "@rollup/plugin-replace/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="], "@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "@rollup/pluginutils/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], + "@rollup/pluginutils/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="], - "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "cacheable/keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="], + "cacheable/keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], "configstore/write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], @@ -1493,21 +1488,21 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - "glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], - "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": "bin/which" }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], "htmlhint/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "make-dir/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "node-sarif-builder/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "node-sarif-builder/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], "npm/@gar/promise-retry": ["@gar/promise-retry@1.0.2", "", { "dependencies": { "retry": "^0.13.1" } }, "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g=="], @@ -1591,7 +1586,7 @@ "npm/common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], - "npm/cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "npm/cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "npm/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -1605,7 +1600,7 @@ "npm/fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", { "bundled": true }, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], - "npm/fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "npm/fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "npm/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" }, "bundled": true }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], @@ -1667,7 +1662,7 @@ "npm/libnpmversion": ["libnpmversion@8.0.3", "", { "dependencies": { "@npmcli/git": "^7.0.0", "@npmcli/run-script": "^10.0.0", "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.3.7" }, "bundled": true }, "sha512-Avj1GG3DT6MGzWOOk3yA7rORcMDUPizkIGbI8glHCO7WoYn3NYNmskLDwxg2NMY1Tyf2vrHAqTuSG58uqd1lJg=="], - "npm/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "npm/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "npm/make-fetch-happen": ["make-fetch-happen@15.0.4", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" }, "bundled": true }, "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw=="], @@ -1801,8 +1796,6 @@ "npm/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], "r-json/w-json": ["w-json@1.3.10", "", {}, "sha512-XadVyw0xE+oZ5FGApXsdswv96rOhStzKqL53uSe5UaTadABGkWIg1+DTx8kiZ/VqTZTBneoL0l65RcPe4W3ecw=="], @@ -1811,13 +1804,11 @@ "stringify-object/is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="], - "stylelint/file-entry-cache": ["file-entry-cache@11.1.1", "", { "dependencies": { "flat-cache": "^6.1.19" } }, "sha512-TPVFSDE7q91Dlk1xpFLvFllf8r0HyOMOlnWy7Z2HBku5H3KhIeOGInexrIeg2D64DosVB/JXkrrk6N/7Wriq4A=="], + "stylelint/file-entry-cache": ["file-entry-cache@11.1.2", "", { "dependencies": { "flat-cache": "^6.1.20" } }, "sha512-N2WFfK12gmrK1c1GXOqiAJ1tc5YE+R53zvQ+t5P8S5XhnmKYVB5eZEiLNZKDSmoG8wqqbF9EXYBBW/nef19log=="], "stylelint/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "stylelint-scss/mdn-data": ["mdn-data@2.26.0", "", {}, "sha512-ZqI0qjKWHMPcGUfLmlr80NPNVHIOjPMHtIOe1qXYFGS0YBZ1YKAzo9yk8W+gGrLCN0Xdv/RKxqdIsqPakEfmow=="], - - "table/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "table/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "tcp-port-used/debug": ["debug@4.3.1", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ=="], @@ -1825,35 +1816,25 @@ "websocket/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "workbox-build/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "workbox-build/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "workbox-build/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": "dist/esm/bin.mjs" }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "workbox-build/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], - "workbox-build/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "workbox-build/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="], "yazl/buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], "@apideck/better-ajv-errors/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "@rollup/plugin-node-resolve/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "configstore/write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "npm/minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -1861,7 +1842,7 @@ "npm/promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "stylelint/file-entry-cache/flat-cache": ["flat-cache@6.1.19", "", { "dependencies": { "cacheable": "^2.2.0", "flatted": "^3.3.3", "hookified": "^1.13.0" } }, "sha512-l/K33newPTZMTGAnnzaiqSl6NnH7Namh8jBNjrgjprWxGmZUuxx/sJNIRaijOh3n7q7ESbhNZC+pvVZMFdeU4A=="], + "stylelint/file-entry-cache/flat-cache": ["flat-cache@6.1.20", "", { "dependencies": { "cacheable": "^2.3.2", "flatted": "^3.3.3", "hookified": "^1.15.0" } }, "sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ=="], "table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -1871,24 +1852,18 @@ "workbox-build/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "workbox-build/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - - "workbox-build/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "workbox-build/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], - - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "workbox-build/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "npm/minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "npm/minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "workbox-build/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "workbox-build/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "workbox-build/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/js/api.js b/js/api.js index e44f867..fef8435 100644 --- a/js/api.js +++ b/js/api.js @@ -12,6 +12,7 @@ import { addMetadataToAudio } from './metadata.js'; import { DashDownloader } from './dash-downloader.js'; import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js'; import { ffmpeg } from './ffmpeg.js'; +import { initTagLib } from './taglib.js'; export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25'; @@ -1109,6 +1110,8 @@ export class LosslessAPI { } async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) { + // Initialize taglib in the background. + initTagLib().catch(console.error); const { onProgress, track } = options; try { diff --git a/js/app.js b/js/app.js index 7126174..ddf454d 100644 --- a/js/app.js +++ b/js/app.js @@ -98,6 +98,8 @@ let settingsModule = null; let downloadsModule = null; let metadataModule = null; +export const managers = {}; + async function loadSettingsModule() { if (!settingsModule) { settingsModule = await import('./settings.js'); @@ -496,6 +498,7 @@ document.addEventListener('DOMContentLoaded', async () => { window.monochromeScrobbler = scrobbler; const lyricsManager = new LyricsManager(api); ui.lyricsManager = lyricsManager; + managers.lyricsManager = lyricsManager; // Check browser support for local files const selectLocalBtn = document.getElementById('select-local-folder-btn'); diff --git a/js/downloads.js b/js/downloads.js index f3c16ce..a67a59b 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -17,6 +17,7 @@ import { DashDownloader } from './dash-downloader.js'; import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js'; import { encodeToMp3 } from './mp3-encoder.js'; import { ffmpeg } from './ffmpeg.js'; +import { initTagLib } from './taglib.js'; const downloadTasks = new Map(); const bulkDownloadTasks = new Map(); @@ -269,6 +270,9 @@ function removeBulkDownloadTask(notifEl) { } async function downloadTrackBlob(track, quality, api, lyricsManager = null, signal = null) { + // Initialize taglib in the background. + initTagLib().catch(console.error); + let enrichedTrack = { ...track, artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null), diff --git a/js/metadata.js b/js/metadata.js index 70e591f..7939788 100644 --- a/js/metadata.js +++ b/js/metadata.js @@ -1,5 +1,7 @@ -import { getCoverBlob, detectAudioFormat, getTrackTitle } from './utils.js'; -import { addMp3Metadata } from './id3-writer.js'; +import { getCoverBlob, getTrackTitle } from './utils.js'; +import { initTagLib } from './taglib.js'; +import { PICTURE_TYPE_VALUES } from 'taglib-wasm'; +import { managers } from './app.js'; const VENDOR_STRING = 'Monochrome'; const DEFAULT_TITLE = 'Unknown Title'; @@ -49,24 +51,134 @@ function getFullArtistString(track) { * @returns {Promise} - Audio blob with embedded metadata */ export async function addMetadataToAudio(audioBlob, track, api, _quality) { - // Always check actual file signature, not just quality setting - // DASH Hi-Res streams may return fragmented MP4 instead of raw FLAC - const buffer = await audioBlob.slice(0, 12).arrayBuffer(); - const view = new DataView(buffer); + const tagLib = await initTagLib(); + const file = await tagLib.open(await audioBlob.arrayBuffer()); - const format = detectAudioFormat(view, audioBlob.type); + try { + const isMp4 = file.isMP4(); - switch (format) { - case 'flac': - return await addFlacMetadata(audioBlob, track, api); - case 'mp4': - return await addM4aMetadata(audioBlob, track, api); - case 'mp3': - return await addMp3Metadata(audioBlob, track, api); - default: - // Unknown format - return original without modification - console.warn(`Unknown audio format (mime: ${audioBlob.type}), returning original blob`); - return audioBlob; + const discNumber = track.volumeNumber ?? track.discNumber; + const lyricsFetch = managers?.lyricsManager?.fetchLyrics?.(track.id, track); + const coverFetch = getCoverBlob(api, track.album.cover); + + // Add standard tags + if (track.title) { + file.setProperty('TITLE', getTrackTitle(track)); + } + const artistStr = getFullArtistString(track); + if (artistStr) { + file.setProperty('ARTIST', artistStr); + } + if (track.album?.title) { + file.setProperty('ALBUM', track.album.title); + } + const albumArtist = track.album?.artist?.name || track.artist?.name; + if (albumArtist) { + file.setProperty('ALBUMARTIST', albumArtist); + } + if (track.trackNumber) { + let trackString = String(track.trackNumber); + + if (isMp4 && track.trackNumber && track.album?.numberOfTracks) { + trackString = `${track.trackNumber}/${track.album.numberOfTracks}`; + } + + if (isMp4) { + file.setProperty('TRACKNUMBER', trackString); + } else { + file.setProperty('TRACKNUMBER', String(track.trackNumber)); + } + } + if (!isMp4 && track.album?.numberOfTracks) { + file.setProperty('TRACKTOTAL', String(track.album.numberOfTracks)); + } + + if (discNumber) { + file.setProperty('DISCNUMBER', String(discNumber)); + } + + if (track.bpm != null) { + const bpm = Number(track.bpm); + if (Number.isFinite(bpm)) { + file.setProperty('BPM', String(Math.round(bpm))); + } + } + if (track.replayGain) { + const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = track.replayGain; + if (albumReplayGain) file.setProperty('REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain)); + if (albumPeakAmplitude) file.setProperty('REPLAYGAIN_ALBUM_PEAK', String(albumPeakAmplitude)); + if (trackReplayGain) file.setProperty('REPLAYGAIN_TRACK_GAIN', String(trackReplayGain)); + if (trackPeakAmplitude) file.setProperty('REPLAYGAIN_TRACK_PEAK', String(trackPeakAmplitude)); + } + + const releaseDateStr = + track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : ''); + if (releaseDateStr) { + try { + const year = new Date(releaseDateStr).getFullYear(); + if (!isNaN(year)) { + file.setProperty('DATE', String(year)); + } + } catch { + // Invalid date, skip + } + } + + if (track.copyright) { + file.setProperty('COPYRIGHT', track.copyright); + } + if (track.isrc) { + file.setProperty('ISRC', track.isrc); + + if (isMp4) { + file.setMP4Item('xid ', `:isrc:${track.isrc}`); + } + } + if (track.explicit) { + if (isMp4) { + file.setMP4Item('rtng', '1'); + } else { + file.setProperty('ITUNESADVISORY', '1'); + } + } + + if (track.album?.cover) { + const coverBlob = await coverFetch; + const coverBuffer = new Uint8Array(await coverBlob.arrayBuffer()); + + if (coverBlob) { + file.setPictures([ + { + mimeType: coverBlob.type, + data: coverBuffer, + type: PICTURE_TYPE_VALUES.FrontCover, + description: 'Cover Art', + }, + ]); + } + } + + try { + const lyrics = await lyricsFetch; + const lyricsString = lyrics?.subtitles || lyrics?.plainLyrics; + + if (lyricsString) { + //if (isMp4) { + // file.setMP4Item('@lyr', String(lyricsString)); + //} else { + file.setProperty('LYRICS', String(lyricsString).replace(/\r/g, '').replace(/\n/g, '\r\n')); + //} + } + } catch (e) { + console.warn('Error fetching lyrics', track, e); + } + + await file.save(); + + return new Blob([file.getFileBuffer()], { type: audioBlob.type, name: audioBlob.name }); + } finally { + // Always dispose, even if there was an error. + file.dispose(); } } @@ -526,78 +638,6 @@ function getMimeType(data) { return 'image/jpeg'; } -/** - * Adds Vorbis comment metadata to FLAC files - */ -async function addFlacMetadata(flacBlob, track, api) { - try { - const arrayBuffer = await flacBlob.arrayBuffer(); - const dataView = new DataView(arrayBuffer); - - // Verify FLAC signature - if (!isFlacFile(dataView)) { - console.warn('Not a valid FLAC file, returning original'); - return flacBlob; - } - - // Parse FLAC structure - const blocks = parseFlacBlocks(dataView); - - // If parsing failed or no audio data found, return original - if (!blocks || blocks.length === 0 || blocks.audioDataOffset === undefined) { - console.warn('Failed to parse FLAC blocks, returning original'); - return flacBlob; - } - - // Check for STREAMINFO block (must be first, type 0) - if (blocks[0].type !== 0) { - console.warn('FLAC file missing STREAMINFO block, returning original'); - return flacBlob; - } - - // Create or update Vorbis comment block - const vorbisCommentBlock = createVorbisCommentBlock(track); - - // Fetch album artwork if available - let pictureBlock = null; - if (track.album?.cover) { - try { - pictureBlock = await createFlacPictureBlock(track.album.cover, api); - } catch (error) { - console.warn('Failed to embed album art:', error); - } - } - - // Rebuild FLAC file with new metadata - let newFlacData; - try { - newFlacData = rebuildFlacWithMetadata(dataView, blocks, vorbisCommentBlock, pictureBlock); - } catch (rebuildError) { - console.error('Failed to rebuild FLAC structure:', rebuildError); - return flacBlob; - } - - // Validate the rebuilt file - const validationView = new DataView(newFlacData.buffer); - if (!isFlacFile(validationView)) { - console.error('Rebuilt FLAC has invalid signature, returning original'); - return flacBlob; - } - - // Validate new file has proper block structure - const newBlocks = parseFlacBlocks(validationView); - if (!newBlocks || newBlocks.length === 0 || newBlocks.audioDataOffset === undefined) { - console.error('Rebuilt FLAC has invalid block structure, returning original'); - return flacBlob; - } - - return new Blob([newFlacData], { type: 'audio/flac' }); - } catch (error) { - console.error('Failed to add FLAC metadata:', error); - return flacBlob; - } -} - function isFlacFile(dataView) { // Check for "fLaC" signature at the beginning return ( @@ -663,336 +703,6 @@ function parseFlacBlocks(dataView) { return blocks; } -function createVorbisCommentBlock(track) { - // Vorbis comment structure - const comments = []; - const discNumber = track.volumeNumber ?? track.discNumber; - - // Add standard tags - if (track.title) { - comments.push(['TITLE', getTrackTitle(track)]); - } - const artistStr = getFullArtistString(track); - if (artistStr) { - comments.push(['ARTIST', artistStr]); - } - if (track.album?.title) { - comments.push(['ALBUM', track.album.title]); - } - const albumArtist = track.album?.artist?.name || track.artist?.name; - if (albumArtist) { - comments.push(['ALBUMARTIST', albumArtist]); - } - if (track.trackNumber) { - comments.push(['TRACKNUMBER', String(track.trackNumber)]); - } - if (discNumber) { - comments.push(['DISCNUMBER', String(discNumber)]); - } - if (track.album?.numberOfTracks) { - comments.push(['TRACKTOTAL', String(track.album.numberOfTracks)]); - } - if (track.bpm != null) { - const bpm = Number(track.bpm); - if (Number.isFinite(bpm)) { - comments.push(['TEMPO', String(Math.round(bpm))]); - } - } - if (track.replayGain) { - const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = track.replayGain; - if (albumReplayGain) comments.push(['REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain)]); - if (albumPeakAmplitude) comments.push(['REPLAYGAIN_ALBUM_PEAK', String(albumPeakAmplitude)]); - if (trackReplayGain) comments.push(['REPLAYGAIN_TRACK_GAIN', String(trackReplayGain)]); - if (trackPeakAmplitude) comments.push(['REPLAYGAIN_TRACK_PEAK', String(trackPeakAmplitude)]); - } - - const releaseDateStr = - track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : ''); - if (releaseDateStr) { - try { - const year = new Date(releaseDateStr).getFullYear(); - if (!isNaN(year)) { - comments.push(['DATE', String(year)]); - } - } catch { - // Invalid date, skip - } - } - - if (track.copyright) { - comments.push(['COPYRIGHT', track.copyright]); - } - if (track.isrc) { - comments.push(['ISRC', track.isrc]); - } - if (track.explicit) { - comments.push(['ITUNESADVISORY', '1']); - } - - // Calculate total size - const vendor = VENDOR_STRING; - const vendorBytes = new TextEncoder().encode(vendor); - - let totalSize = 4 + vendorBytes.length + 4; // vendor length + vendor + comment count - - const encodedComments = comments.map(([key, value]) => { - const text = `${key}=${value}`; - const bytes = new TextEncoder().encode(text); - totalSize += 4 + bytes.length; - return bytes; - }); - - // Create buffer - const buffer = new ArrayBuffer(totalSize); - const view = new DataView(buffer); - const uint8Array = new Uint8Array(buffer); - - let offset = 0; - - // Vendor length (little-endian) - view.setUint32(offset, vendorBytes.length, true); - offset += 4; - - // Vendor string - uint8Array.set(vendorBytes, offset); - offset += vendorBytes.length; - - // Comment count (little-endian) - view.setUint32(offset, comments.length, true); - offset += 4; - - // Comments - for (const commentBytes of encodedComments) { - view.setUint32(offset, commentBytes.length, true); - offset += 4; - uint8Array.set(commentBytes, offset); - offset += commentBytes.length; - } - - return uint8Array; -} - -async function createFlacPictureBlock(coverId, api) { - try { - // Fetch album art - const imageBlob = await getCoverBlob(api, coverId); - if (!imageBlob) { - throw new Error('Failed to fetch album art'); - } - - const imageBytes = new Uint8Array(await imageBlob.arrayBuffer()); - - // Detect MIME type from blob or use default - const mimeType = imageBlob.type || 'image/jpeg'; - const mimeBytes = new TextEncoder().encode(mimeType); - const description = ''; - const descBytes = new TextEncoder().encode(description); - - // Calculate total size - const totalSize = - 4 + // picture type - 4 + - mimeBytes.length + // mime length + mime - 4 + - descBytes.length + // desc length + desc - 4 + // width - 4 + // height - 4 + // color depth - 4 + // indexed colors - 4 + - imageBytes.length; // image length + image - - const buffer = new ArrayBuffer(totalSize); - const view = new DataView(buffer); - const uint8Array = new Uint8Array(buffer); - - let offset = 0; - - // Picture type (3 = front cover) - view.setUint32(offset, 3, false); - offset += 4; - - // MIME type length - view.setUint32(offset, mimeBytes.length, false); - offset += 4; - - // MIME type - uint8Array.set(mimeBytes, offset); - offset += mimeBytes.length; - - // Description length - view.setUint32(offset, descBytes.length, false); - offset += 4; - - // Description (empty) - if (descBytes.length > 0) { - uint8Array.set(descBytes, offset); - offset += descBytes.length; - } - - // Width (0 = unknown) - view.setUint32(offset, 0, false); - offset += 4; - - // Height (0 = unknown) - view.setUint32(offset, 0, false); - offset += 4; - - // Color depth (0 = unknown) - view.setUint32(offset, 0, false); - offset += 4; - - // Indexed colors (0 = not indexed) - view.setUint32(offset, 0, false); - offset += 4; - - // Image data length - view.setUint32(offset, imageBytes.length, false); - offset += 4; - - // Image data - uint8Array.set(imageBytes, offset); - - return uint8Array; - } catch (error) { - console.error('Failed to create FLAC picture block:', error); - return null; - } -} - -function rebuildFlacWithMetadata(dataView, blocks, vorbisCommentBlock, pictureBlock) { - const originalArray = new Uint8Array(dataView.buffer); - - // Remove old Vorbis comment and picture blocks - const filteredBlocks = blocks.filter((b) => b.type !== 4 && b.type !== 6); // 4 = Vorbis, 6 = Picture - - // Calculate new file size - let newSize = 4; // "fLaC" signature - - // Add STREAMINFO and other essential blocks - for (const block of filteredBlocks) { - newSize += 4 + block.size; // header + data - } - - // Add new Vorbis comment block - newSize += 4 + vorbisCommentBlock.length; - - // Add picture block if available - if (pictureBlock) { - newSize += 4 + pictureBlock.length; - } - - // Add audio data - const audioDataOffset = blocks.audioDataOffset; - if (audioDataOffset === undefined) { - throw new Error('Invalid FLAC file structure: unable to locate audio data stream'); - } - const audioDataSize = dataView.byteLength - audioDataOffset; - newSize += audioDataSize; - - // Build new file - const newFile = new Uint8Array(newSize); - let offset = 0; - - // Write "fLaC" signature - newFile[offset++] = 0x66; // 'f' - newFile[offset++] = 0x4c; // 'L' - newFile[offset++] = 0x61; // 'a' - newFile[offset++] = 0x43; // 'C' - - // Write existing blocks (except Vorbis and Picture) - for (let i = 0; i < filteredBlocks.length; i++) { - const block = filteredBlocks[i]; - const isLast = false; // We'll add more blocks - - // Write block header - const header = (isLast ? 0x80 : 0x00) | block.type; - newFile[offset++] = header; - newFile[offset++] = (block.size >> 16) & 0xff; - newFile[offset++] = (block.size >> 8) & 0xff; - newFile[offset++] = block.size & 0xff; - - // Write block data - newFile.set(originalArray.subarray(block.offset, block.offset + block.size), offset); - offset += block.size; - } - - // Write new Vorbis comment block - const vorbisHeaderOffset = offset; - const vorbisHeader = 0x04; // Vorbis comment type - newFile[offset++] = vorbisHeader; - newFile[offset++] = (vorbisCommentBlock.length >> 16) & 0xff; - newFile[offset++] = (vorbisCommentBlock.length >> 8) & 0xff; - newFile[offset++] = vorbisCommentBlock.length & 0xff; - newFile.set(vorbisCommentBlock, offset); - offset += vorbisCommentBlock.length; - - let lastBlockHeaderOffset = vorbisHeaderOffset; - - // Write picture block if available - if (pictureBlock) { - const pictureHeaderOffset = offset; - const pictureHeader = 0x06; // Picture type - newFile[offset++] = pictureHeader; - newFile[offset++] = (pictureBlock.length >> 16) & 0xff; - newFile[offset++] = (pictureBlock.length >> 8) & 0xff; - newFile[offset++] = pictureBlock.length & 0xff; - newFile.set(pictureBlock, offset); - offset += pictureBlock.length; - lastBlockHeaderOffset = pictureHeaderOffset; - } - - // Mark the last metadata block with the 0x80 flag - newFile[lastBlockHeaderOffset] |= 0x80; - - // Write audio data - if (audioDataSize > 0) { - newFile.set(originalArray.subarray(audioDataOffset, audioDataOffset + audioDataSize), offset); - } - - return newFile; -} - -/** - * Adds metadata to M4A files using MP4 atoms - */ -async function addM4aMetadata(m4aBlob, track, api) { - try { - const arrayBuffer = await m4aBlob.arrayBuffer(); - const dataView = new DataView(arrayBuffer); - - // Parse MP4 atoms - const atoms = parseMp4Atoms(dataView); - - // Create metadata atoms - const metadataAtoms = createMp4MetadataAtoms(track); - - // Fetch album artwork if available - if (track.album?.cover) { - try { - const imageBlob = await getCoverBlob(api, track.album.cover); - if (imageBlob) { - const imageBytes = new Uint8Array(await imageBlob.arrayBuffer()); - metadataAtoms.cover = { - type: 'covr', - data: imageBytes, - }; - } - } catch (error) { - console.warn('Failed to embed album art in M4A:', error); - } - } - - // Rebuild MP4 file with metadata - const newMp4Data = rebuildMp4WithMetadata(dataView, atoms, metadataAtoms); - - return new Blob([newMp4Data], { type: 'audio/mp4' }); - } catch (error) { - console.error('Failed to add M4A metadata:', error); - return m4aBlob; - } -} - function parseMp4Atoms(dataView) { const atoms = []; let offset = 0; @@ -1042,644 +752,3 @@ function parseMp4Atoms(dataView) { return atoms; } - -function createMp4MetadataAtoms(track) { - // MP4 metadata atoms are more complex than FLAC - // We'll create basic iTunes-style metadata - - /** - * Array of arrays: [namespace, name, value] - */ - const userTags = []; - const tags = { - '©nam': getTrackTitle(track) || DEFAULT_TITLE, - '©ART': getFullArtistString(track) || DEFAULT_ARTIST, - '©alb': track.album?.title || DEFAULT_ALBUM, - aART: track.album?.artist?.name || track.artist?.name || DEFAULT_ARTIST, - }; - - if (track.isrc) { - tags['ISRC'] = track.isrc; - tags['xid '] = ':isrc:' + track.isrc; - } - - if (track.copyright) { - tags['cprt'] = track.copyright; - } - - if (track.trackNumber) { - tags['trkn'] = { - current: track.trackNumber, - total: track.album?.numberOfTracks, - }; - } - if (track.explicit) { - tags['rtng'] = 1; // 1 = Explicit, 2 = Clean, 0 = Unknown - } - - const discNumber = track.volumeNumber ?? track.discNumber; - if (discNumber) { - tags['disk'] = { - current: discNumber, - total: 0, - }; - } - - if (track.bpm) { - tags['tmpo'] = Math.round(track.bpm); - } - - const releaseDateStr = - track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : ''); - if (releaseDateStr) { - try { - const year = new Date(releaseDateStr).getFullYear(); - if (!isNaN(year)) { - tags['©day'] = String(year); - } - } catch { - // Invalid date, skip - } - } - - if (track.replayGain) { - const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = track.replayGain; - let trackPeakAmplitudeString = String(trackPeakAmplitude); - let albumPeakAmplitudeString = String(albumPeakAmplitude); - - if (trackPeakAmplitudeString.indexOf('.') === -1) { - trackPeakAmplitudeString += '.000000'; - } - if (albumPeakAmplitudeString.indexOf('.') === -1) { - albumPeakAmplitudeString += '.000000'; - } - - if (trackPeakAmplitude) userTags.push(['com.apple.iTunes', 'replaygain_track_peak', trackPeakAmplitudeString]); - if (trackReplayGain) userTags.push(['com.apple.iTunes', 'replaygain_track_gain', `${trackReplayGain} dB`]); - if (albumPeakAmplitude) userTags.push(['com.apple.iTunes', 'replaygain_album_peak', albumPeakAmplitudeString]); - if (albumReplayGain) userTags.push(['com.apple.iTunes', 'replaygain_album_gain', `${albumReplayGain} dB`]); - } - - return { tags, userTags }; -} - -function rebuildMp4WithMetadata(dataView, atoms, metadataAtoms) { - const originalArray = new Uint8Array(dataView.buffer); - - // Find moov atom - const moovAtom = atoms.find((a) => a.type === 'moov'); - if (!moovAtom) { - console.warn('No moov atom found in M4A file'); - return originalArray; - } - - // Construct the new metadata block (udta -> meta -> ilst) - const newMetadataBytes = createMetadataBlock(metadataAtoms); - - // We need to insert this into the moov atom. - // If udta exists, we merge/replace. For simplicity, we'll append/create. - // Ideally, we should parse moov children to find udta. - - // 1. Calculate new sizes - // New file size = Original size + Metadata block size - // Note: If we are replacing existing metadata, this calculation would be different, - // but here we are assuming we are adding fresh or appending. - // A robust implementation would parse moov children, remove existing udta, and add new one. - - // Let's try to do it right: parse moov children - const moovChildren = parseMp4Atoms(new DataView(originalArray.buffer, moovAtom.offset + 8, moovAtom.size - 8)); - - // Filter out existing udta to replace it - const filteredMoovChildren = moovChildren.filter((a) => a.type !== 'udta'); - - // Calculate new moov size - // Header (8) + Sum of other children sizes + New Metadata Block Size - let newMoovSize = 8; - for (const child of filteredMoovChildren) { - newMoovSize += child.size; - } - newMoovSize += newMetadataBytes.length; - - const sizeDiff = newMoovSize - moovAtom.size; - const newFileSize = originalArray.length + sizeDiff; - - const newFile = new Uint8Array(newFileSize); - let offset = 0; - let originalOffset = 0; - - // Copy atoms before moov - const atomsBeforeMoov = atoms.filter((a) => a.offset < moovAtom.offset); - for (const atom of atomsBeforeMoov) { - newFile.set(originalArray.subarray(atom.offset, atom.offset + atom.size), offset); - offset += atom.size; - originalOffset += atom.size; - } - - // Write new moov atom - // Size - newFile[offset++] = (newMoovSize >> 24) & 0xff; - newFile[offset++] = (newMoovSize >> 16) & 0xff; - newFile[offset++] = (newMoovSize >> 8) & 0xff; - newFile[offset++] = newMoovSize & 0xff; - - // Type 'moov' - newFile[offset++] = 0x6d; - newFile[offset++] = 0x6f; - newFile[offset++] = 0x6f; - newFile[offset++] = 0x76; - - // Write preserved children of moov - for (const child of filteredMoovChildren) { - const absoluteChildStart = moovAtom.offset + 8 + child.offset; - newFile.set(originalArray.subarray(absoluteChildStart, absoluteChildStart + child.size), offset); - offset += child.size; - } - - // Write new metadata block (udta) - newFile.set(newMetadataBytes, offset); - offset += newMetadataBytes.length; - - // Update originalOffset to skip old moov - originalOffset = moovAtom.offset + moovAtom.size; - - // Copy atoms after moov - // Adjust offsets in stco/co64 atoms if necessary? - // Changing the size of moov (or atoms before mdat) shifts the mdat offsets. - // If moov comes before mdat, we MUST update the Chunk Offset Atom (stco or co64). - // This is complex. - - // Safe strategy: If moov is AFTER mdat, we don't need to update offsets. - // If moov is BEFORE mdat, we need to shift offsets. - // Most streaming optimized files have moov before mdat. - - const mdatAtom = atoms.find((a) => a.type === 'mdat'); - const moovBeforeMdat = mdatAtom && moovAtom.offset < mdatAtom.offset; - - if (moovBeforeMdat) { - // We need to update stco/co64 atoms inside the copied moov children content in newFile. - // This is getting very complicated for a simple "add metadata" feature without a proper library. - // However, we can try to find 'stco' or 'co64' in the new buffer we just wrote and offset values. - - // Let's assume we need to shift by sizeDiff. - updateChunkOffsets(newFile, offset - newMoovSize, newMoovSize, sizeDiff); - } - - // Copy remaining data (mdat etc.) - if (originalOffset < originalArray.length) { - newFile.set(originalArray.subarray(originalOffset), offset); - } - - return newFile; -} - -function createMetadataBlock(metadataAtoms) { - const { tags, userTags, cover } = metadataAtoms; - - const ilstChildren = []; - - // Text tags - for (const [key, value] of Object.entries(tags)) { - if (key === 'trkn' || key === 'disk') { - ilstChildren.push(createIntAtom(key, value)); - } else if (key === 'rtng') { - ilstChildren.push(createUintAtom(key, value, 1)); - } else if (key === 'tmpo') { - ilstChildren.push(createUintAtom(key, value, 2)); - } else { - ilstChildren.push(createStringAtom(key, value)); - } - } - - // User tags - for (const [namespace, name, value] of userTags) { - ilstChildren.push(createUserAtom(namespace, name, value)); - } - - // Cover art - if (cover) { - ilstChildren.push(createCoverAtom(cover.data)); - } - - // Construct ilst atom - const ilstSize = 8 + ilstChildren.reduce((acc, buf) => acc + buf.length, 0); - const ilst = new Uint8Array(ilstSize); - let offset = 0; - - writeAtomHeader(ilst, offset, ilstSize, 'ilst'); - offset += 8; - - for (const child of ilstChildren) { - ilst.set(child, offset); - offset += child.length; - } - - // Construct meta atom (FullAtom, version+flags = 4 bytes) - const metaSize = 12 + ilstSize; - const meta = new Uint8Array(metaSize); - offset = 0; - - writeAtomHeader(meta, offset, metaSize, 'meta'); - offset += 8; - - meta[offset++] = 0; // Version - meta[offset++] = 0; // Flags - meta[offset++] = 0; - meta[offset++] = 0; - - meta.set(ilst, offset); - - // Construct hdlr atom (required for meta) - // "mdir" subtype, "appl" manufacturer, 0 flags/masks, empty name - // hdlr size: 4 (size) + 4 (type) + 4 (ver/flags) + 4 (pre_defined) + 4 (handler_type) + 12 (reserved) + name (string) - // Minimal valid hdlr for iTunes metadata: - const hdlrContent = new Uint8Array([ - 0, - 0, - 0, - 0, // Version/Flags - 0, - 0, - 0, - 0, // Pre-defined - 0x6d, - 0x64, - 0x69, - 0x72, // 'mdir' - 0x61, - 0x70, - 0x70, - 0x6c, // 'appl' - 0, - 0, - 0, - 0, // Reserved - 0, - 0, - 0, - 0, - 0, - 0, // Name (empty null-term) check spec? usually simple 0 is enough - ]); - const hdlrSize = 8 + hdlrContent.length; - const hdlr = new Uint8Array(hdlrSize); - writeAtomHeader(hdlr, 0, hdlrSize, 'hdlr'); - hdlr.set(hdlrContent, 8); - - // Construct udta atom - // udta contains meta. meta usually should contain hdlr before ilst? - // Actually, QuickTime spec says meta contains hdlr then ilst. - - const finalMetaSize = 12 + hdlrSize + ilstSize; - const finalMeta = new Uint8Array(finalMetaSize); - offset = 0; - writeAtomHeader(finalMeta, offset, finalMetaSize, 'meta'); - offset += 8; - finalMeta[offset++] = 0; // Version - finalMeta[offset++] = 0; // Flags - finalMeta[offset++] = 0; - finalMeta[offset++] = 0; - - finalMeta.set(hdlr, offset); - offset += hdlrSize; - finalMeta.set(ilst, offset); - - const udtaSize = 8 + finalMetaSize; - const udta = new Uint8Array(udtaSize); - writeAtomHeader(udta, 0, udtaSize, 'udta'); - udta.set(finalMeta, 8); - - return udta; -} - -function createStringAtom(type, value, truncateType = true) { - const typeLength = truncateType ? 4 : type.length; - const textBytes = new TextEncoder().encode(value); - const dataSize = 16 + textBytes.length; // 8 (data atom header) + 8 (flags/null) + text - const atomSize = 4 + typeLength + dataSize; - - const buf = new Uint8Array(atomSize); - let offset = 0; - - // Wrapper atom (e.g., ©nam) - writeAtomHeader(buf, offset, atomSize, type, truncateType); - offset += 4 + typeLength; - - // Data atom - writeAtomHeader(buf, offset, dataSize, 'data'); - offset += 8; - - // Data Type (1 = UTF-8 text) + Locale (0) - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 1; // Type 1 - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - - buf.set(textBytes, offset); - - return buf; -} - -function createUserAtom(namespace, name, value) { - const encoder = new TextEncoder(); - const dashBytes = encoder.encode('----'); // User-defined atom type - const namespaceBytes = encoder.encode(namespace); - const meanBytes = encoder.encode('mean'); // Standard 'mean' atom for namespace - const nameBytes = encoder.encode(name); - const valueBytes = encoder.encode('\x00\x00\x00\x01\x00\x00\x00\x00' + value); - - /** - * Atom structure: - * [----] (atom header) - * [mean] (namespace) - * [name] (name) - * [data] (value) - */ - const atomSize = 8 + 12 + namespaceBytes.length + 12 + nameBytes.length + 8 + valueBytes.length; - - const buf = new Uint8Array(atomSize); - let offset = 0; - writeAtomHeader(buf, offset, atomSize, '----'); - offset += 8; // Skip header - writeAtomHeader(buf, offset, namespaceBytes.length + 12, 'mean'); - offset += 12; - buf.set(namespaceBytes, offset); - offset += namespaceBytes.length; - writeAtomHeader(buf, offset, nameBytes.length + 12, 'name'); - offset += 12; - buf.set(nameBytes, offset); - offset += nameBytes.length; - writeAtomHeader(buf, offset, valueBytes.length + 8, 'data'); - offset += 8; - buf.set(valueBytes, offset); - - return buf; -} - -/** - * Converts a number or BigInt value to a big-endian byte array. - * @param {number|BigInt|null} value - The value to convert to bytes. If null, returns null. - * @param {number|null} [byteLength=null] - Optional fixed byte length. If provided, the result will be padded or truncated to this length. If not provided, returns the minimal byte representation. - * @returns {Uint8Array} A Uint8Array representing the value in big-endian format, or null if value is null. - * @throws {Error} If the value is a negative number. - * @example - * // Variable length (minimal bytes) - * toBigEndianBytes(256); // Uint8Array [ 1, 0 ] - * toBigEndianBytes(0); // Uint8Array [ 0 ] - * - * // Fixed length with padding - * toBigEndianBytes(1, 4); // Uint8Array [ 0, 0, 0, 1 ] - * - * // With BigInt - * toBigEndianBytes(0xDEADBEEFn, 4); // Uint8Array [ 222, 173, 190, 239 ] - */ -function toBigEndianBytes(value, byteLength = null) { - if (value == null) return new Uint8Array(0); - - if (!Number.isSafeInteger(value) || value < 0) { - throw new Error('Value must be a non-negative safe integer.'); - } - - // Fixed-length mode - if (byteLength != null) { - const bytes = new Uint8Array(byteLength); - for (let i = byteLength - 1; i >= 0; i--) { - bytes[i] = value & 0xff; - value = Math.floor(value / 256); - } - return bytes; - } - - // Variable (minimal) mode - if (value === 0) return new Uint8Array([0]); - - const result = []; - while (value > 0) { - result.push(value & 0xff); - value = Math.floor(value / 256); - } - - result.reverse(); - - return new Uint8Array(result); -} - -function createUintAtom(key, value, intByteLength = 1) { - const numberBytes = toBigEndianBytes(value, intByteLength); - const dataSize = 16 + intByteLength; // Atom header (8) + number bytes - const atomSize = 8 + dataSize; - - const buf = new Uint8Array(atomSize); - let offset = 0; - - // Wrapper atom (e.g., ©nam) - writeAtomHeader(buf, offset, atomSize, key); - offset += 8; - - // Data atom - writeAtomHeader(buf, offset, dataSize, 'data'); - offset += 8; - - // Data Type ((Big Endian Unsigned Integer) + Locale (0)) - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 21; // Type 21 - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf.set(numberBytes, offset++); - - return buf; -} - -function createIntAtom(type, value) { - // trkn/disk are special: data is 8 bytes. - // reserved(2) + track(2) + total(2) + reserved(2) - const dataSize = 16 + 8; - const atomSize = 8 + dataSize; - - const buf = new Uint8Array(atomSize); - let offset = 0; - - writeAtomHeader(buf, offset, atomSize, type); - offset += 8; - - writeAtomHeader(buf, offset, dataSize, 'data'); - offset += 8; - - // Data Type (0 = implicit/int) + Locale - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; // Type 0 - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - - const current = typeof value === 'object' ? value.current : value; - const total = typeof value === 'object' ? value.total : 0; - - // Numbering payload (track/disc number + total) - buf[offset++] = 0; - buf[offset++] = 0; - const numberValue = parseInt(current, 10) || 0; - buf[offset++] = (numberValue >> 8) & 0xff; - buf[offset++] = numberValue & 0xff; - const totalValue = parseInt(total, 10) || 0; - buf[offset++] = (totalValue >> 8) & 0xff; - buf[offset++] = totalValue & 0xff; - buf[offset++] = 0; - buf[offset++] = 0; - - return buf; -} - -function createCoverAtom(imageBytes) { - const dataSize = 16 + imageBytes.length; - const atomSize = 8 + dataSize; - - const buf = new Uint8Array(atomSize); - let offset = 0; - - writeAtomHeader(buf, offset, atomSize, 'covr'); - offset += 8; - - writeAtomHeader(buf, offset, dataSize, 'data'); - offset += 8; - - // Data Type (13 = JPEG, 14 = PNG) - // We try to detect or default to JPEG (13) - let type = 13; - if (imageBytes[0] === 0x89 && imageBytes[1] === 0x50) { - // PNG signature - type = 14; - } - - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = type; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - buf[offset++] = 0; - - buf.set(imageBytes, offset); - - return buf; -} - -/** - * Creates an atom header for MP4 metadata. - * @param {number} size - The size of the atom in bytes. - * @param {string} type - The 4-character atom type identifier. - * @param {boolean} [truncate=false] - Whether to truncate the type to 4 characters or use full length. - * @returns {Uint8Array} A byte array containing the atom header with size and type information. - */ -function getAtomHeader(size, type, truncate = false) { - const buf = new Uint8Array(4 + (truncate ? 4 : type.length)); - buf[0] = (size >> 24) & 0xff; - buf[1] = (size >> 16) & 0xff; - buf[2] = (size >> 8) & 0xff; - buf[3] = size & 0xff; - - for (let i = 0; i < (truncate ? 4 : type.length); i++) { - buf[4 + i] = type.charCodeAt(i); - } - - return buf; -} - -/** - * Writes an atom header to a buffer at the specified offset. - * @param {Uint8Array} buf - The buffer to write the atom header to. - * @param {number} offset - The offset in the buffer where the atom header should be written. - * @param {number} size - The size of the atom. - * @param {string} type - The type of the atom (typically a 4-character code). - * @param {boolean} [truncate=true] - Whether to truncate the atom header. Defaults to true. - * @returns {void} - */ -function writeAtomHeader(buf, offset, size, type, truncate = true) { - buf.set(getAtomHeader(size, type, truncate), offset); -} - -function updateChunkOffsets(buffer, moovOffset, moovSize, shift) { - const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); - - // Scan moov for stco/co64 - // This is a naive recursive search restricted to the known moov range - - // We parse atoms starting from moov content - let offset = moovOffset + 8; // Skip moov header - const end = moovOffset + moovSize; - - findAndShiftOffsets(view, offset, end, shift); -} - -function findAndShiftOffsets(view, start, end, shift) { - let offset = start; - - while (offset + 8 <= end) { - const size = view.getUint32(offset, false); - const type = String.fromCharCode( - view.getUint8(offset + 4), - view.getUint8(offset + 5), - view.getUint8(offset + 6), - view.getUint8(offset + 7) - ); - - if (size < 8) break; - - if (type === 'trak' || type === 'mdia' || type === 'minf' || type === 'stbl') { - // Container atoms, recurse - findAndShiftOffsets(view, offset + 8, offset + size, shift); - } else if (type === 'stco') { - // Chunk Offset Atom (32-bit) - // Header (8) + Version(1) + Flags(3) + Count(4) + Entries(Count * 4) - const count = view.getUint32(offset + 12, false); - for (let i = 0; i < count; i++) { - const entryOffset = offset + 16 + i * 4; - const oldVal = view.getUint32(entryOffset, false); - view.setUint32(entryOffset, oldVal + shift, false); - } - } else if (type === 'co64') { - // Chunk Offset Atom (64-bit) - // Header (8) + Version(1) + Flags(3) + Count(4) + Entries(Count * 8) - const count = view.getUint32(offset + 12, false); - for (let i = 0; i < count; i++) { - const entryOffset = offset + 16 + i * 8; - // Read 64-bit int - const oldHigh = view.getUint32(entryOffset, false); - const oldLow = view.getUint32(entryOffset + 4, false); - - // Add shift (assuming shift is small enough not to overflow low 32 in a way that affects high simply?) - // Shift is Javascript number (double), up to 9007199254740991. - // 32-bit uint max is 4294967295. - - // Proper 64-bit addition - // Construct BigInt - // Note: BigInt might not be available in all older environments, but modern browsers support it. - // Fallback: simpler logic - - let newLow = oldLow + shift; - let carry = 0; - if (newLow > 0xffffffff) { - carry = Math.floor(newLow / 0x100000000); - newLow = newLow >>> 0; - } - const newHigh = oldHigh + carry; - - view.setUint32(entryOffset, newHigh, false); - view.setUint32(entryOffset + 4, newLow, false); - } - } - - offset += size; - } -} diff --git a/js/taglib.js b/js/taglib.js new file mode 100644 index 0000000..3baf106 --- /dev/null +++ b/js/taglib.js @@ -0,0 +1,29 @@ +import { TagLib as _TagLib } from 'taglib-wasm'; + +/** + * @type {typeof import('taglib-wasm').TagLib} + */ +export const TagLib = _TagLib; +import TagLibWasm from '!/taglib-wasm/dist/taglib-web.wasm?url'; + +export { TagLibWasm }; + +let tagLib = null; +const wasmBinary = fetch(TagLibWasm).then((r) => r.arrayBuffer()); + +/** + * + * @returns {ReturnType} + */ +export async function initTagLib() { + if (tagLib) return await tagLib; + + tagLib = TagLib.initialize({ + wasmBinary: await wasmBinary, + legacyMode: true, + }); + + console.log('TagLib initialized', { tagLib: await tagLib, TagLibWasm }); + + return await tagLib; +} diff --git a/js/utils.js b/js/utils.js index 9bfbda1..33e0bf0 100644 --- a/js/utils.js +++ b/js/utils.js @@ -398,6 +398,9 @@ function resizeImageBlob(blob, size) { /** * Fetches and caches cover art as a Blob + * @param {Object} api - API instance with getCoverUrl method + * @param {string} coverId - ID of the cover art to fetch + * @returns {Promise} - Cover art blob or null if not available */ export async function getCoverBlob(api, coverId) { if (!coverId) return null; diff --git a/package.json b/package.json index e241243..8ad2398 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "homepage": "https://github.com/SamidyFR/monochrome#readme", "devDependencies": { "@neutralinojs/neu": "^11.7.0", + "@types/node": "^25.3.3", "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", "globals": "^17.4.0", @@ -39,6 +40,7 @@ "stylelint": "^16.26.1", "stylelint-config-standard": "^39.0.1", "stylelint-config-standard-scss": "^16.0.0", + "typescript": "^5.9.3", "vite": "^7.3.1", "vite-plugin-neutralino": "^1.0.3", "vite-plugin-pwa": "^1.2.0" @@ -60,6 +62,7 @@ "fuse.js": "^7.1.0", "jose": "^6.1.3", "npm": "^11.11.0", - "pocketbase": "^0.26.8" + "pocketbase": "^0.26.8", + "taglib-wasm": "^0.9.0" } } diff --git a/vite.config.js b/vite.config.js index 29e860f..82a70a1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -10,6 +10,7 @@ export default defineConfig(({ mode }) => { base: './', resolve: { alias: { + '!': '/node_modules', pocketbase: '/node_modules/pocketbase/dist/pocketbase.es.js', }, }, From 44a7c3b61c00e19c5940d6cc80a5f46c0678789f Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:24:18 +0000 Subject: [PATCH 02/15] fix(downloads): cache ffmpeg core js and wasm This creates a blob url outside of the worker for for the core .js and .wasm files so they aren't downloaded on each run. --- bun.lock | 3 +++ js/api.js | 5 ++++- js/downloads.js | 5 ++++- js/ffmpeg.js | 44 +++++++++++++++++++++++++++++++++----------- js/ffmpeg.worker.js | 13 +++++-------- package-lock.json | 10 ++++++++++ package.json | 1 + 7 files changed, 60 insertions(+), 21 deletions(-) diff --git a/bun.lock b/bun.lock index c498d44..bbd5ff1 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "monochrome", "dependencies": { + "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1", @@ -328,6 +329,8 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@ffmpeg/core": ["@ffmpeg/core@0.12.10", "", {}, "sha512-dzNplnn2Nxle2c2i2rrDhqcB19q9cglCkWnoMTDN9Q9l3PvdjZWd1HfSPjCNWc/p8Q3CT+Es9fWOR0UhAeYQZA=="], + "@ffmpeg/ffmpeg": ["@ffmpeg/ffmpeg@0.12.15", "", { "dependencies": { "@ffmpeg/types": "^0.12.4" } }, "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw=="], "@ffmpeg/types": ["@ffmpeg/types@0.12.4", "", {}, "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A=="], diff --git a/js/api.js b/js/api.js index fef8435..1e6cd40 100644 --- a/js/api.js +++ b/js/api.js @@ -11,7 +11,7 @@ import { APICache } from './cache.js'; import { addMetadataToAudio } from './metadata.js'; import { DashDownloader } from './dash-downloader.js'; import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js'; -import { ffmpeg } from './ffmpeg.js'; +import { ffmpeg, loadFfmpeg } from './ffmpeg.js'; import { initTagLib } from './taglib.js'; export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; @@ -1112,6 +1112,9 @@ export class LosslessAPI { async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) { // Initialize taglib in the background. initTagLib().catch(console.error); + + // Load ffmpeg in the background. + loadFfmpeg().catch(console.error); const { onProgress, track } = options; try { diff --git a/js/downloads.js b/js/downloads.js index a67a59b..9a9fffb 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -16,7 +16,7 @@ import { addMetadataToAudio } from './metadata.js'; import { DashDownloader } from './dash-downloader.js'; import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js'; import { encodeToMp3 } from './mp3-encoder.js'; -import { ffmpeg } from './ffmpeg.js'; +import { ffmpeg, loadFfmpeg } from './ffmpeg.js'; import { initTagLib } from './taglib.js'; const downloadTasks = new Map(); @@ -273,6 +273,9 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign // Initialize taglib in the background. initTagLib().catch(console.error); + // Load ffmpeg in the background. + loadFfmpeg().catch(console.error); + let enrichedTrack = { ...track, artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null), diff --git a/js/ffmpeg.js b/js/ffmpeg.js index 2a280bd..9485ed2 100644 --- a/js/ffmpeg.js +++ b/js/ffmpeg.js @@ -1,3 +1,8 @@ +import { toBlobURL } from '@ffmpeg/util'; +const ffmpegBase = 'https://unpkg.com/@ffmpeg/core/dist/esm'; +const coreJs = `${ffmpegBase}/ffmpeg-core.js`; +const coreWasm = `${ffmpegBase}/ffmpeg-core.wasm`; + class FfmpegError extends Error { constructor(message) { super(message); @@ -6,6 +11,20 @@ class FfmpegError extends Error { } } +export function loadFfmpeg() { + return ( + loadFfmpeg.promise || + (loadFfmpeg.promise = (async () => { + const data = { + coreURL: await toBlobURL(coreJs, 'text/javascript'), + wasmURL: await toBlobURL(coreWasm, 'application/wasm'), + }; + + return data; + })()) + ); +} + async function ffmpegWorker( audioBlob, args = {}, @@ -15,6 +34,7 @@ async function ffmpegWorker( signal = null ) { const audioData = await audioBlob.arrayBuffer(); + const assets = loadFfmpeg(); return new Promise((resolve, reject) => { const worker = new Worker(new URL('./ffmpeg.worker.js', import.meta.url), { type: 'module' }); @@ -57,18 +77,20 @@ async function ffmpegWorker( reject(new FfmpegError('Worker failed: ' + error.message)); }; - // Transfer audio data to worker - worker.postMessage( - { - audioData, - ...args, - output: { - name: outputName, - mime: outputMime, + (async () => { + worker.postMessage( + { + audioData, + ...args, + output: { + name: outputName, + mime: outputMime, + }, + loadOptions: await assets, }, - }, - [audioData] - ); + [audioData] + ); + })(); }); } diff --git a/js/ffmpeg.worker.js b/js/ffmpeg.worker.js index 763388a..8b5ba90 100644 --- a/js/ffmpeg.worker.js +++ b/js/ffmpeg.worker.js @@ -1,10 +1,9 @@ import { FFmpeg } from '@ffmpeg/ffmpeg'; -import { toBlobURL } from '@ffmpeg/util'; let ffmpeg = null; let loadingPromise = null; -async function loadFFmpeg() { +async function loadFFmpeg(loadOptions = {}) { if (loadingPromise) return loadingPromise; loadingPromise = (async () => { @@ -25,11 +24,7 @@ async function loadFFmpeg() { self.postMessage({ type: 'progress', stage: 'loading', message: 'Loading FFmpeg...' }); - const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm'; - await ffmpeg.load({ - coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), - wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), - }); + await ffmpeg.load(loadOptions); })(); return loadingPromise; @@ -45,10 +40,12 @@ self.onmessage = async (e) => { }, encodeStartMessage = 'Encoding...', encodeEndMessage = 'Finalizing...', + loadOptions = {}, } = e.data; try { - await loadFFmpeg(); + console.log(loadOptions); + await loadFFmpeg(loadOptions); self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage }); diff --git a/package-lock.json b/package-lock.json index b76bb22..f492dc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1", @@ -2578,6 +2579,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@ffmpeg/core": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@ffmpeg/core/-/core-0.12.10.tgz", + "integrity": "sha512-dzNplnn2Nxle2c2i2rrDhqcB19q9cglCkWnoMTDN9Q9l3PvdjZWd1HfSPjCNWc/p8Q3CT+Es9fWOR0UhAeYQZA==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=16.x" + } + }, "node_modules/@ffmpeg/ffmpeg": { "version": "0.12.15", "resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz", diff --git a/package.json b/package.json index 8ad2398..af789e9 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "source-map": "^0.7.4" }, "dependencies": { + "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1", From 1173388ee3a179489f2240a2dbbfe9dd88de3806 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:53:16 +0000 Subject: [PATCH 03/15] fix(packaging): patch taglib-wasm issues --- bun.lock | 4 ++-- vite.config.js | 32 +++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index bbd5ff1..5ed2841 100644 --- a/bun.lock +++ b/bun.lock @@ -323,7 +323,7 @@ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], - "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="], + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], @@ -723,7 +723,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="], + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], diff --git a/vite.config.js b/vite.config.js index 82a70a1..375098b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,6 +2,7 @@ import { defineConfig } from 'vite'; import { VitePWA } from 'vite-plugin-pwa'; import neutralino from 'vite-plugin-neutralino'; import authGatePlugin from './vite-plugin-auth-gate.js'; +import path from 'path'; export default defineConfig(({ mode }) => { const IS_NEUTRALINO = mode === 'neutralino'; @@ -15,7 +16,8 @@ export default defineConfig(({ mode }) => { }, }, optimizeDeps: { - exclude: ['pocketbase', '@ffmpeg/ffmpeg', '@ffmpeg/util'], + exclude: ['pocketbase', '@ffmpeg/ffmpeg', '@ffmpeg/util', 'taglib-wasm'], + external: ['taglib-wasm'], }, server: { fs: { @@ -72,6 +74,34 @@ export default defineConfig(({ mode }) => { includeAssets: ['discord.html'], manifest: false, // Use existing public/manifest.json }), + { + name: 'ignore-taglib', + resolveId(id) { + if ( + id == './dist/taglib-wrapper.js' || + id == '../../build/taglib-wrapper.js' || + id == '../../dist/taglib-wrapper.js' + ) { + return path.resolve('node_modules/taglib-wasm/dist/taglib-wrapper.js'); + } + + return id; + }, + load(id) { + if (id.endsWith('taglib-wasm/dist/src/worker-pool.js')) { + return 'export const getGlobalWorkerPool = () => { throw new Error("Worker pool is not supported in this environment"); }; export class TagLibWorkerPool { constructor() { throw new Error("Worker pool is not supported in this environment"); } } export function createWorkerPool() { throw new Error("Worker pool is not supported in this environment"); } export function terminateGlobalWorkerPool() { throw new Error("Worker pool is not supported in this environment"); }'; + } + + if (id.endsWith('taglib-wasm/dist/src/runtime/wasmer-sdk-loader.js')) { + return [ + 'export const initializeWasmer = null;', + 'export const loadWasmerWasi = null', + 'export const isWasmerAvailable = null;', + 'export const WasmerExecutionError = null;', + ].join('\n'); + } + }, + }, ], }; }); From 0f20106076121cb528b3c5840d58b94600cc5f20 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:43:13 +0000 Subject: [PATCH 04/15] feat(downloads): prefetch data while downloading to improve performance and update taglib-wasm --- bun.lock | 10 +++--- js/api.js | 10 +++--- js/downloads.js | 10 +++--- js/global.d.ts | 4 +++ js/metadata.js | 83 ++++++++++++++++++++++++++++++++++++------------- js/taglib.js | 29 ----------------- js/taglib.ts | 19 +++++++++++ js/utils.js | 8 +++++ package.json | 8 ++--- tsconfig.json | 21 +++++++++++++ vite.config.js | 28 ----------------- 11 files changed, 131 insertions(+), 99 deletions(-) create mode 100644 js/global.d.ts delete mode 100644 js/taglib.js create mode 100644 js/taglib.ts create mode 100644 tsconfig.json diff --git a/bun.lock b/bun.lock index 5ed2841..fdf2fb4 100644 --- a/bun.lock +++ b/bun.lock @@ -16,18 +16,18 @@ "cookie-session": "^2.1.1", "dashjs": "^5.1.1", "fuse.js": "^7.1.0", - "jose": "^6.1.3", + "jose": "^6.2.0", "npm": "^11.11.0", "pocketbase": "^0.26.8", - "taglib-wasm": "^0.9.0", + "taglib-wasm": "^1.0.5", }, "devDependencies": { "@neutralinojs/neu": "^11.7.0", - "@types/node": "^25.3.3", + "@types/node": "^25.3.5", "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", "globals": "^17.4.0", - "htmlhint": "^1.9.1", + "htmlhint": "^1.9.2", "miniflare": "^4.20260301.1", "prettier": "^3.8.1", "stylelint": "^16.26.1", @@ -1279,7 +1279,7 @@ "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], - "taglib-wasm": ["taglib-wasm@0.9.0", "", { "dependencies": { "@msgpack/msgpack": "^3.1.3" }, "peerDependencies": { "typescript": ">=4.5.0" } }, "sha512-E6Z/rGT6vE+9HuRnklSJNvEBdq+VyVVrXvMJ3o7/4oY3tsBwLYp949SgmkTUSegTgDzBjguTN74XVeEKtPVSOA=="], + "taglib-wasm": ["taglib-wasm@1.0.5", "", { "dependencies": { "@msgpack/msgpack": "^3.1.3" }, "peerDependencies": { "typescript": ">=4.5.0" }, "optionalPeers": ["typescript"] }, "sha512-kuDHX78FbjLOqldWxBBkEgjyyDagYRGcYqr4g6ObkmEMO203Sp1R0KxkELoH9VkZxsOVR8yoSWaSaG9f5oTqyQ=="], "tcp-port-used": ["tcp-port-used@1.0.2", "", { "dependencies": { "debug": "4.3.1", "is2": "^2.0.6" } }, "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA=="], diff --git a/js/api.js b/js/api.js index 1e6cd40..4bcf764 100644 --- a/js/api.js +++ b/js/api.js @@ -8,11 +8,10 @@ import { } from './utils.js'; import { trackDateSettings, losslessContainerSettings } from './storage.js'; import { APICache } from './cache.js'; -import { addMetadataToAudio } from './metadata.js'; +import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js'; import { DashDownloader } from './dash-downloader.js'; import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js'; import { ffmpeg, loadFfmpeg } from './ffmpeg.js'; -import { initTagLib } from './taglib.js'; export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25'; @@ -1110,12 +1109,11 @@ export class LosslessAPI { } async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) { - // Initialize taglib in the background. - initTagLib().catch(console.error); - // Load ffmpeg in the background. loadFfmpeg().catch(console.error); + const { onProgress, track } = options; + const prefetchPromises = prefetchMetadataObjects(track, this); try { // MP3_320 is not a native TIDAL quality, we download LOSSLESS and convert @@ -1271,7 +1269,7 @@ export class LosslessAPI { }; } - blob = await addMetadataToAudio(blob, enrichedTrack, this, quality); + blob = await addMetadataToAudio(blob, enrichedTrack, this, quality, prefetchPromises); } // Detect actual format and fix filename extension if needed diff --git a/js/downloads.js b/js/downloads.js index 9a9fffb..011aa39 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -12,12 +12,11 @@ import { escapeHtml, } from './utils.js'; import { lyricsSettings, bulkDownloadSettings, losslessContainerSettings, playlistSettings } from './storage.js'; -import { addMetadataToAudio } from './metadata.js'; +import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js'; import { DashDownloader } from './dash-downloader.js'; import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js'; import { encodeToMp3 } from './mp3-encoder.js'; import { ffmpeg, loadFfmpeg } from './ffmpeg.js'; -import { initTagLib } from './taglib.js'; const downloadTasks = new Map(); const bulkDownloadTasks = new Map(); @@ -270,12 +269,11 @@ function removeBulkDownloadTask(notifEl) { } async function downloadTrackBlob(track, quality, api, lyricsManager = null, signal = null) { - // Initialize taglib in the background. - initTagLib().catch(console.error); - // Load ffmpeg in the background. loadFfmpeg().catch(console.error); + const prefetchPromises = prefetchMetadataObjects(track, api); + let enrichedTrack = { ...track, artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null), @@ -408,7 +406,7 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign const extension = await getExtensionFromBlob(blob); // Add metadata to the blob - blob = await addMetadataToAudio(blob, enrichedTrack, api, quality); + blob = await addMetadataToAudio(blob, enrichedTrack, api, quality, prefetchPromises); return { blob, extension }; } diff --git a/js/global.d.ts b/js/global.d.ts new file mode 100644 index 0000000..ed623f9 --- /dev/null +++ b/js/global.d.ts @@ -0,0 +1,4 @@ +declare module '*?url' { + const content: string; + export default content; +} diff --git a/js/metadata.js b/js/metadata.js index 7939788..42200a9 100644 --- a/js/metadata.js +++ b/js/metadata.js @@ -42,6 +42,16 @@ function getFullArtistString(track) { return knownArtists.join('; ') || null; } +export function prefetchMetadataObjects(track, api) { + const _tagLib = initTagLib().catch(console.error); + const coverFetch = track?.album?.cover + ? getCoverBlob(api, track.album.cover).catch(console.error) + : Promise.resolve(null); + const lyricsFetch = managers?.lyricsManager?.fetchLyrics?.(track.id, track)?.catch(console.error); + + return { _tagLib, coverFetch, lyricsFetch }; +} + /** * Adds metadata tags to audio files (FLAC, M4A or MP3) * @param {Blob} audioBlob - The audio file blob @@ -50,32 +60,42 @@ function getFullArtistString(track) { * @param {string} quality - Audio quality * @returns {Promise} - Audio blob with embedded metadata */ -export async function addMetadataToAudio(audioBlob, track, api, _quality) { - const tagLib = await initTagLib(); - const file = await tagLib.open(await audioBlob.arrayBuffer()); +export async function addMetadataToAudio(audioBlob, track, api, _quality, prefetchPromises) { + const { _tagLib, coverFetch, lyricsFetch } = prefetchPromises; + console.time('Get audio array buffer'); + const audioBuffer = await audioBlob.arrayBuffer(); + console.timeEnd('Get audio array buffer'); + + console.time('Open file with taglib'); + const tagLib = await _tagLib; + const file = await tagLib.open(audioBuffer); + console.timeEnd('Open file with taglib'); + + console.time('Tagging file'); try { const isMp4 = file.isMP4(); - const discNumber = track.volumeNumber ?? track.discNumber; - const lyricsFetch = managers?.lyricsManager?.fetchLyrics?.(track.id, track); - const coverFetch = getCoverBlob(api, track.album.cover); // Add standard tags if (track.title) { file.setProperty('TITLE', getTrackTitle(track)); } + const artistStr = getFullArtistString(track); if (artistStr) { file.setProperty('ARTIST', artistStr); } + if (track.album?.title) { file.setProperty('ALBUM', track.album.title); } + const albumArtist = track.album?.artist?.name || track.artist?.name; if (albumArtist) { file.setProperty('ALBUMARTIST', albumArtist); } + if (track.trackNumber) { let trackString = String(track.trackNumber); @@ -89,6 +109,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) { file.setProperty('TRACKNUMBER', String(track.trackNumber)); } } + if (!isMp4 && track.album?.numberOfTracks) { file.setProperty('TRACKTOTAL', String(track.album.numberOfTracks)); } @@ -103,6 +124,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) { file.setProperty('BPM', String(Math.round(bpm))); } } + if (track.replayGain) { const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = track.replayGain; if (albumReplayGain) file.setProperty('REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain)); @@ -113,6 +135,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) { const releaseDateStr = track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : ''); + if (releaseDateStr) { try { const year = new Date(releaseDateStr).getFullYear(); @@ -127,6 +150,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) { if (track.copyright) { file.setProperty('COPYRIGHT', track.copyright); } + if (track.isrc) { file.setProperty('ISRC', track.isrc); @@ -134,6 +158,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) { file.setMP4Item('xid ', `:isrc:${track.isrc}`); } } + if (track.explicit) { if (isMp4) { file.setMP4Item('rtng', '1'); @@ -142,20 +167,24 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) { } } - if (track.album?.cover) { - const coverBlob = await coverFetch; - const coverBuffer = new Uint8Array(await coverBlob.arrayBuffer()); + try { + if (track.album?.cover) { + const coverBlob = await coverFetch; + const coverBuffer = new Uint8Array(await coverBlob.arrayBuffer()); - if (coverBlob) { - file.setPictures([ - { - mimeType: coverBlob.type, - data: coverBuffer, - type: PICTURE_TYPE_VALUES.FrontCover, - description: 'Cover Art', - }, - ]); + if (coverBlob) { + file.setPictures([ + { + mimeType: coverBlob.type, + data: coverBuffer, + type: PICTURE_TYPE_VALUES.FrontCover, + description: 'Cover Art', + }, + ]); + } } + } catch (e) { + console.warn('Error setting cover metadata.', track, e); } try { @@ -170,16 +199,28 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality) { //} } } catch (e) { - console.warn('Error fetching lyrics', track, e); + console.warn('Error setting lyrics metadata', track, e); } - await file.save(); + console.timeEnd('Tagging file'); - return new Blob([file.getFileBuffer()], { type: audioBlob.type, name: audioBlob.name }); + console.time('Saving in-memory buffer'); + await file.save(); + console.timeEnd('Saving in-memory buffer'); + + console.time('Saving blob'); + const blob = new Blob([file.getFileBuffer()], { type: audioBlob.type, name: audioBlob.name }); + console.timeEnd('Saving blob'); + + return blob; + } catch (err) { + console.error(err); } finally { // Always dispose, even if there was an error. file.dispose(); } + + return audioBlob; } /** diff --git a/js/taglib.js b/js/taglib.js deleted file mode 100644 index 3baf106..0000000 --- a/js/taglib.js +++ /dev/null @@ -1,29 +0,0 @@ -import { TagLib as _TagLib } from 'taglib-wasm'; - -/** - * @type {typeof import('taglib-wasm').TagLib} - */ -export const TagLib = _TagLib; -import TagLibWasm from '!/taglib-wasm/dist/taglib-web.wasm?url'; - -export { TagLibWasm }; - -let tagLib = null; -const wasmBinary = fetch(TagLibWasm).then((r) => r.arrayBuffer()); - -/** - * - * @returns {ReturnType} - */ -export async function initTagLib() { - if (tagLib) return await tagLib; - - tagLib = TagLib.initialize({ - wasmBinary: await wasmBinary, - legacyMode: true, - }); - - console.log('TagLib initialized', { tagLib: await tagLib, TagLibWasm }); - - return await tagLib; -} diff --git a/js/taglib.ts b/js/taglib.ts new file mode 100644 index 0000000..fea89f8 --- /dev/null +++ b/js/taglib.ts @@ -0,0 +1,19 @@ +import { TagLib } from 'taglib-wasm'; +import { fetchBlobURL } from './utils'; +import _TagLibWasm from '!/taglib-wasm/dist/taglib-web.wasm?url'; + +let tagLib: Promise | null = null; + +export async function initTagLib(): Promise { + if (tagLib) return await tagLib; + + const TagLibWasm = await fetchBlobURL(_TagLibWasm); + + tagLib = TagLib.initialize({ + wasmUrl: TagLibWasm, + }); + + console.log('TagLib initialized', { tagLib: await tagLib, TagLibWasm }); + + return await tagLib; +} diff --git a/js/utils.js b/js/utils.js index 33e0bf0..c7281ac 100644 --- a/js/utils.js +++ b/js/utils.js @@ -533,3 +533,11 @@ export const getShareUrl = (path) => { const safePath = path.startsWith('/') ? path : `/${path}`; return `${baseUrl}${safePath}`; }; + +export function fetchBlob(url) { + return fetch(url).then((d) => d.blob()); +} + +export async function fetchBlobURL(url) { + return await URL.createObjectURL(await fetchBlob(url)); +} diff --git a/package.json b/package.json index af789e9..9fe206f 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,11 @@ "homepage": "https://github.com/SamidyFR/monochrome#readme", "devDependencies": { "@neutralinojs/neu": "^11.7.0", - "@types/node": "^25.3.3", + "@types/node": "^25.3.5", "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", "globals": "^17.4.0", - "htmlhint": "^1.9.1", + "htmlhint": "^1.9.2", "miniflare": "^4.20260301.1", "prettier": "^3.8.1", "stylelint": "^16.26.1", @@ -61,9 +61,9 @@ "cookie-session": "^2.1.1", "dashjs": "^5.1.1", "fuse.js": "^7.1.0", - "jose": "^6.1.3", + "jose": "^6.2.0", "npm": "^11.11.0", "pocketbase": "^0.26.8", - "taglib-wasm": "^0.9.0" + "taglib-wasm": "^1.0.5" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..71dc5e1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client", "node"], + "baseUrl": ".", + "paths": { + "!/*": ["node_modules/*"] + }, + "allowJs": true, + "checkJs": false, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["js/**/*.ts", "js/**/*.d.ts"] +} diff --git a/vite.config.js b/vite.config.js index 375098b..3f29d56 100644 --- a/vite.config.js +++ b/vite.config.js @@ -74,34 +74,6 @@ export default defineConfig(({ mode }) => { includeAssets: ['discord.html'], manifest: false, // Use existing public/manifest.json }), - { - name: 'ignore-taglib', - resolveId(id) { - if ( - id == './dist/taglib-wrapper.js' || - id == '../../build/taglib-wrapper.js' || - id == '../../dist/taglib-wrapper.js' - ) { - return path.resolve('node_modules/taglib-wasm/dist/taglib-wrapper.js'); - } - - return id; - }, - load(id) { - if (id.endsWith('taglib-wasm/dist/src/worker-pool.js')) { - return 'export const getGlobalWorkerPool = () => { throw new Error("Worker pool is not supported in this environment"); }; export class TagLibWorkerPool { constructor() { throw new Error("Worker pool is not supported in this environment"); } } export function createWorkerPool() { throw new Error("Worker pool is not supported in this environment"); } export function terminateGlobalWorkerPool() { throw new Error("Worker pool is not supported in this environment"); }'; - } - - if (id.endsWith('taglib-wasm/dist/src/runtime/wasmer-sdk-loader.js')) { - return [ - 'export const initializeWasmer = null;', - 'export const loadWasmerWasi = null', - 'export const isWasmerAvailable = null;', - 'export const WasmerExecutionError = null;', - ].join('\n'); - } - }, - }, ], }; }); From ff1efe093e2000fe2817cc19d019403f8bfed474 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:13:47 +0000 Subject: [PATCH 05/15] feat(ffmpeg, taglib, utils): replace toBlobURL with fetchBlobURL for improved loading --- js/ffmpeg.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/ffmpeg.js b/js/ffmpeg.js index 9485ed2..f9fbd62 100644 --- a/js/ffmpeg.js +++ b/js/ffmpeg.js @@ -1,4 +1,4 @@ -import { toBlobURL } from '@ffmpeg/util'; +import { fetchBlobURL } from './utils'; const ffmpegBase = 'https://unpkg.com/@ffmpeg/core/dist/esm'; const coreJs = `${ffmpegBase}/ffmpeg-core.js`; const coreWasm = `${ffmpegBase}/ffmpeg-core.wasm`; @@ -16,8 +16,8 @@ export function loadFfmpeg() { loadFfmpeg.promise || (loadFfmpeg.promise = (async () => { const data = { - coreURL: await toBlobURL(coreJs, 'text/javascript'), - wasmURL: await toBlobURL(coreWasm, 'application/wasm'), + coreURL: await fetchBlobURL(coreJs), + wasmURL: await fetchBlobURL(coreWasm), }; return data; From 497d42b9fd737de69fb0e3e9361d94b4ea5c0f34 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Fri, 6 Mar 2026 23:44:33 +0000 Subject: [PATCH 06/15] feat(ffmpeg): enhance progress tracking and logging - Improved progress tracking in FFmpeg worker by extracting total duration and current time from logs. - Updated downloadTrackBlob function to use console logging for progress updates. - Enhanced error handling and progress reporting during audio encoding. --- js/api.js | 19 +++++++++-- js/downloads.js | 6 ++-- js/ffmpeg.worker.js | 81 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 14 deletions(-) diff --git a/js/api.js b/js/api.js index 4bcf764..0ea7b1a 100644 --- a/js/api.js +++ b/js/api.js @@ -1201,7 +1201,14 @@ export class LosslessAPI { // Convert to MP3 320kbps if requested if (quality === 'MP3_320') { try { - blob = await encodeToMp3(blob, onProgress, options.signal); + blob = await encodeToMp3( + blob, + (progress) => { + console.log(progress); + onProgress?.(progress); + }, + options.signal + ); } catch (encodingError) { if (onProgress) { onProgress({ @@ -1223,7 +1230,10 @@ export class LosslessAPI { { args: ['-c:a', 'copy'] }, 'output.flac', 'audio/flac', - onProgress, + (progress) => { + console.log(progress); + onProgress?.(progress); + }, options.signal ); } @@ -1234,7 +1244,10 @@ export class LosslessAPI { { args: ['-c:a', 'alac'] }, 'output.m4a', 'audio/mp4', - onProgress, + (progress) => { + console.log(progress); + onProgress?.(progress); + }, options.signal ); break; diff --git a/js/downloads.js b/js/downloads.js index 011aa39..3f34e2c 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -362,7 +362,7 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign // Convert to MP3 320kbps if requested if (quality === 'MP3_320') { - blob = await encodeToMp3(blob, () => undefined, signal); + blob = await encodeToMp3(blob, console.log, signal); } if (quality.endsWith('LOSSLESS')) { @@ -375,7 +375,7 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign { args: ['-c:a', 'copy'] }, 'output.flac', 'audio/flac', - () => undefined, + console.log, signal ); } @@ -386,7 +386,7 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign { args: ['-c:a', 'alac'] }, 'output.m4a', 'audio/mp4', - () => undefined, + console.log, signal ); break; diff --git a/js/ffmpeg.worker.js b/js/ffmpeg.worker.js index 8b5ba90..969a126 100644 --- a/js/ffmpeg.worker.js +++ b/js/ffmpeg.worker.js @@ -3,6 +3,36 @@ import { FFmpeg } from '@ffmpeg/ffmpeg'; let ffmpeg = null; let loadingPromise = null; +// For granular progress +let totalDurationSeconds = null; +let lastProgress = 0; + +function parseTimestamp(str) { + // Expects format: 00:03:19.26 + const match = str.match(/(\d+):(\d+):(\d+\.?\d*)/); + if (!match) return null; + const [, h, m, s] = match; + return parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s); +} + +function extractDurationFromLog(log) { + // Looks for 'Duration: 00:03:19.26' + const match = log.match(/Duration: (\d+:\d+:\d+\.?\d*)/); + if (match) { + return parseTimestamp(match[1]); + } + return null; +} + +function extractTimeFromLog(log) { + // Looks for 'time=00:01:05.53' + const match = log.match(/time=(\d+:\d+:\d+\.?\d*)/); + if (match) { + return parseTimestamp(match[1]); + } + return null; +} + async function loadFFmpeg(loadOptions = {}) { if (loadingPromise) return loadingPromise; @@ -11,20 +41,55 @@ async function loadFFmpeg(loadOptions = {}) { ffmpeg.on('log', ({ message }) => { self.postMessage({ type: 'log', message }); + + // Try to extract total duration from input log + if (totalDurationSeconds === null) { + const dur = extractDurationFromLog(message); + if (dur) { + totalDurationSeconds = dur; + self.postMessage({ type: 'progress', stage: 'parsing', message: `Detected duration: ${dur}s` }); + } + } + + // Try to extract current time from progress log + if (totalDurationSeconds) { + const cur = extractTimeFromLog(message); + if (cur !== null) { + let progress = Math.min(100, (cur / totalDurationSeconds) * 100); + // Only send if progress increased by at least 0.1% + if (progress - lastProgress >= 0.1 || progress === 100) { + lastProgress = progress; + self.postMessage({ + type: 'progress', + stage: 'encoding', + progress, + time: cur, + message: `Encoding: ${progress.toFixed(1)}% (${cur.toFixed(2)}s / ${totalDurationSeconds.toFixed(2)}s)`, + }); + } + } + } }); + // Optionally keep the original progress event for fallback ffmpeg.on('progress', ({ progress, time }) => { - self.postMessage({ - type: 'progress', - stage: 'encoding', - progress: progress * 100, - time, - }); + // Only send if we don't have granular progress + if (!totalDurationSeconds) { + self.postMessage({ + type: 'progress', + stage: 'encoding', + progress: progress * 100, + time, + }); + } }); self.postMessage({ type: 'progress', stage: 'loading', message: 'Loading FFmpeg...' }); await ffmpeg.load(loadOptions); + // Reset progress state for each run + totalDurationSeconds = null; + lastProgress = 0; })(); return loadingPromise; @@ -47,7 +112,7 @@ self.onmessage = async (e) => { console.log(loadOptions); await loadFFmpeg(loadOptions); - self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage }); + self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage, progress: 0.0 }); try { // Write input file to FFmpeg virtual filesystem @@ -61,7 +126,7 @@ self.onmessage = async (e) => { // Run FFMPEG with the provided arguments. await ffmpeg.exec(ffmpegArgs); - self.postMessage({ type: 'progress', stage: 'finalizing', message: encodeEndMessage }); + self.postMessage({ type: 'progress', stage: 'finalizing', message: encodeEndMessage, progress: 100.0 }); // Read output file - use Uint8Array directly to avoid extra bytes from ArrayBuffer const data = await ffmpeg.readFile(output.name); From efa3521aff2ae47ac6e195bdca384d90b9143a0f Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:09:44 +0000 Subject: [PATCH 07/15] feat(taglib): refactor and improve metadata handling, worker integration, and code quality - Refactor metadata handling to use fetchTagLib and addMetadataWithTagLib for improved loading and worker-based processing - Update prefetchMetadataObjects and addMetadataToAudio for simplified and more robust metadata extraction - Add taglib.worker.ts for audio metadata processing in a worker - Implement getMetadataWithTagLib function - Auto-fix linting issues and remove unnecessary debugger statements --- bun.lock | 5 +- js/BaseCodec.ts | 195 ++++++++++++++++++++++++++ js/doTimed.ts | 26 ++++ js/metadata.js | 188 +++++++++---------------- js/taglib.ts | 73 ++++++++-- js/taglib.worker.ts | 334 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- tsconfig.json | 2 +- 8 files changed, 692 insertions(+), 134 deletions(-) create mode 100644 js/BaseCodec.ts create mode 100644 js/doTimed.ts create mode 100644 js/taglib.worker.ts diff --git a/bun.lock b/bun.lock index fdf2fb4..9b8e910 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "npm": "^11.11.0", "pocketbase": "^0.26.8", "taglib-wasm": "^1.0.5", + "uuid": "^13.0.0", }, "devDependencies": { "@neutralinojs/neu": "^11.7.0", @@ -1345,7 +1346,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], @@ -1465,6 +1466,8 @@ "@keyv/bigmap/keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], + "@neutralinojs/neu/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "@rollup/plugin-babel/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="], diff --git a/js/BaseCodec.ts b/js/BaseCodec.ts new file mode 100644 index 0000000..3ea92fe --- /dev/null +++ b/js/BaseCodec.ts @@ -0,0 +1,195 @@ +class BaseCodec { + private readonly dictionary: string[]; + private readonly base: number; + private readonly dictionarySet: Set; + + constructor(dictionary: string) { + if (new Set(dictionary).size !== dictionary.length) { + throw new Error('Dictionary must not contain duplicate characters.'); + } + + if (dictionary.length < 2) { + throw new Error('Dictionary must contain at least 2 symbols.'); + } + + this.dictionary = [...dictionary]; + this.dictionarySet = new Set(dictionary); + this.base = dictionary.length; + } + + /** + * Encode overloads: + * - number → sync encoding (base-N) + * - string | Uint8Array | Blob → byte-level encoding + */ + encode(input: number): string; + encode(input: string | Uint8Array): string; + encode(input: number | string | Uint8Array): string { + if (typeof input === 'number') { + return this.encodeNumber(input); + } + return this.encodeBytes(input); + } + + /** + * Converts a number to a base-N string using the provided dictionary. + * Rounds the number first; prefixes '-' if negative. + */ + encodeNumber(num: number): string { + if (!Number.isFinite(num)) { + throw new Error('Input must be a finite number.'); + } + + const negative = num < 0; + num = Math.round(Math.abs(num)); + + if (num === 0) { + return this.dictionary[0]; + } + + let encoded = ''; + while (num > 0) { + encoded = this.dictionary[num % this.base] + encoded; + num = Math.floor(num / this.base); + } + + return negative ? '-' + encoded : encoded; + } + + /** + * Asynchronously encodes binary input (string, bytes, or Blob) using base-N byte-level logic. + */ + encodeBytes(input: string | Uint8Array | ArrayBuffer): string { + let bytes: Uint8Array; + + if (typeof input === 'string') { + bytes = new TextEncoder().encode(input); + } else if (input instanceof Uint8Array) { + bytes = input; + } else if (input instanceof ArrayBuffer) { + bytes = new Uint8Array(input); + } else if (Array.isArray(input)) { + bytes = new Uint8Array(input); + } else { + throw new Error('Unsupported input type for encode'); + } + + // Count leading zeros + let zeroCount = 0; + while (zeroCount < bytes.length && bytes[zeroCount] === 0) zeroCount++; + + const digits: string[] = []; + let inputArray = Array.from(bytes); + + while (inputArray.length > 0 && !(inputArray.length === 1 && inputArray[0] === 0)) { + const newInput: number[] = []; + let remainder = 0; + + for (const byte of inputArray) { + const acc = (remainder << 8) + byte; + const digit = Math.floor(acc / this.base); + remainder = acc % this.base; + if (newInput.length > 0 || digit !== 0) newInput.push(digit); + } + + digits.push(this.dictionary[remainder]); + inputArray = newInput; + } + + for (let i = 0; i < zeroCount; i++) digits.push(this.dictionary[0]); + + return digits.reverse().join(''); + } + + /** + * Decodes a base-N string back to a number. Handles optional '-' prefix. + */ + decodeNumber(str: string): number { + if (typeof str !== 'string' || str.length === 0) { + throw new Error('Input must be a non-empty string.'); + } + + const negative = str[0] === '-'; + if (negative) str = str.slice(1); + + let num = 0; + + if (new Set(str).isSubsetOf(this.dictionarySet) === false) { + throw new Error('Input contains invalid characters.'); + } + + for (let i = 0; i < str.length; i++) { + const val = this.dictionary.indexOf(str[i]); + num = num * this.base + val; + } + + return negative ? -num : num; + } + + /** + * Decodes a string or binary representation back to a Uint8Array. + */ + decodeBytes(input: string): Uint8Array { + if (input.length === 0) return new Uint8Array(); + + let zeroCount = 0; + while (zeroCount < input.length && input[zeroCount] === this.dictionary[0]) zeroCount++; + + const charToValue: Record = Object.fromEntries(this.dictionary.map((c, i) => [c, i])); + + const bytes: number[] = []; + let inputArray = Array.from(input, (c) => { + const v = charToValue[c]; + if (v === undefined) throw new Error(`Invalid character: ${c}`); + return v; + }); + + while (inputArray.length > 0 && !(inputArray.length === 1 && inputArray[0] === 0)) { + const newInput: number[] = []; + let remainder = 0; + + for (const digit of inputArray) { + const acc = remainder * this.base + digit; + const byte = Math.floor(acc / 256); + remainder = acc % 256; + if (newInput.length > 0 || byte !== 0) newInput.push(byte); + } + + bytes.push(remainder); + inputArray = newInput; + } + + for (let i = 0; i < zeroCount; i++) bytes.push(0); + + return new Uint8Array(bytes.reverse()); + } +} + +const dictionaries: Record = {}; + +export const Base64Dictionary = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +export const InvisibleDictionary = '\u200B\u200C\u200D\uFEFF'; + +export function baseCodecFrom(dictionary: string): BaseCodec { + return dictionaries[dictionary] || (dictionaries[dictionary] = new BaseCodec(dictionary)); +} + +namespace BaseCodec { + /** + * Converts a number to a Base64 string. + * Rounds the number first; prefixes '-' if negative. + * @param {number} num - The number to convert. + * @returns {string} The Base64-encoded representation. + */ + export const encode = (num: number) => baseCodecFrom(Base64Dictionary).encodeNumber(num); + + /** + * Decodes a Base64 string back to a number. + * Handles optional '-' prefix. + * @param {string} str - The Base64-encoded string. + * @returns {number} The decoded number. + */ + export const decode = (str: string) => baseCodecFrom(Base64Dictionary).decodeNumber(str); +} + +export default BaseCodec; diff --git a/js/doTimed.ts b/js/doTimed.ts new file mode 100644 index 0000000..812ddb9 --- /dev/null +++ b/js/doTimed.ts @@ -0,0 +1,26 @@ +import { InvisibleDictionary, baseCodecFrom } from './BaseCodec'; +import { v7 } from 'uuid'; + +export const InvisibleCodec = baseCodecFrom(InvisibleDictionary); + +export function doTimed(message: string, callback: () => T): T { + const hiddenId = InvisibleCodec.encode(v7()); + console.time(message + hiddenId); + try { + const output = callback(); + return output; + } finally { + console.timeEnd(message + hiddenId); + } +} + +export async function doTimedAsync(message: string, callback: () => T): Promise> { + const hiddenId = InvisibleCodec.encode(v7()); + console.time(message + hiddenId); + try { + const output = await callback(); + return output; + } finally { + console.timeEnd(message + hiddenId); + } +} diff --git a/js/metadata.js b/js/metadata.js index 42200a9..e09320a 100644 --- a/js/metadata.js +++ b/js/metadata.js @@ -1,6 +1,6 @@ import { getCoverBlob, getTrackTitle } from './utils.js'; -import { initTagLib } from './taglib.js'; -import { PICTURE_TYPE_VALUES } from 'taglib-wasm'; +import { fetchTagLib, addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts'; +import { doTimed, doTimedAsync } from './doTimed.ts'; import { managers } from './app.js'; const VENDOR_STRING = 'Monochrome'; @@ -43,7 +43,7 @@ function getFullArtistString(track) { } export function prefetchMetadataObjects(track, api) { - const _tagLib = initTagLib().catch(console.error); + const _tagLib = fetchTagLib().catch(console.error); const coverFetch = track?.album?.cover ? getCoverBlob(api, track.album.cover).catch(console.error) : Promise.resolve(null); @@ -61,109 +61,56 @@ export function prefetchMetadataObjects(track, api) { * @returns {Promise} - Audio blob with embedded metadata */ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefetchPromises) { - const { _tagLib, coverFetch, lyricsFetch } = prefetchPromises; + const { coverFetch, lyricsFetch } = prefetchPromises; - console.time('Get audio array buffer'); - const audioBuffer = await audioBlob.arrayBuffer(); - console.timeEnd('Get audio array buffer'); + /** + * @type {import("./taglib.worker.ts").TagLibMetadata} + */ + const data = {}; - console.time('Open file with taglib'); - const tagLib = await _tagLib; - const file = await tagLib.open(audioBuffer); - console.timeEnd('Open file with taglib'); + const audioBuffer = await doTimedAsync('Get audio array buffer', () => audioBlob.arrayBuffer()); - console.time('Tagging file'); try { - const isMp4 = file.isMP4(); - const discNumber = track.volumeNumber ?? track.discNumber; - - // Add standard tags - if (track.title) { - file.setProperty('TITLE', getTrackTitle(track)); - } - - const artistStr = getFullArtistString(track); - if (artistStr) { - file.setProperty('ARTIST', artistStr); - } - - if (track.album?.title) { - file.setProperty('ALBUM', track.album.title); - } - - const albumArtist = track.album?.artist?.name || track.artist?.name; - if (albumArtist) { - file.setProperty('ALBUMARTIST', albumArtist); - } - - if (track.trackNumber) { - let trackString = String(track.trackNumber); - - if (isMp4 && track.trackNumber && track.album?.numberOfTracks) { - trackString = `${track.trackNumber}/${track.album.numberOfTracks}`; - } - - if (isMp4) { - file.setProperty('TRACKNUMBER', trackString); - } else { - file.setProperty('TRACKNUMBER', String(track.trackNumber)); - } - } - - if (!isMp4 && track.album?.numberOfTracks) { - file.setProperty('TRACKTOTAL', String(track.album.numberOfTracks)); - } - - if (discNumber) { - file.setProperty('DISCNUMBER', String(discNumber)); - } + data.title = getTrackTitle(track); + data.artist = getFullArtistString(track); + data.albumTitle = track.album.title; + data.albumArtist = track.album?.artist?.name || track.artist?.name; + data.trackNumber = track.trackNumber; + data.discNumber = track.volumeNumber ?? track.discNumber; + data.totalTracks = track.album.numberOfTracks; + data.copyright = track.copyright; + data.isrc = track.isrc; + data.explicit = Boolean(track.explicit); if (track.bpm != null) { const bpm = Number(track.bpm); if (Number.isFinite(bpm)) { - file.setProperty('BPM', String(Math.round(bpm))); + data.bpm = Math.round(bpm); } } if (track.replayGain) { const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = track.replayGain; - if (albumReplayGain) file.setProperty('REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain)); - if (albumPeakAmplitude) file.setProperty('REPLAYGAIN_ALBUM_PEAK', String(albumPeakAmplitude)); - if (trackReplayGain) file.setProperty('REPLAYGAIN_TRACK_GAIN', String(trackReplayGain)); - if (trackPeakAmplitude) file.setProperty('REPLAYGAIN_TRACK_PEAK', String(trackPeakAmplitude)); + data.replayGain = { + albumReplayGain: `${Number(albumReplayGain)} dB`, + trackReplayGain: `${Number(trackReplayGain)} dB`, + albumPeakAmplitude: albumPeakAmplitude ? Number(albumPeakAmplitude) : undefined, + trackPeakAmplitude: trackPeakAmplitude ? Number(trackPeakAmplitude) : undefined, + }; } const releaseDateStr = - track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : ''); + track.album?.releaseDate?.trim() || track?.streamStartDate?.split('T')?.[0]?.trim() || undefined; if (releaseDateStr) { try { - const year = new Date(releaseDateStr).getFullYear(); + const year = Number(releaseDateStr.split('-')[0]); if (!isNaN(year)) { - file.setProperty('DATE', String(year)); + data.releaseDate = String(releaseDateStr); } } catch { // Invalid date, skip - } - } - - if (track.copyright) { - file.setProperty('COPYRIGHT', track.copyright); - } - - if (track.isrc) { - file.setProperty('ISRC', track.isrc); - - if (isMp4) { - file.setMP4Item('xid ', `:isrc:${track.isrc}`); - } - } - - if (track.explicit) { - if (isMp4) { - file.setMP4Item('rtng', '1'); - } else { - file.setProperty('ITUNESADVISORY', '1'); + console.warn('Invalid date', releaseDateStr); } } @@ -173,14 +120,10 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet const coverBuffer = new Uint8Array(await coverBlob.arrayBuffer()); if (coverBlob) { - file.setPictures([ - { - mimeType: coverBlob.type, - data: coverBuffer, - type: PICTURE_TYPE_VALUES.FrontCover, - description: 'Cover Art', - }, - ]); + data.cover = { + data: coverBuffer, + type: getMimeType(coverBuffer), + }; } } } catch (e) { @@ -189,35 +132,24 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet try { const lyrics = await lyricsFetch; - const lyricsString = lyrics?.subtitles || lyrics?.plainLyrics; - - if (lyricsString) { - //if (isMp4) { - // file.setMP4Item('@lyr', String(lyricsString)); - //} else { - file.setProperty('LYRICS', String(lyricsString).replace(/\r/g, '').replace(/\n/g, '\r\n')); - //} - } + data.lyrics = lyrics?.subtitles || lyrics?.plainLyrics; } catch (e) { console.warn('Error setting lyrics metadata', track, e); } - console.timeEnd('Tagging file'); + const newAudioBuffer = await addMetadataWithTagLib(audioBuffer, { + ...data, + }); - console.time('Saving in-memory buffer'); - await file.save(); - console.timeEnd('Saving in-memory buffer'); - - console.time('Saving blob'); - const blob = new Blob([file.getFileBuffer()], { type: audioBlob.type, name: audioBlob.name }); - console.timeEnd('Saving blob'); - - return blob; + return doTimed( + 'Create new audio blob', + () => + new Blob([newAudioBuffer], { + type: audioBlob.type, + }) + ); } catch (err) { console.error(err); - } finally { - // Always dispose, even if there was an error. - file.dispose(); } return audioBlob; @@ -237,18 +169,36 @@ export async function readTrackMetadata(file, siblings = []) { duration: 0, isrc: null, copyright: null, + explicit: false, isLocal: true, file: file, id: `local-${file.name}-${file.lastModified}`, }; try { - if (file.type === 'audio/flac' || file.name.endsWith('.flac')) { - await readFlacMetadata(file, metadata); - } else if (file.type === 'audio/mp4' || file.name.endsWith('.m4a')) { - await readM4aMetadata(file, metadata); - } else if (file.type === 'audio/mpeg' || file.name.endsWith('.mp3')) { - await readMp3Metadata(file, metadata); + const data = await getMetadataWithTagLib(await file.arrayBuffer()); + + if (data) { + metadata.title = data.title || metadata.title; + metadata.artists.push( + ...(data.artist || '') + .split(';') + .map((a) => a.trim()) + .filter((a) => a) + ); + metadata.artist = data.artist || metadata.artist; + metadata.album.title = data.albumTitle || metadata.album.title; + metadata.album.releaseDate = data.releaseDate || metadata.album.releaseDate; + + if (data.cover) { + const blob = new Blob([data.cover.data], { type: data.cover.type }); + metadata.album.cover = URL.createObjectURL(blob); + } + + metadata.duration = data.duration; + metadata.isrc = data.isrc || metadata.isrc; + metadata.copyright = data.copyright || metadata.copyright; + metadata.explicit = !!data.explicit; } } catch (e) { console.warn('Error reading metadata for', file.name, e); diff --git a/js/taglib.ts b/js/taglib.ts index fea89f8..63373ba 100644 --- a/js/taglib.ts +++ b/js/taglib.ts @@ -1,19 +1,68 @@ import { TagLib } from 'taglib-wasm'; import { fetchBlobURL } from './utils'; import _TagLibWasm from '!/taglib-wasm/dist/taglib-web.wasm?url'; +import type { + TagLibWorkerMessageType, + AddMetadataMessage, + GetMetadataMessage, + TagLibFileResponse, + TagLibMetadataResponse, + TagLibMetadata, + TagLibReadMetadata, +} from './taglib.worker'; +import TagLibWorker from './taglib.worker.ts?url'; let tagLib: Promise | null = null; -export async function initTagLib(): Promise { - if (tagLib) return await tagLib; - - const TagLibWasm = await fetchBlobURL(_TagLibWasm); - - tagLib = TagLib.initialize({ - wasmUrl: TagLibWasm, - }); - - console.log('TagLib initialized', { tagLib: await tagLib, TagLibWasm }); - - return await tagLib; +async function fetchTagLib(): Promise { + return fetchTagLib.blobUrl || (fetchTagLib.blobUrl = await fetchBlobURL(_TagLibWasm)); +} + +namespace fetchTagLib { + export let blobUrl = ''; +} + +export { fetchTagLib }; + +export async function addMetadataWithTagLib( + audioData: Uint8Array, + data: Omit +) { + const worker = new Worker(new URL(TagLibWorker, import.meta.url), { type: 'module' }); + const wasmUrl = await fetchTagLib(); + + return new Promise((resolve, reject) => { + worker.onmessage = (e: MessageEvent) => { + const { data, error } = e.data; + + if (error) { + reject(new Error(error)); + } else { + resolve(data!); + } + }; + worker.onerror = reject; + worker.onmessageerror = reject; + worker.postMessage({ ...data, type: 'Add', wasmUrl, audioData }); + }); +} + +export async function getMetadataWithTagLib(audioData: Uint8Array) { + const worker = new Worker(new URL(TagLibWorker, import.meta.url), { type: 'module' }); + const wasmUrl = await fetchTagLib(); + + return new Promise((resolve, reject) => { + worker.onmessage = (e: MessageEvent) => { + const { data, error } = e.data; + + if (error) { + reject(new Error(error)); + } else { + resolve(data!); + } + }; + worker.onerror = reject; + worker.onmessageerror = reject; + worker.postMessage({ type: 'Get', wasmUrl, audioData }); + }); } diff --git a/js/taglib.worker.ts b/js/taglib.worker.ts new file mode 100644 index 0000000..391cc9c --- /dev/null +++ b/js/taglib.worker.ts @@ -0,0 +1,334 @@ +// filepath: /workspaces/monochrome/js/taglib.worker.ts + +import { TagLib, type PictureType } from 'taglib-wasm'; +import { doTimed, doTimedAsync } from './doTimed'; + +const PICTURE_TYPE_VALUES = { + FrontCover: 3, +}; + +export type TagLibWorkerMessageType = 'Add' | 'Get'; + +export interface TagLibWorkerMessage { + type: TagLibWorkerMessageType; + wasmUrl: string; + audioData: Uint8Array; +} + +interface TagLibWorkerResponse { + type: TagLibWorkerMessageType; + data?: T; + error?: string; +} + +export interface TagLibMetadata { + title?: string; + artist?: string; + albumTitle?: string; + albumArtist?: string; + trackNumber?: number; + totalTracks?: number; + discNumber?: number; + totalDiscs?: number; + bpm?: number; + replayGain?: { + albumReplayGain?: string; + albumPeakAmplitude?: number; + trackReplayGain?: string; + trackPeakAmplitude?: number; + }; + cover?: { + data: Uint8Array; + type: string; + }; + releaseDate?: string; + copyright?: string; + isrc?: string; + explicit?: boolean; + lyrics?: string; +} + +export interface TagLibReadMetadata extends TagLibMetadata { + duration: number; +} + +export type TagLibFileResponse = TagLibWorkerResponse; +export type TagLibMetadataResponse = TagLibWorkerResponse; + +export type AddMetadataMessage = TagLibWorkerMessage & { + type: 'Add'; +} & TagLibMetadata; + +export type GetMetadataMessage = TagLibWorkerMessage & { + type: 'Get'; +}; + +async function addMetadataToAudio(message: AddMetadataMessage): Promise { + const { + wasmUrl, + audioData, + title, + artist, + albumTitle, + albumArtist, + trackNumber, + totalTracks, + discNumber, + totalDiscs, + bpm, + replayGain, + cover, + releaseDate, + copyright, + isrc, + explicit, + lyrics, + } = message; + + const file = await doTimedAsync('Open file with taglib', async () => { + const tagLib = await TagLib.initialize({ + wasmUrl: wasmUrl, + }); + return await tagLib.open(audioData); + }); + + try { + doTimed('Tagging file', () => { + const isMp4 = file.isMP4(); + + if (title) { + file.setProperty('TITLE', title); + } + + if (artist) { + file.setProperty('ARTIST', artist); + } + + if (albumTitle) { + file.setProperty('ALBUM', albumTitle); + } + + const _albumArtist = albumArtist || artist; + if (_albumArtist) { + file.setProperty('ALBUMARTIST', _albumArtist); + } + + if (trackNumber) { + let trackString = String(trackNumber); + + if (isMp4 && trackNumber && totalTracks) { + trackString = `${trackNumber}/${totalTracks}`; + } + + if (isMp4) { + file.setProperty('TRACKNUMBER', trackString); + } else { + file.setProperty('TRACKNUMBER', String(trackNumber)); + } + } + + if (!isMp4 && totalTracks) { + file.setProperty('TRACKTOTAL', String(totalTracks)); + } + + if (discNumber) { + let discString = String(discNumber); + + if (isMp4 && discNumber && totalDiscs) { + discString = `${discNumber}/${totalDiscs}`; + } + + if (isMp4) { + file.setProperty('DISCNUMBER', discString); + } else { + file.setProperty('DISCNUMBER', String(discNumber)); + } + } + + if (!isMp4 && totalDiscs) { + file.setProperty('DISCTOTAL', String(totalDiscs)); + } + + if (bpm != null && Number.isFinite(bpm)) { + file.setProperty('BPM', String(Math.round(bpm))); + } + + if (replayGain) { + const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = replayGain; + if (albumReplayGain) file.setProperty('REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain)); + if (albumPeakAmplitude) file.setProperty('REPLAYGAIN_ALBUM_PEAK', String(albumPeakAmplitude)); + if (trackReplayGain) file.setProperty('REPLAYGAIN_TRACK_GAIN', String(trackReplayGain)); + if (trackPeakAmplitude) file.setProperty('REPLAYGAIN_TRACK_PEAK', String(trackPeakAmplitude)); + } + + if (releaseDate) { + try { + const year = Number(releaseDate.split('-')[0]); + if (!isNaN(year)) { + file.setProperty('DATE', String(year)); + } + } catch { + // Invalid date, skip + } + } + + if (copyright) { + file.setProperty('COPYRIGHT', copyright); + } + + if (isrc) { + file.setProperty('ISRC', isrc); + + if (isMp4) { + file.setMP4Item('xid ', `:isrc:${isrc}`); + } + } + + if (explicit) { + if (isMp4) { + file.setMP4Item('rtng', '1'); + } else { + file.setProperty('ITUNESADVISORY', '1'); + } + } + + if (lyrics) { + file.setProperty('LYRICS', lyrics.replace(/\r/g, '').replace(/\n/g, '\r\n')); + } + + if (cover) { + file.setPictures([ + { + mimeType: cover.type, + data: cover.data, + type: 'FrontCover', + description: 'Cover Art', + }, + ]); + } + }); + + await doTimedAsync('Saving in-memory buffer', () => file.save()); + + return file.getFileBuffer(); + } catch (err) { + console.error(err); + } finally { + file.dispose(); + } + + return audioData; +} + +async function getMetadataFromAudio(message: GetMetadataMessage): Promise { + const { wasmUrl, audioData } = message; + const data: TagLibReadMetadata = { + duration: 0, + }; + + const file = await doTimedAsync('Open file with taglib', async () => { + const tagLib = await TagLib.initialize({ + wasmUrl: wasmUrl, + }); + return await tagLib.open(audioData); + }); + + try { + const pictures = file.getPictures(); + const isMp4 = file.isMP4(); + const media = file.audioProperties(); + + data.duration = media.duration; + + data.title = file.getProperty('TITLE') || undefined; + data.artist = file.getProperty('ARTIST') || undefined; + data.albumTitle = file.getProperty('ALBUM') || undefined; + data.albumArtist = file.getProperty('ALBUMARTIST') || undefined; + const [trackNumber, trackTotal] = file + .getProperty('TRACKNUMBER') + ?.split('/') + .map((t) => Number(t.trim() || 0) || undefined); + data.trackNumber = trackNumber || undefined; + data.totalTracks = trackTotal ? trackTotal : Number(file.getProperty('TRACKTOTAL') || 0) || undefined; + + const [discNumber, discTotal] = file + .getProperty('DISCNUMBER') + ?.split('/') + .map((t) => Number(t.trim() || 0) || undefined); + data.discNumber = Number(file.getProperty('DISCNUMBER') || 0) || undefined; + + data.bpm = Number(file.getProperty('BPM') || 0) || undefined; + data.copyright = file.getProperty('COPYRIGHT') || undefined; + data.lyrics = file.getProperty('LYRICS') || undefined; + data.releaseDate = file.getProperty('DATE') || undefined; + + const [replayGainAlbumGain, replayGainAlbumPeak, replayGainTrackGain, replayGainTrackPeak] = [ + file.getProperty('REPLAYGAIN_ALBUM_GAIN'), + file.getProperty('REPLAYGAIN_ALBUM_PEAK'), + file.getProperty('REPLAYGAIN_TRACK_GAIN'), + file.getProperty('REPLAYGAIN_TRACK_PEAK'), + ]; + + const replayGain: TagLibMetadata['replayGain'] = {}; + if (replayGainAlbumGain) replayGain.albumReplayGain = replayGainAlbumGain; + if (replayGainAlbumPeak) replayGain.albumPeakAmplitude = Number(replayGainAlbumPeak); + if (replayGainTrackGain) replayGain.trackReplayGain = replayGainTrackGain; + if (replayGainTrackPeak) replayGain.trackPeakAmplitude = Number(replayGainTrackPeak); + if (Object.keys(replayGain).length > 0) { + data.replayGain = replayGain; + } + + data.isrc = (isMp4 && file.getMP4Item('xid ')?.split(':').at(-1)) || file.getProperty('ISRC') || undefined; + data.explicit = (isMp4 && file.getMP4Item('rtng') === '1') || file.getProperty('ITUNESADVISORY') === '1'; + + if (pictures.length > 0) { + const picture = pictures.filter((p) => p.type === 'FrontCover')[0]; + if (picture) { + data.cover = { + data: picture.data, + type: picture.mimeType, + }; + } + } + } catch (err) { + console.error(err); + } finally { + file.dispose(); + } + + return data; +} + +self.onmessage = async (event: MessageEvent) => { + switch (event.data.type) { + case 'Add': + try { + const result = await addMetadataToAudio(event.data as AddMetadataMessage); + self.postMessage({ + type: event.data.type, + data: result, + } satisfies TagLibFileResponse); + } catch (error) { + self.postMessage({ + type: event.data.type, + error: error instanceof Error ? error.message : String(error), + } satisfies TagLibWorkerResponse); + } + break; + + case 'Get': + try { + const result = await getMetadataFromAudio(event.data as GetMetadataMessage); + self.postMessage({ + type: event.data.type, + data: result, + } satisfies TagLibMetadataResponse); + } catch (error) { + self.postMessage({ + type: event.data.type, + error: error instanceof Error ? error.message : String(error), + } satisfies TagLibWorkerResponse); + } + break; + } +}; diff --git a/package.json b/package.json index 9fe206f..e4fed9e 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "jose": "^6.2.0", "npm": "^11.11.0", "pocketbase": "^0.26.8", - "taglib-wasm": "^1.0.5" + "taglib-wasm": "^1.0.5", + "uuid": "^13.0.0" } } diff --git a/tsconfig.json b/tsconfig.json index 71dc5e1..18c5a77 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "types": ["vite/client", "node"], "baseUrl": ".", "paths": { From 42101353aba57adcc6d92a1736a6c4c6aa711990 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:53:54 +0000 Subject: [PATCH 08/15] feat(metadata): re-add flac and m4a metadata code as separate files --- js/api.js | 21 +- js/downloads.js | 21 +- js/id3-writer.js | 156 -------- js/metadata.flac.js | 588 ++++++++++++++++++++++++++++++ js/metadata.js | 566 +---------------------------- js/metadata.mp3.js | 346 ++++++++++++++++++ js/metadata.mp4.js | 846 ++++++++++++++++++++++++++++++++++++++++++++ js/taglib.worker.ts | 29 +- js/utils.js | 41 +++ 9 files changed, 1877 insertions(+), 737 deletions(-) delete mode 100644 js/id3-writer.js create mode 100644 js/metadata.flac.js create mode 100644 js/metadata.mp3.js create mode 100644 js/metadata.mp4.js diff --git a/js/api.js b/js/api.js index cc8e0e3..dd0f551 100644 --- a/js/api.js +++ b/js/api.js @@ -13,6 +13,7 @@ import { DashDownloader } from './dash-downloader.js'; import { HlsDownloader } from './hls-downloader.js'; import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js'; import { ffmpeg, loadFfmpeg } from './ffmpeg.js'; +import { rebuildFlacWithoutMetadata } from './metadata.flac.js'; export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25'; @@ -1399,14 +1400,18 @@ export class LosslessAPI { try { switch (losslessContainerSettings.getContainer()) { case 'flac': - blob = await ffmpeg( - blob, - { args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] }, - 'output.flac', - 'audio/flac', - onProgress, - options.signal - ); + if ((await getExtensionFromBlob(blob)) != 'flac') { + blob = await ffmpeg( + blob, + { args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] }, + 'output.flac', + 'audio/flac', + onProgress, + options.signal + ); + } else { + blob = await rebuildFlacWithoutMetadata(blob); + } break; case 'alac': blob = await ffmpeg( diff --git a/js/downloads.js b/js/downloads.js index cb437ce..b984c4c 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -13,6 +13,7 @@ import { } from './utils.js'; import { lyricsSettings, bulkDownloadSettings, losslessContainerSettings, playlistSettings } from './storage.js'; import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js'; +import { rebuildFlacWithoutMetadata } from './metadata.flac.js'; import { DashDownloader } from './dash-downloader.js'; import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js'; import { encodeToMp3 } from './mp3-encoder.js'; @@ -379,14 +380,18 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign try { switch (losslessContainerSettings.getContainer()) { case 'flac': - blob = await ffmpeg( - blob, - { args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] }, - 'output.flac', - 'audio/flac', - onProgress, - signal - ); + if ((await getExtensionFromBlob(blob)) != 'flac') { + blob = await ffmpeg( + blob, + { args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] }, + 'output.flac', + 'audio/flac', + onProgress, + signal + ); + } else { + blob = await rebuildFlacWithoutMetadata(blob); + } break; case 'alac': blob = await ffmpeg( diff --git a/js/id3-writer.js b/js/id3-writer.js deleted file mode 100644 index 47a841b..0000000 --- a/js/id3-writer.js +++ /dev/null @@ -1,156 +0,0 @@ -import { getCoverBlob, getTrackTitle } from './utils.js'; - -async function writeID3v2Tag(mp3Blob, metadata, coverBlob = null) { - const frames = []; - - if (metadata.title) { - frames.push(createTextFrame('TIT2', getTrackTitle(metadata))); - } - - const artistName = metadata.artist?.name || metadata.artists?.[0]?.name; - if (artistName) { - frames.push(createTextFrame('TPE1', artistName)); - } - - if (metadata.album?.title) { - frames.push(createTextFrame('TALB', metadata.album.title)); - } - - const albumArtistName = metadata.album?.artist?.name || metadata.artist?.name || metadata.artists?.[0]?.name; - if (albumArtistName) { - frames.push(createTextFrame('TPE2', albumArtistName)); - } - - if (metadata.trackNumber) { - frames.push(createTextFrame('TRCK', metadata.trackNumber.toString())); - } - - if (metadata.album?.releaseDate) { - const year = new Date(metadata.album.releaseDate).getFullYear(); - if (!Number.isNaN(year) && Number.isFinite(year)) { - frames.push(createTextFrame('TYER', year.toString())); - } - } - - if (metadata.isrc) { - frames.push(createTextFrame('TSRC', metadata.isrc)); - } - - if (metadata.copyright) { - frames.push(createTextFrame('TCOP', metadata.copyright)); - } - - frames.push(createTextFrame('TENC', 'Monochrome')); - - if (coverBlob) { - frames.push(await createAPICFrame(coverBlob)); - } - - return buildID3v2Tag(mp3Blob, frames); -} - -function createTextFrame(frameId, text) { - // ID3v2.3 UTF-16 encoding with BOM - const bom = new Uint8Array([0xff, 0xfe]); // UTF-16LE BOM - const utf16Bytes = new Uint8Array(text.length * 2); - - for (let i = 0; i < text.length; i++) { - const charCode = text.charCodeAt(i); - utf16Bytes[i * 2] = charCode & 0xff; - utf16Bytes[i * 2 + 1] = (charCode >> 8) & 0xff; - } - - const frameSize = 1 + bom.length + utf16Bytes.length; - const frame = new Uint8Array(10 + frameSize); - const view = new DataView(frame.buffer); - - for (let i = 0; i < 4; i++) { - frame[i] = frameId.charCodeAt(i); - } - - view.setUint32(4, frameSize, false); - - frame[10] = 0x01; // UTF-16 with BOM - - frame.set(bom, 11); - frame.set(utf16Bytes, 11 + bom.length); - - return frame; -} - -async function createAPICFrame(coverBlob) { - const imageBytes = new Uint8Array(await coverBlob.arrayBuffer()); - const mimeType = coverBlob.type || 'image/jpeg'; - const mimeBytes = new TextEncoder().encode(mimeType); - - const frameSize = 1 + mimeBytes.length + 1 + 1 + 1 + imageBytes.length; - - const frame = new Uint8Array(10 + frameSize); - const view = new DataView(frame.buffer); - - for (let i = 0; i < 4; i++) { - frame[i] = 'APIC'.charCodeAt(i); - } - - view.setUint32(4, frameSize, false); - - let offset = 10; - frame[offset++] = 0x00; - - frame.set(mimeBytes, offset); - offset += mimeBytes.length; - frame[offset++] = 0x00; - - frame[offset++] = 0x03; - - frame[offset++] = 0x00; - - frame.set(imageBytes, offset); - - return frame; -} - -function buildID3v2Tag(mp3Blob, frames) { - const framesData = new Uint8Array(frames.reduce((acc, f) => acc + f.length, 0)); - let offset = 0; - for (const frame of frames) { - framesData.set(frame, offset); - offset += frame.length; - } - - const tagSize = framesData.length; - - const header = new Uint8Array(10); - header[0] = 0x49; - header[1] = 0x44; - header[2] = 0x33; - header[3] = 0x03; - header[4] = 0x00; - header[5] = 0x00; - - header[6] = (tagSize >> 21) & 0x7f; - header[7] = (tagSize >> 14) & 0x7f; - header[8] = (tagSize >> 7) & 0x7f; - header[9] = tagSize & 0x7f; - - return new Blob([header, framesData, mp3Blob], { type: 'audio/mpeg' }); -} - -export async function addMp3Metadata(mp3Blob, track, api) { - try { - let coverBlob = null; - - if (track.album?.cover) { - try { - coverBlob = await getCoverBlob(api, track.album.cover); - } catch (error) { - console.warn('Failed to fetch album art for MP3:', error); - } - } - - return await writeID3v2Tag(mp3Blob, track, coverBlob); - } catch (error) { - console.error('Failed to add MP3 metadata:', error); - return mp3Blob; - } -} diff --git a/js/metadata.flac.js b/js/metadata.flac.js new file mode 100644 index 0000000..35e4271 --- /dev/null +++ b/js/metadata.flac.js @@ -0,0 +1,588 @@ +import { getCoverBlob, getTrackTitle } from './utils.js'; +import { getFullArtistString } from './utils.js'; +import { METADATA_STRINGS } from './metadata.js'; + +export const FLAC_MIME_TYPE = 'audio/flac'; + +export async function readFlacMetadata(file, metadata) { + const arrayBuffer = await file.arrayBuffer(); + const dataView = new DataView(arrayBuffer); + + if (!isFlacFile(dataView)) return; + + const blocks = parseFlacBlocks(dataView); + const vorbisBlock = blocks.find((b) => b.type === 4); + const pictureBlock = blocks.find((b) => b.type === 6); + const streamInfo = blocks.find((b) => b.type === 0); + + const artists = []; + if (vorbisBlock) { + const offset = vorbisBlock.offset; + const vendorLen = dataView.getUint32(offset, true); + let pos = offset + 4 + vendorLen; + const commentListLen = dataView.getUint32(pos, true); + pos += 4; + + for (let i = 0; i < commentListLen; i++) { + const len = dataView.getUint32(pos, true); + pos += 4; + const comment = new TextDecoder().decode(new Uint8Array(arrayBuffer, pos, len)); + pos += len; + + const eqIdx = comment.indexOf('='); + if (eqIdx > -1) { + const key = comment.substring(0, eqIdx); + const value = comment.substring(eqIdx + 1); + const upperKey = key.toUpperCase(); + if (upperKey === 'TITLE') metadata.title = value; + if (upperKey === 'ARTIST' || upperKey === 'ALBUMARTIST') { + artists.push(value); + } + if (upperKey === 'ALBUM') metadata.album.title = value; + if (upperKey === 'ISRC') metadata.isrc = value; + if (upperKey === 'COPYRIGHT') metadata.copyright = value; + if (upperKey === 'ITUNESADVISORY') metadata.explicit = value === '1'; + } + } + } + + if (streamInfo) { + const offset = streamInfo.offset; + + // Sample Rate is 20 bits spanning bytes 10, 11, and the first 4 bits of 12 + const byte10 = dataView.getUint8(offset + 10); + const byte11 = dataView.getUint8(offset + 11); + const byte12 = dataView.getUint8(offset + 12); + + // since data for some reason spans across multiple bytes, we need to combine them into one int + const sampleRate = (byte10 << 12) | (byte11 << 4) | (byte12 >> 4); + + const byte13 = dataView.getUint8(offset + 13); + const tsHigh = byte13 & 0x0f; + const tsLow = dataView.getUint32(offset + 14, false); + + // same thing for total samples + const totalSamples = tsHigh * 0x100000000 + tsLow; + + if (sampleRate > 0) { + // beatiful + metadata.duration = totalSamples / sampleRate; + } + } + + if (artists.length > 0) { + metadata.artists = artists.flatMap((a) => a.split(/; |\/|\\/)).map((name) => ({ name: name.trim() })); + } + + if (pictureBlock) { + try { + let pos = pictureBlock.offset; + pos += 4; + const mimeLen = dataView.getUint32(pos, false); + pos += 4; + const mime = new TextDecoder().decode(new Uint8Array(arrayBuffer, pos, mimeLen)); + pos += mimeLen; + const descLen = dataView.getUint32(pos, false); + pos += 4; + pos += descLen; + pos += 16; + const dataLen = dataView.getUint32(pos, false); + pos += 4; + const pictureData = new Uint8Array(arrayBuffer, pos, dataLen); + const blob = new Blob([pictureData], { type: mime }); + metadata.album.cover = URL.createObjectURL(blob); + } catch (e) { + console.warn('Error parsing FLAC picture:', e); + } + } +} + +/** + * Adds Vorbis comment metadata to FLAC files + */ +export async function addFlacMetadata(flacBlob, track, api) { + try { + const arrayBuffer = await flacBlob.arrayBuffer(); + const dataView = new DataView(arrayBuffer); + + // Verify FLAC signature + if (!isFlacFile(dataView)) { + console.warn('Not a valid FLAC file, returning original'); + return flacBlob; + } + + // Parse FLAC structure + const blocks = parseFlacBlocks(dataView); + + // If parsing failed or no audio data found, return original + if (!blocks || blocks.length === 0 || blocks.audioDataOffset === undefined) { + console.warn('Failed to parse FLAC blocks, returning original'); + return flacBlob; + } + + // Check for STREAMINFO block (must be first, type 0) + if (blocks[0].type !== 0) { + console.warn('FLAC file missing STREAMINFO block, returning original'); + return flacBlob; + } + + // Create or update Vorbis comment block + const vorbisCommentBlock = createVorbisCommentBlock(track); + + // Fetch album artwork if available + let pictureBlock = null; + if (track.album?.cover) { + try { + pictureBlock = await createFlacPictureBlock(track.album.cover, api); + } catch (error) { + console.warn('Failed to embed album art:', error); + } + } + + // Rebuild FLAC file with new metadata + let newFlacData; + try { + newFlacData = rebuildFlacWithMetadata(dataView, blocks, vorbisCommentBlock, pictureBlock); + } catch (rebuildError) { + console.error('Failed to rebuild FLAC structure:', rebuildError); + return flacBlob; + } + + // Validate the rebuilt file + const validationView = new DataView(newFlacData.buffer); + if (!isFlacFile(validationView)) { + console.error('Rebuilt FLAC has invalid signature, returning original'); + return flacBlob; + } + + // Validate new file has proper block structure + const newBlocks = parseFlacBlocks(validationView); + if (!newBlocks || newBlocks.length === 0 || newBlocks.audioDataOffset === undefined) { + console.error('Rebuilt FLAC has invalid block structure, returning original'); + return flacBlob; + } + + return new Blob([newFlacData], { type: 'audio/flac' }); + } catch (error) { + console.error('Failed to add FLAC metadata:', error); + return flacBlob; + } +} + +export function isFlacFile(dataView) { + // Check for "fLaC" signature at the beginning + return ( + dataView.byteLength >= 4 && + dataView.getUint8(0) === 0x66 && // 'f' + dataView.getUint8(1) === 0x4c && // 'L' + dataView.getUint8(2) === 0x61 && // 'a' + dataView.getUint8(3) === 0x43 + ); // 'C' +} + +export function parseFlacBlocks(dataView) { + const blocks = []; + let offset = 4; // Skip "fLaC" signature + + while (offset + 4 <= dataView.byteLength) { + const header = dataView.getUint8(offset); + const isLast = (header & 0x80) !== 0; + const blockType = header & 0x7f; + + // Block type 127 is invalid, types > 6 are reserved (except 127) + // Valid types: 0=STREAMINFO, 1=PADDING, 2=APPLICATION, 3=SEEKTABLE, 4=VORBIS_COMMENT, 5=CUESHEET, 6=PICTURE + if (blockType === 127) { + console.warn('Encountered invalid block type 127, stopping parse'); + break; + } + + const blockSize = + (dataView.getUint8(offset + 1) << 16) | + (dataView.getUint8(offset + 2) << 8) | + dataView.getUint8(offset + 3); + + // Validate block size + if (blockSize < 0 || offset + 4 + blockSize > dataView.byteLength) { + console.warn(`Invalid block size ${blockSize} at offset ${offset}, stopping parse`); + break; + } + + blocks.push({ + type: blockType, + isLast: isLast, + size: blockSize, + offset: offset + 4, + headerOffset: offset, + }); + + offset += 4 + blockSize; + + if (isLast) { + // Save the audio data offset + blocks.audioDataOffset = offset; + break; + } + } + + // If we didn't find the last block marker, estimate audio offset + if (blocks.audioDataOffset === undefined && blocks.length > 0) { + const lastBlock = blocks[blocks.length - 1]; + blocks.audioDataOffset = lastBlock.headerOffset + 4 + lastBlock.size; + console.warn('No last-block marker found, estimated audio offset:', blocks.audioDataOffset); + } + + return blocks; +} + +export function createVorbisComments(track) { + // Vorbis comment structure + const comments = []; + const discNumber = track.volumeNumber ?? track.discNumber; + + // Add standard tags + if (track.title) { + comments.push(['TITLE', getTrackTitle(track)]); + } + const artistStr = getFullArtistString(track); + if (artistStr) { + comments.push(['ARTIST', artistStr]); + } + if (track.album?.title) { + comments.push(['ALBUM', track.album.title]); + } + const albumArtist = track.album?.artist?.name || track.artist?.name; + if (albumArtist) { + comments.push(['ALBUMARTIST', albumArtist]); + } + if (track.trackNumber) { + comments.push(['TRACKNUMBER', String(track.trackNumber)]); + } + if (discNumber) { + comments.push(['DISCNUMBER', String(discNumber)]); + } + if (track.album?.numberOfTracks) { + comments.push(['TRACKTOTAL', String(track.album.numberOfTracks)]); + } + if (track.bpm != null) { + const bpm = Number(track.bpm); + if (Number.isFinite(bpm)) { + comments.push(['TEMPO', String(Math.round(bpm))]); + } + } + if (track.replayGain) { + const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = track.replayGain; + if (albumReplayGain) comments.push(['REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain)]); + if (albumPeakAmplitude) comments.push(['REPLAYGAIN_ALBUM_PEAK', String(albumPeakAmplitude)]); + if (trackReplayGain) comments.push(['REPLAYGAIN_TRACK_GAIN', String(trackReplayGain)]); + if (trackPeakAmplitude) comments.push(['REPLAYGAIN_TRACK_PEAK', String(trackPeakAmplitude)]); + } + + const releaseDateStr = + track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : ''); + if (releaseDateStr) { + try { + const year = new Date(releaseDateStr).getFullYear(); + if (!isNaN(year)) { + comments.push(['DATE', String(year)]); + } + } catch { + // Invalid date, skip + } + } + + if (track.copyright) { + comments.push(['COPYRIGHT', track.copyright]); + } + if (track.isrc) { + comments.push(['ISRC', track.isrc]); + } + if (track.explicit) { + comments.push(['ITUNESADVISORY', '1']); + } + + return comments; +} + +export function createVorbisCommentBlock(comments = []) { + // Calculate total size + const vendor = METADATA_STRINGS.VENDOR_STRING; + const vendorBytes = new TextEncoder().encode(vendor); + + let totalSize = 4 + vendorBytes.length + 4; // vendor length + vendor + comment count + + const encodedComments = comments.map(([key, value]) => { + const text = `${key}=${value}`; + const bytes = new TextEncoder().encode(text); + totalSize += 4 + bytes.length; + return bytes; + }); + + // Create buffer + const buffer = new ArrayBuffer(totalSize); + const view = new DataView(buffer); + const uint8Array = new Uint8Array(buffer); + + let offset = 0; + + // Vendor length (little-endian) + view.setUint32(offset, vendorBytes.length, true); + offset += 4; + + // Vendor string + uint8Array.set(vendorBytes, offset); + offset += vendorBytes.length; + + // Comment count (little-endian) + view.setUint32(offset, comments.length, true); + offset += 4; + + // Comments + for (const commentBytes of encodedComments) { + view.setUint32(offset, commentBytes.length, true); + offset += 4; + uint8Array.set(commentBytes, offset); + offset += commentBytes.length; + } + + return uint8Array; +} + +export async function createFlacPictureBlock(coverId, api) { + try { + // Fetch album art + const imageBlob = await getCoverBlob(api, coverId); + if (!imageBlob) { + throw new Error('Failed to fetch album art'); + } + + const imageBytes = new Uint8Array(await imageBlob.arrayBuffer()); + + // Detect MIME type from blob or use default + const mimeType = imageBlob.type || 'image/jpeg'; + const mimeBytes = new TextEncoder().encode(mimeType); + const description = ''; + const descBytes = new TextEncoder().encode(description); + + // Calculate total size + const totalSize = + 4 + // picture type + 4 + + mimeBytes.length + // mime length + mime + 4 + + descBytes.length + // desc length + desc + 4 + // width + 4 + // height + 4 + // color depth + 4 + // indexed colors + 4 + + imageBytes.length; // image length + image + + const buffer = new ArrayBuffer(totalSize); + const view = new DataView(buffer); + const uint8Array = new Uint8Array(buffer); + + let offset = 0; + + // Picture type (3 = front cover) + view.setUint32(offset, 3, false); + offset += 4; + + // MIME type length + view.setUint32(offset, mimeBytes.length, false); + offset += 4; + + // MIME type + uint8Array.set(mimeBytes, offset); + offset += mimeBytes.length; + + // Description length + view.setUint32(offset, descBytes.length, false); + offset += 4; + + // Description (empty) + if (descBytes.length > 0) { + uint8Array.set(descBytes, offset); + offset += descBytes.length; + } + + // Width (0 = unknown) + view.setUint32(offset, 0, false); + offset += 4; + + // Height (0 = unknown) + view.setUint32(offset, 0, false); + offset += 4; + + // Color depth (0 = unknown) + view.setUint32(offset, 0, false); + offset += 4; + + // Indexed colors (0 = not indexed) + view.setUint32(offset, 0, false); + offset += 4; + + // Image data length + view.setUint32(offset, imageBytes.length, false); + offset += 4; + + // Image data + uint8Array.set(imageBytes, offset); + + return uint8Array; + } catch (error) { + console.error('Failed to create FLAC picture block:', error); + return null; + } +} + +export function rebuildFlacWithMetadata( + dataView, + blocks, + vorbisCommentBlock = createVorbisCommentBlock(), + pictureBlock +) { + const originalArray = new Uint8Array(dataView.buffer); + + // Remove old Vorbis comment and picture blocks + const filteredBlocks = blocks.filter((b) => b.type !== 4 && b.type !== 6); // 4 = Vorbis, 6 = Picture + + // Calculate new file size + let newSize = 4; // "fLaC" signature + + // Add STREAMINFO and other essential blocks + for (const block of filteredBlocks) { + newSize += 4 + block.size; // header + data + } + + if (vorbisCommentBlock) { + // Add new Vorbis comment block + newSize += 4 + vorbisCommentBlock.length; + } + + // Add picture block if available + if (pictureBlock) { + newSize += 4 + pictureBlock.length; + } + + // Add audio data + const audioDataOffset = blocks.audioDataOffset; + if (audioDataOffset === undefined) { + throw new Error('Invalid FLAC file structure: unable to locate audio data stream'); + } + const audioDataSize = dataView.byteLength - audioDataOffset; + newSize += audioDataSize; + + // Build new file + const newFile = new Uint8Array(newSize); + let offset = 0; + + // Write "fLaC" signature + newFile[offset++] = 0x66; // 'f' + newFile[offset++] = 0x4c; // 'L' + newFile[offset++] = 0x61; // 'a' + newFile[offset++] = 0x43; // 'C' + + // Write existing blocks (except Vorbis and Picture) + for (let i = 0; i < filteredBlocks.length; i++) { + const block = filteredBlocks[i]; + const isLast = false; // We'll add more blocks + + // Write block header + const header = (isLast ? 0x80 : 0x00) | block.type; + newFile[offset++] = header; + newFile[offset++] = (block.size >> 16) & 0xff; + newFile[offset++] = (block.size >> 8) & 0xff; + newFile[offset++] = block.size & 0xff; + + // Write block data + newFile.set(originalArray.subarray(block.offset, block.offset + block.size), offset); + offset += block.size; + } + + let lastBlockHeaderOffset = offset; + + if (vorbisCommentBlock) { + // Write new Vorbis comment block + const vorbisHeaderOffset = offset; + const vorbisHeader = 0x04; // Vorbis comment type + newFile[offset++] = vorbisHeader; + newFile[offset++] = (vorbisCommentBlock.length >> 16) & 0xff; + newFile[offset++] = (vorbisCommentBlock.length >> 8) & 0xff; + newFile[offset++] = vorbisCommentBlock.length & 0xff; + newFile.set(vorbisCommentBlock, offset); + offset += vorbisCommentBlock.length; + lastBlockHeaderOffset = vorbisHeaderOffset; + } + + // Write picture block if available + if (pictureBlock) { + const pictureHeaderOffset = offset; + const pictureHeader = 0x06; // Picture type + newFile[offset++] = pictureHeader; + newFile[offset++] = (pictureBlock.length >> 16) & 0xff; + newFile[offset++] = (pictureBlock.length >> 8) & 0xff; + newFile[offset++] = pictureBlock.length & 0xff; + newFile.set(pictureBlock, offset); + offset += pictureBlock.length; + lastBlockHeaderOffset = pictureHeaderOffset; + } + + // Mark the last metadata block with the 0x80 flag + newFile[lastBlockHeaderOffset] |= 0x80; + + // Write audio data + if (audioDataSize > 0) { + newFile.set(originalArray.subarray(audioDataOffset, audioDataOffset + audioDataSize), offset); + } + + return newFile; +} + +export function getFlacBlocks(dataView) { + // Verify FLAC signature + if (!isFlacFile(dataView)) { + throw new Error('Not a valid FLAC file'); + } + + // Parse FLAC structure + const blocks = parseFlacBlocks(dataView); + + // If parsing failed or no audio data found, return original + if (!blocks || blocks.length === 0 || blocks.audioDataOffset === undefined) { + throw new Error('Failed to parse FLAC blocks'); + } + + // Check for STREAMINFO block (must be first, type 0) + if (blocks[0].type !== 0) { + throw new Error('FLAC file missing STREAMINFO block'); + } + + return blocks; +} + +/** + * Removes all metadata from a FLAC file blob and returns the rebuilt FLAC data. + * + * @async + * @param {Blob} flacBlob - The FLAC audio file as a Blob object + * @returns {Promise} A Promise that resolves to a new Blob containing the FLAC file without metadata, + * or the original flacBlob if an error occurs during processing + * @throws {Error} Logs errors to console but catches and returns original blob instead of throwing + * + * @example + * const flacFile = new Blob([arrayBuffer], { type: 'audio/flac' }); + * const cleanFlac = await rebuildFlacWithoutMetadata(flacFile); + */ +export async function rebuildFlacWithoutMetadata(flacBlob) { + try { + const arrayBuffer = await flacBlob.arrayBuffer(); + const dataView = new DataView(arrayBuffer); + const blocks = getFlacBlocks(dataView); + return new Blob([rebuildFlacWithMetadata(dataView, blocks, createVorbisCommentBlock(), null)], { + type: FLAC_MIME_TYPE, + }); + } catch (err) { + console.error('Error rebuilding FLAC file:', err); + return flacBlob; + } +} diff --git a/js/metadata.js b/js/metadata.js index e09320a..c6c37ec 100644 --- a/js/metadata.js +++ b/js/metadata.js @@ -1,46 +1,14 @@ -import { getCoverBlob, getTrackTitle } from './utils.js'; +import { getCoverBlob, getTrackTitle, getFullArtistString, getMimeType } from './utils.js'; import { fetchTagLib, addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts'; import { doTimed, doTimedAsync } from './doTimed.ts'; import { managers } from './app.js'; -const VENDOR_STRING = 'Monochrome'; -const DEFAULT_TITLE = 'Unknown Title'; -const DEFAULT_ARTIST = 'Unknown Artist'; -const DEFAULT_ALBUM = 'Unknown Album'; - -/** - * Builds a full artist string by combining the track's listed artists - * with any featured artists parsed from the title (feat./with). - */ -function getFullArtistString(track) { - const knownArtists = - Array.isArray(track.artists) && track.artists.length > 0 - ? track.artists.map((a) => (typeof a === 'string' ? a : a.name) || '').filter(Boolean) - : track.artist?.name - ? [track.artist.name] - : []; - - // Parse featured artists from title, e.g. "Song (feat. A, B & C)" or "(with X & Y)" - // Note: splitting on '&' may incorrectly fragment compound artist names like "Simon & Garfunkel". - const featPattern = /\(\s*(?:feat\.?|ft\.?|with)\s+(.+?)\s*\)/gi; - const allFeatArtists = [...(track.title?.matchAll(featPattern) ?? [])].flatMap((m) => - m[1] - .split(/\s*[,&]\s*/) - .map((s) => s.trim()) - .filter(Boolean) - ); - if (allFeatArtists.length > 0) { - const knownLower = new Set(knownArtists.map((n) => n.toLowerCase())); - for (const feat of allFeatArtists) { - if (!knownLower.has(feat.toLowerCase())) { - knownArtists.push(feat); - knownLower.add(feat.toLowerCase()); - } - } - } - - return knownArtists.join('; ') || null; -} +export const METADATA_STRINGS = { + VENDOR_STRING: 'Monochrome', + DEFAULT_TITLE: 'Unknown Title', + DEFAULT_ARTIST: 'Unknown Artist', + DEFAULT_ALBUM: 'Unknown Album', +}; export function prefetchMetadataObjects(track, api) { const _tagLib = fetchTagLib().catch(console.error); @@ -137,6 +105,10 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet console.warn('Error setting lyrics metadata', track, e); } + if (!(audioBuffer instanceof Uint8Array)) { + throw new Error('Audio buffer is not a Uint8Array'); + } + const newAudioBuffer = await addMetadataWithTagLib(audioBuffer, { ...data, }); @@ -227,519 +199,3 @@ export async function readTrackMetadata(file, siblings = []) { return metadata; } - -async function readFlacMetadata(file, metadata) { - const arrayBuffer = await file.arrayBuffer(); - const dataView = new DataView(arrayBuffer); - - if (!isFlacFile(dataView)) return; - - const blocks = parseFlacBlocks(dataView); - const vorbisBlock = blocks.find((b) => b.type === 4); - const pictureBlock = blocks.find((b) => b.type === 6); - const streamInfo = blocks.find((b) => b.type === 0); - - const artists = []; - if (vorbisBlock) { - const offset = vorbisBlock.offset; - const vendorLen = dataView.getUint32(offset, true); - let pos = offset + 4 + vendorLen; - const commentListLen = dataView.getUint32(pos, true); - pos += 4; - - for (let i = 0; i < commentListLen; i++) { - const len = dataView.getUint32(pos, true); - pos += 4; - const comment = new TextDecoder().decode(new Uint8Array(arrayBuffer, pos, len)); - pos += len; - - const eqIdx = comment.indexOf('='); - if (eqIdx > -1) { - const key = comment.substring(0, eqIdx); - const value = comment.substring(eqIdx + 1); - const upperKey = key.toUpperCase(); - if (upperKey === 'TITLE') metadata.title = value; - if (upperKey === 'ARTIST' || upperKey === 'ALBUMARTIST') { - artists.push(value); - } - if (upperKey === 'ALBUM') metadata.album.title = value; - if (upperKey === 'ISRC') metadata.isrc = value; - if (upperKey === 'COPYRIGHT') metadata.copyright = value; - if (upperKey === 'ITUNESADVISORY') metadata.explicit = value === '1'; - } - } - } - - if (streamInfo) { - const offset = streamInfo.offset; - - // Sample Rate is 20 bits spanning bytes 10, 11, and the first 4 bits of 12 - const byte10 = dataView.getUint8(offset + 10); - const byte11 = dataView.getUint8(offset + 11); - const byte12 = dataView.getUint8(offset + 12); - - // since data for some reason spans across multiple bytes, we need to combine them into one int - const sampleRate = (byte10 << 12) | (byte11 << 4) | (byte12 >> 4); - - const byte13 = dataView.getUint8(offset + 13); - const tsHigh = byte13 & 0x0f; - const tsLow = dataView.getUint32(offset + 14, false); - - // same thing for total samples - const totalSamples = tsHigh * 0x100000000 + tsLow; - - if (sampleRate > 0) { - // beatiful - metadata.duration = totalSamples / sampleRate; - } - } - - if (artists.length > 0) { - metadata.artists = artists.flatMap((a) => a.split(/; |\/|\\/)).map((name) => ({ name: name.trim() })); - } - - if (pictureBlock) { - try { - let pos = pictureBlock.offset; - pos += 4; - const mimeLen = dataView.getUint32(pos, false); - pos += 4; - const mime = new TextDecoder().decode(new Uint8Array(arrayBuffer, pos, mimeLen)); - pos += mimeLen; - const descLen = dataView.getUint32(pos, false); - pos += 4; - pos += descLen; - pos += 16; - const dataLen = dataView.getUint32(pos, false); - pos += 4; - const pictureData = new Uint8Array(arrayBuffer, pos, dataLen); - const blob = new Blob([pictureData], { type: mime }); - metadata.album.cover = URL.createObjectURL(blob); - } catch (e) { - console.warn('Error parsing FLAC picture:', e); - } - } -} - -async function readM4aMetadata(file, metadata) { - try { - const chunkSize = Math.min(file.size, 5 * 1024 * 1024); - const buffer = await file.slice(0, chunkSize).arrayBuffer(); - const view = new DataView(buffer); - - const atoms = parseMp4Atoms(view); - - const moov = atoms.find((a) => a.type === 'moov'); - if (!moov) return; - - const moovStart = moov.offset + 8; - const moovLen = moov.size - 8; - const moovData = new DataView(view.buffer, moovStart, moovLen); - const moovAtoms = parseMp4Atoms(moovData); - - // mvhd metadata tag - const mvhd = moovAtoms.find((a) => a.type === 'mvhd'); - if (mvhd) { - const mvhdStart = moovStart + mvhd.offset + 8; - const version = view.getUint8(mvhdStart); - - // resolution and length, basically - let timeScale, duration; - - if (version === 0) { - // 32-bit format - timeScale = view.getUint32(mvhdStart + 12, false); - duration = view.getUint32(mvhdStart + 16, false); - } else if (version === 1) { - // 64-bit format - timeScale = view.getUint32(mvhdStart + 20, false); - const durHigh = view.getUint32(mvhdStart + 24, false); - const durLow = view.getUint32(mvhdStart + 28, false); - duration = durHigh * 0x100000000 + durLow; - } - - if (timeScale > 0) { - metadata.duration = duration / timeScale; - } - } - - const udta = moovAtoms.find((a) => a.type === 'udta'); - if (!udta) return; - - const udtaStart = moovStart + udta.offset + 8; - const udtaLen = udta.size - 8; - const udtaData = new DataView(view.buffer, udtaStart, udtaLen); - const udtaAtoms = parseMp4Atoms(udtaData); - - const meta = udtaAtoms.find((a) => a.type === 'meta'); - if (!meta) return; - - const metaStart = udtaStart + meta.offset + 12; - const metaLen = meta.size - 12; - const metaData = new DataView(view.buffer, metaStart, metaLen); - const metaAtoms = parseMp4Atoms(metaData); - - const ilst = metaAtoms.find((a) => a.type === 'ilst'); - if (!ilst) return; - - const ilstStart = metaStart + ilst.offset + 8; - const ilstLen = ilst.size - 8; - const ilstData = new DataView(view.buffer, ilstStart, ilstLen); - const items = parseMp4Atoms(ilstData); - - let artistStr = null; - - for (const item of items) { - const itemStart = ilstStart + item.offset + 8; - const itemLen = item.size - 8; - const itemData = new DataView(view.buffer, itemStart, itemLen); - const dataAtom = parseMp4Atoms(itemData).find((a) => a.type === 'data'); - if (dataAtom) { - const contentLen = dataAtom.size - 16; - const contentOffset = itemStart + dataAtom.offset + 16; - - if (item.type === '©nam') { - metadata.title = new TextDecoder().decode(new Uint8Array(view.buffer, contentOffset, contentLen)); - } else if (item.type === '©ART') { - artistStr = new TextDecoder().decode(new Uint8Array(view.buffer, contentOffset, contentLen)); - } else if (item.type === '©alb') { - metadata.album.title = new TextDecoder().decode( - new Uint8Array(view.buffer, contentOffset, contentLen) - ); - } else if (item.type === 'ISRC') { - metadata.isrc = new TextDecoder().decode(new Uint8Array(view.buffer, contentOffset, contentLen)); - } else if (item.type === 'cprt') { - metadata.copyright = new TextDecoder().decode( - new Uint8Array(view.buffer, contentOffset, contentLen) - ); - } else if (item.type === 'covr') { - const pictureData = new Uint8Array(view.buffer, contentOffset, contentLen); - const mime = getMimeType(pictureData); - const blob = new Blob([pictureData], { type: mime }); - metadata.album.cover = URL.createObjectURL(blob); - } else if (item.type === 'rtng') { - metadata.explicit = - contentLen > 0 && new Uint8Array(view.buffer, contentOffset, contentLen)[0] === 1; - } - } - } - - if (artistStr) { - metadata.artists = artistStr.split(/; |\/|\\/).map((name) => ({ name: name.trim() })); - } - } catch (e) { - console.warn('Error parsing M4A:', e); - } -} - -async function readMp3Metadata(file, metadata) { - let buffer = await file.slice(0, 10).arrayBuffer(); - let view = new DataView(buffer); - - if (view.getUint8(0) === 0x49 && view.getUint8(1) === 0x44 && view.getUint8(2) === 0x33) { - const majorVer = view.getUint8(3); - const size = readSynchsafeInteger32(view, 6); - const tagSize = size + 10; - - buffer = await file.slice(0, tagSize).arrayBuffer(); - view = new DataView(buffer); - - let offset = 10; - if ((view.getUint8(5) & 0x40) !== 0) { - const extSize = readSynchsafeInteger32(view, offset); - offset += extSize; - } - - let tpe1 = null; - let tpe2 = null; - while (offset < view.byteLength) { - let frameId, frameSize; - - if (majorVer === 3) { - frameId = new TextDecoder().decode(new Uint8Array(buffer, offset, 4)); - frameSize = view.getUint32(offset + 4, false); - offset += 10; - } else if (majorVer === 4) { - frameId = new TextDecoder().decode(new Uint8Array(buffer, offset, 4)); - frameSize = readSynchsafeInteger32(view, offset + 4); - offset += 10; - } else { - break; - } - - if (frameId.charCodeAt(0) === 0) break; - if (offset + frameSize > view.byteLength) break; - - const frameData = new DataView(buffer, offset, frameSize); - if (frameId === 'TIT2') metadata.title = readID3Text(frameData); - if (frameId === 'TPE1') tpe1 = readID3Text(frameData); - if (frameId === 'TPE2') tpe2 = readID3Text(frameData); - if (frameId === 'TALB') metadata.album.title = readID3Text(frameData); - if (frameId === 'TSRC') metadata.isrc = readID3Text(frameData); - if (frameId === 'TCOP') metadata.copyright = readID3Text(frameData); - if (frameId === 'TLEN') metadata.duration = parseInt(readID3Text(frameData)) / 1000; // usually not present - if (frameId === 'TYER' || frameId === 'TDRC') { - const year = readID3Text(frameData); - if (year) metadata.album.releaseDate = year; - } - if (frameId === 'APIC') { - try { - const encoding = frameData.getUint8(0); - let mimeType = ''; - let pos = 1; - while (pos < frameData.byteLength && frameData.getUint8(pos) !== 0) { - mimeType += String.fromCharCode(frameData.getUint8(pos)); - pos++; - } - pos++; - pos++; - let terminator = encoding === 1 || encoding === 2 ? 2 : 1; - while (pos < frameData.byteLength) { - if (frameData.getUint8(pos) === 0) { - if (terminator === 1) { - pos++; - break; - } else if (pos + 1 < frameData.byteLength && frameData.getUint8(pos + 1) === 0) { - pos += 2; - break; - } - } - pos++; - } - const pictureData = new Uint8Array(buffer, offset + pos, frameSize - pos); - const blob = new Blob([pictureData], { type: mimeType || 'image/jpeg' }); - metadata.album.cover = URL.createObjectURL(blob); - } catch (e) { - console.warn('Error parsing APIC:', e); - } - } - - offset += frameSize; - } - - const artistStr = tpe1 || tpe2; - if (artistStr) { - metadata.artists = artistStr.split('/').map((name) => ({ name: name.trim() })); - } - - if (!metadata.duration || metadata.duration === 0) { - metadata.duration = await calculateMp3Duration(file, tagSize); - } - } - - if (file.size > 128) { - const tailBuffer = await file.slice(file.size - 128).arrayBuffer(); - const tag = new TextDecoder().decode(new Uint8Array(tailBuffer, 0, 3)); - if (tag === 'TAG') { - const title = new TextDecoder() - .decode(new Uint8Array(tailBuffer, 3, 30)) - .replace(/\0/g, '') - .trim(); - const artist = new TextDecoder() - .decode(new Uint8Array(tailBuffer, 33, 30)) - .replace(/\0/g, '') - .trim(); - const album = new TextDecoder() - .decode(new Uint8Array(tailBuffer, 63, 30)) - .replace(/\0/g, '') - .trim(); - if (title) metadata.title = title; - if (artist && metadata.artists.length === 0) { - metadata.artists = [{ name: artist }]; - } - if (album) metadata.album.title = album; - } - } -} - -// since mp3 file don't have metadata about duration, estimating it -// uses evil bitwise magic -async function calculateMp3Duration(file, startOffset) { - const buffer = await file.slice(startOffset, startOffset + 32768).arrayBuffer(); - const view = new DataView(buffer); - const uint8 = new Uint8Array(buffer); - - let offset = 0; - - // finding sync word - while (offset < view.byteLength - 4 && !(uint8[offset] === 0xff && (uint8[offset + 1] & 0xe0) === 0xe0)) { - offset++; - } - if (offset >= view.byteLength - 4) return 0; - - const header = view.getUint32(offset, false); - - // header info - const mpegVer = (header >> 19) & 3; - const brIdx = (header >> 12) & 15; - const srIdx = (header >> 10) & 3; - - // Reject invalid headers - if (mpegVer === 1 || brIdx === 0 || brIdx === 15 || srIdx === 3) return 0; - - const sampleRates = [[11025, 12000, 8000], null, [22050, 24000, 16000], [44100, 48000, 32000]]; - const brMpeg1 = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0]; - const brMpeg2 = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]; - - const sampleRate = sampleRates[mpegVer][srIdx]; - const bitrate = mpegVer === 3 ? brMpeg1[brIdx] : brMpeg2[brIdx]; - - // this xing header is present in many mp3 files and contains total frame count, which allows for accurate duration calculation - const channelMode = (header >> 6) & 3; // mono or stereo - const xingOffset = offset + 4 + (mpegVer === 3 ? (channelMode === 3 ? 17 : 32) : channelMode === 3 ? 9 : 17); // the position of xing header - - if (xingOffset + 8 <= view.byteLength) { - const sig = view.getUint32(xingOffset, false); - if ((sig === 0x58696e67 || sig === 0x496e666f) && view.getUint32(xingOffset + 4, false) & 1) { - const frames = view.getUint32(xingOffset + 8, false); - // basically, duration = frames * samples per frame / sample rate - return (frames * (mpegVer === 3 ? 1152 : 576)) / sampleRate; - } - } - - // if no Xing header, estimate duration from file size and bitrate - return ((file.size - startOffset) * 8) / (bitrate * 1000); -} - -function readSynchsafeInteger32(view, offset) { - return ( - ((view.getUint8(offset) & 0x7f) << 21) | - ((view.getUint8(offset + 1) & 0x7f) << 14) | - ((view.getUint8(offset + 2) & 0x7f) << 7) | - (view.getUint8(offset + 3) & 0x7f) - ); -} - -function readID3Text(view) { - const encoding = view.getUint8(0); - const buffer = view.buffer.slice(view.byteOffset + 1, view.byteOffset + view.byteLength); - let decoder; - if (encoding === 0) decoder = new TextDecoder('iso-8859-1'); - else if (encoding === 1) decoder = new TextDecoder('utf-16'); - else if (encoding === 2) decoder = new TextDecoder('utf-16be'); - else decoder = new TextDecoder('utf-8'); - - return decoder.decode(buffer).replace(/\0/g, ''); -} - -function getMimeType(data) { - if (data.length >= 2 && data[0] === 0xff && data[1] === 0xd8) return 'image/jpeg'; - if (data.length >= 8 && data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) - return 'image/png'; - return 'image/jpeg'; -} - -function isFlacFile(dataView) { - // Check for "fLaC" signature at the beginning - return ( - dataView.byteLength >= 4 && - dataView.getUint8(0) === 0x66 && // 'f' - dataView.getUint8(1) === 0x4c && // 'L' - dataView.getUint8(2) === 0x61 && // 'a' - dataView.getUint8(3) === 0x43 - ); // 'C' -} - -function parseFlacBlocks(dataView) { - const blocks = []; - let offset = 4; // Skip "fLaC" signature - - while (offset + 4 <= dataView.byteLength) { - const header = dataView.getUint8(offset); - const isLast = (header & 0x80) !== 0; - const blockType = header & 0x7f; - - // Block type 127 is invalid, types > 6 are reserved (except 127) - // Valid types: 0=STREAMINFO, 1=PADDING, 2=APPLICATION, 3=SEEKTABLE, 4=VORBIS_COMMENT, 5=CUESHEET, 6=PICTURE - if (blockType === 127) { - console.warn('Encountered invalid block type 127, stopping parse'); - break; - } - - const blockSize = - (dataView.getUint8(offset + 1) << 16) | - (dataView.getUint8(offset + 2) << 8) | - dataView.getUint8(offset + 3); - - // Validate block size - if (blockSize < 0 || offset + 4 + blockSize > dataView.byteLength) { - console.warn(`Invalid block size ${blockSize} at offset ${offset}, stopping parse`); - break; - } - - blocks.push({ - type: blockType, - isLast: isLast, - size: blockSize, - offset: offset + 4, - headerOffset: offset, - }); - - offset += 4 + blockSize; - - if (isLast) { - // Save the audio data offset - blocks.audioDataOffset = offset; - break; - } - } - - // If we didn't find the last block marker, estimate audio offset - if (blocks.audioDataOffset === undefined && blocks.length > 0) { - const lastBlock = blocks[blocks.length - 1]; - blocks.audioDataOffset = lastBlock.headerOffset + 4 + lastBlock.size; - console.warn('No last-block marker found, estimated audio offset:', blocks.audioDataOffset); - } - - return blocks; -} - -function parseMp4Atoms(dataView) { - const atoms = []; - let offset = 0; - - while (offset + 8 <= dataView.byteLength) { - // MP4 atoms use big-endian byte order - let size = dataView.getUint32(offset, false); - - // Handle special size values - if (size === 0) { - // Size 0 means the atom extends to the end of the file - size = dataView.byteLength - offset; - } else if (size === 1) { - // Size 1 means 64-bit extended size follows (after the type field) - if (offset + 16 > dataView.byteLength) { - break; - } - // Read 64-bit size from offset+8 (big-endian) - const sizeHigh = dataView.getUint32(offset + 8, false); - const sizeLow = dataView.getUint32(offset + 12, false); - if (sizeHigh !== 0) { - console.warn('64-bit MP4 atoms larger than 4GB are not supported - file may be processed incompletely'); - break; - } - size = sizeLow; - } - - if (size < 8 || offset + size > dataView.byteLength) { - break; - } - - const type = String.fromCharCode( - dataView.getUint8(offset + 4), - dataView.getUint8(offset + 5), - dataView.getUint8(offset + 6), - dataView.getUint8(offset + 7) - ); - - atoms.push({ - type: type, - offset: offset, - size: size, - }); - - offset += size; - } - - return atoms; -} diff --git a/js/metadata.mp3.js b/js/metadata.mp3.js new file mode 100644 index 0000000..cc9eb3d --- /dev/null +++ b/js/metadata.mp3.js @@ -0,0 +1,346 @@ +import { getCoverBlob, getTrackTitle } from './utils.js'; + +export async function writeID3v2Tag(mp3Blob, metadata, coverBlob = null) { + const frames = []; + + if (metadata.title) { + frames.push(createTextFrame('TIT2', getTrackTitle(metadata))); + } + + const artistName = metadata.artist?.name || metadata.artists?.[0]?.name; + if (artistName) { + frames.push(createTextFrame('TPE1', artistName)); + } + + if (metadata.album?.title) { + frames.push(createTextFrame('TALB', metadata.album.title)); + } + + const albumArtistName = metadata.album?.artist?.name || metadata.artist?.name || metadata.artists?.[0]?.name; + if (albumArtistName) { + frames.push(createTextFrame('TPE2', albumArtistName)); + } + + if (metadata.trackNumber) { + frames.push(createTextFrame('TRCK', metadata.trackNumber.toString())); + } + + if (metadata.album?.releaseDate) { + const year = new Date(metadata.album.releaseDate).getFullYear(); + if (!Number.isNaN(year) && Number.isFinite(year)) { + frames.push(createTextFrame('TYER', year.toString())); + } + } + + if (metadata.isrc) { + frames.push(createTextFrame('TSRC', metadata.isrc)); + } + + if (metadata.copyright) { + frames.push(createTextFrame('TCOP', metadata.copyright)); + } + + frames.push(createTextFrame('TENC', 'Monochrome')); + + if (coverBlob) { + frames.push(await createAPICFrame(coverBlob)); + } + + return buildID3v2Tag(mp3Blob, frames); +} + +export function createTextFrame(frameId, text) { + // ID3v2.3 UTF-16 encoding with BOM + const bom = new Uint8Array([0xff, 0xfe]); // UTF-16LE BOM + const utf16Bytes = new Uint8Array(text.length * 2); + + for (let i = 0; i < text.length; i++) { + const charCode = text.charCodeAt(i); + utf16Bytes[i * 2] = charCode & 0xff; + utf16Bytes[i * 2 + 1] = (charCode >> 8) & 0xff; + } + + const frameSize = 1 + bom.length + utf16Bytes.length; + const frame = new Uint8Array(10 + frameSize); + const view = new DataView(frame.buffer); + + for (let i = 0; i < 4; i++) { + frame[i] = frameId.charCodeAt(i); + } + + view.setUint32(4, frameSize, false); + + frame[10] = 0x01; // UTF-16 with BOM + + frame.set(bom, 11); + frame.set(utf16Bytes, 11 + bom.length); + + return frame; +} + +export async function createAPICFrame(coverBlob) { + const imageBytes = new Uint8Array(await coverBlob.arrayBuffer()); + const mimeType = coverBlob.type || 'image/jpeg'; + const mimeBytes = new TextEncoder().encode(mimeType); + + const frameSize = 1 + mimeBytes.length + 1 + 1 + 1 + imageBytes.length; + + const frame = new Uint8Array(10 + frameSize); + const view = new DataView(frame.buffer); + + for (let i = 0; i < 4; i++) { + frame[i] = 'APIC'.charCodeAt(i); + } + + view.setUint32(4, frameSize, false); + + let offset = 10; + frame[offset++] = 0x00; + + frame.set(mimeBytes, offset); + offset += mimeBytes.length; + frame[offset++] = 0x00; + + frame[offset++] = 0x03; + + frame[offset++] = 0x00; + + frame.set(imageBytes, offset); + + return frame; +} + +export function buildID3v2Tag(mp3Blob, frames) { + const framesData = new Uint8Array(frames.reduce((acc, f) => acc + f.length, 0)); + let offset = 0; + for (const frame of frames) { + framesData.set(frame, offset); + offset += frame.length; + } + + const tagSize = framesData.length; + + const header = new Uint8Array(10); + header[0] = 0x49; + header[1] = 0x44; + header[2] = 0x33; + header[3] = 0x03; + header[4] = 0x00; + header[5] = 0x00; + + header[6] = (tagSize >> 21) & 0x7f; + header[7] = (tagSize >> 14) & 0x7f; + header[8] = (tagSize >> 7) & 0x7f; + header[9] = tagSize & 0x7f; + + return new Blob([header, framesData, mp3Blob], { type: 'audio/mpeg' }); +} + +export async function addMp3Metadata(mp3Blob, track, api) { + try { + let coverBlob = null; + + if (track.album?.cover) { + try { + coverBlob = await getCoverBlob(api, track.album.cover); + } catch (error) { + console.warn('Failed to fetch album art for MP3:', error); + } + } + + return await writeID3v2Tag(mp3Blob, track, coverBlob); + } catch (error) { + console.error('Failed to add MP3 metadata:', error); + return mp3Blob; + } +} + +export async function readMp3Metadata(file, metadata) { + let buffer = await file.slice(0, 10).arrayBuffer(); + let view = new DataView(buffer); + + if (view.getUint8(0) === 0x49 && view.getUint8(1) === 0x44 && view.getUint8(2) === 0x33) { + const majorVer = view.getUint8(3); + const size = readSynchsafeInteger32(view, 6); + const tagSize = size + 10; + + buffer = await file.slice(0, tagSize).arrayBuffer(); + view = new DataView(buffer); + + let offset = 10; + if ((view.getUint8(5) & 0x40) !== 0) { + const extSize = readSynchsafeInteger32(view, offset); + offset += extSize; + } + + let tpe1 = null; + let tpe2 = null; + while (offset < view.byteLength) { + let frameId, frameSize; + + if (majorVer === 3) { + frameId = new TextDecoder().decode(new Uint8Array(buffer, offset, 4)); + frameSize = view.getUint32(offset + 4, false); + offset += 10; + } else if (majorVer === 4) { + frameId = new TextDecoder().decode(new Uint8Array(buffer, offset, 4)); + frameSize = readSynchsafeInteger32(view, offset + 4); + offset += 10; + } else { + break; + } + + if (frameId.charCodeAt(0) === 0) break; + if (offset + frameSize > view.byteLength) break; + + const frameData = new DataView(buffer, offset, frameSize); + if (frameId === 'TIT2') metadata.title = readID3Text(frameData); + if (frameId === 'TPE1') tpe1 = readID3Text(frameData); + if (frameId === 'TPE2') tpe2 = readID3Text(frameData); + if (frameId === 'TALB') metadata.album.title = readID3Text(frameData); + if (frameId === 'TSRC') metadata.isrc = readID3Text(frameData); + if (frameId === 'TCOP') metadata.copyright = readID3Text(frameData); + if (frameId === 'TLEN') metadata.duration = parseInt(readID3Text(frameData)) / 1000; // usually not present + if (frameId === 'TYER' || frameId === 'TDRC') { + const year = readID3Text(frameData); + if (year) metadata.album.releaseDate = year; + } + if (frameId === 'APIC') { + try { + const encoding = frameData.getUint8(0); + let mimeType = ''; + let pos = 1; + while (pos < frameData.byteLength && frameData.getUint8(pos) !== 0) { + mimeType += String.fromCharCode(frameData.getUint8(pos)); + pos++; + } + pos++; + pos++; + let terminator = encoding === 1 || encoding === 2 ? 2 : 1; + while (pos < frameData.byteLength) { + if (frameData.getUint8(pos) === 0) { + if (terminator === 1) { + pos++; + break; + } else if (pos + 1 < frameData.byteLength && frameData.getUint8(pos + 1) === 0) { + pos += 2; + break; + } + } + pos++; + } + const pictureData = new Uint8Array(buffer, offset + pos, frameSize - pos); + const blob = new Blob([pictureData], { type: mimeType || 'image/jpeg' }); + metadata.album.cover = URL.createObjectURL(blob); + } catch (e) { + console.warn('Error parsing APIC:', e); + } + } + + offset += frameSize; + } + + const artistStr = tpe1 || tpe2; + if (artistStr) { + metadata.artists = artistStr.split('/').map((name) => ({ name: name.trim() })); + } + + if (!metadata.duration || metadata.duration === 0) { + metadata.duration = await calculateMp3Duration(file, tagSize); + } + } + + if (file.size > 128) { + const tailBuffer = await file.slice(file.size - 128).arrayBuffer(); + const tag = new TextDecoder().decode(new Uint8Array(tailBuffer, 0, 3)); + if (tag === 'TAG') { + const title = new TextDecoder() + .decode(new Uint8Array(tailBuffer, 3, 30)) + .replace(/\0/g, '') + .trim(); + const artist = new TextDecoder() + .decode(new Uint8Array(tailBuffer, 33, 30)) + .replace(/\0/g, '') + .trim(); + const album = new TextDecoder() + .decode(new Uint8Array(tailBuffer, 63, 30)) + .replace(/\0/g, '') + .trim(); + if (title) metadata.title = title; + if (artist && metadata.artists.length === 0) { + metadata.artists = [{ name: artist }]; + } + if (album) metadata.album.title = album; + } + } +} + +// since mp3 file don't have metadata about duration, estimating it +// uses evil bitwise magic +export async function calculateMp3Duration(file, startOffset) { + const buffer = await file.slice(startOffset, startOffset + 32768).arrayBuffer(); + const view = new DataView(buffer); + const uint8 = new Uint8Array(buffer); + + let offset = 0; + + // finding sync word + while (offset < view.byteLength - 4 && !(uint8[offset] === 0xff && (uint8[offset + 1] & 0xe0) === 0xe0)) { + offset++; + } + if (offset >= view.byteLength - 4) return 0; + + const header = view.getUint32(offset, false); + + // header info + const mpegVer = (header >> 19) & 3; + const brIdx = (header >> 12) & 15; + const srIdx = (header >> 10) & 3; + + // Reject invalid headers + if (mpegVer === 1 || brIdx === 0 || brIdx === 15 || srIdx === 3) return 0; + + const sampleRates = [[11025, 12000, 8000], null, [22050, 24000, 16000], [44100, 48000, 32000]]; + const brMpeg1 = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0]; + const brMpeg2 = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]; + + const sampleRate = sampleRates[mpegVer][srIdx]; + const bitrate = mpegVer === 3 ? brMpeg1[brIdx] : brMpeg2[brIdx]; + + // this xing header is present in many mp3 files and contains total frame count, which allows for accurate duration calculation + const channelMode = (header >> 6) & 3; // mono or stereo + const xingOffset = offset + 4 + (mpegVer === 3 ? (channelMode === 3 ? 17 : 32) : channelMode === 3 ? 9 : 17); // the position of xing header + + if (xingOffset + 8 <= view.byteLength) { + const sig = view.getUint32(xingOffset, false); + if ((sig === 0x58696e67 || sig === 0x496e666f) && view.getUint32(xingOffset + 4, false) & 1) { + const frames = view.getUint32(xingOffset + 8, false); + // basically, duration = frames * samples per frame / sample rate + return (frames * (mpegVer === 3 ? 1152 : 576)) / sampleRate; + } + } + + // if no Xing header, estimate duration from file size and bitrate + return ((file.size - startOffset) * 8) / (bitrate * 1000); +} + +export function readSynchsafeInteger32(view, offset) { + return ( + ((view.getUint8(offset) & 0x7f) << 21) | + ((view.getUint8(offset + 1) & 0x7f) << 14) | + ((view.getUint8(offset + 2) & 0x7f) << 7) | + (view.getUint8(offset + 3) & 0x7f) + ); +} + +export function readID3Text(view) { + const encoding = view.getUint8(0); + const buffer = view.buffer.slice(view.byteOffset + 1, view.byteOffset + view.byteLength); + let decoder; + if (encoding === 0) decoder = new TextDecoder('iso-8859-1'); + else if (encoding === 1) decoder = new TextDecoder('utf-16'); + else if (encoding === 2) decoder = new TextDecoder('utf-16be'); + else decoder = new TextDecoder('utf-8'); + + return decoder.decode(buffer).replace(/\0/g, ''); +} diff --git a/js/metadata.mp4.js b/js/metadata.mp4.js new file mode 100644 index 0000000..21e6682 --- /dev/null +++ b/js/metadata.mp4.js @@ -0,0 +1,846 @@ +import { getCoverBlob, getTrackTitle, getMimeType, getFullArtistString } from './utils.js'; +import { METADATA_STRINGS } from './metadata.js'; + +const { DEFAULT_TITLE, DEFAULT_ARTIST, DEFAULT_ALBUM } = METADATA_STRINGS; + +export async function readM4aMetadata(file, metadata) { + try { + const chunkSize = Math.min(file.size, 5 * 1024 * 1024); + const buffer = await file.slice(0, chunkSize).arrayBuffer(); + const view = new DataView(buffer); + + const atoms = parseMp4Atoms(view); + + const moov = atoms.find((a) => a.type === 'moov'); + if (!moov) return; + + const moovStart = moov.offset + 8; + const moovLen = moov.size - 8; + const moovData = new DataView(view.buffer, moovStart, moovLen); + const moovAtoms = parseMp4Atoms(moovData); + + // mvhd metadata tag + const mvhd = moovAtoms.find((a) => a.type === 'mvhd'); + if (mvhd) { + const mvhdStart = moovStart + mvhd.offset + 8; + const version = view.getUint8(mvhdStart); + + // resolution and length, basically + let timeScale, duration; + + if (version === 0) { + // 32-bit format + timeScale = view.getUint32(mvhdStart + 12, false); + duration = view.getUint32(mvhdStart + 16, false); + } else if (version === 1) { + // 64-bit format + timeScale = view.getUint32(mvhdStart + 20, false); + const durHigh = view.getUint32(mvhdStart + 24, false); + const durLow = view.getUint32(mvhdStart + 28, false); + duration = durHigh * 0x100000000 + durLow; + } + + if (timeScale > 0) { + metadata.duration = duration / timeScale; + } + } + + const udta = moovAtoms.find((a) => a.type === 'udta'); + if (!udta) return; + + const udtaStart = moovStart + udta.offset + 8; + const udtaLen = udta.size - 8; + const udtaData = new DataView(view.buffer, udtaStart, udtaLen); + const udtaAtoms = parseMp4Atoms(udtaData); + + const meta = udtaAtoms.find((a) => a.type === 'meta'); + if (!meta) return; + + const metaStart = udtaStart + meta.offset + 12; + const metaLen = meta.size - 12; + const metaData = new DataView(view.buffer, metaStart, metaLen); + const metaAtoms = parseMp4Atoms(metaData); + + const ilst = metaAtoms.find((a) => a.type === 'ilst'); + if (!ilst) return; + + const ilstStart = metaStart + ilst.offset + 8; + const ilstLen = ilst.size - 8; + const ilstData = new DataView(view.buffer, ilstStart, ilstLen); + const items = parseMp4Atoms(ilstData); + + let artistStr = null; + + for (const item of items) { + const itemStart = ilstStart + item.offset + 8; + const itemLen = item.size - 8; + const itemData = new DataView(view.buffer, itemStart, itemLen); + const dataAtom = parseMp4Atoms(itemData).find((a) => a.type === 'data'); + if (dataAtom) { + const contentLen = dataAtom.size - 16; + const contentOffset = itemStart + dataAtom.offset + 16; + + if (item.type === '©nam') { + metadata.title = new TextDecoder().decode(new Uint8Array(view.buffer, contentOffset, contentLen)); + } else if (item.type === '©ART') { + artistStr = new TextDecoder().decode(new Uint8Array(view.buffer, contentOffset, contentLen)); + } else if (item.type === '©alb') { + metadata.album.title = new TextDecoder().decode( + new Uint8Array(view.buffer, contentOffset, contentLen) + ); + } else if (item.type === 'ISRC') { + metadata.isrc = new TextDecoder().decode(new Uint8Array(view.buffer, contentOffset, contentLen)); + } else if (item.type === 'cprt') { + metadata.copyright = new TextDecoder().decode( + new Uint8Array(view.buffer, contentOffset, contentLen) + ); + } else if (item.type === 'covr') { + const pictureData = new Uint8Array(view.buffer, contentOffset, contentLen); + const mime = getMimeType(pictureData); + const blob = new Blob([pictureData], { type: mime }); + metadata.album.cover = URL.createObjectURL(blob); + } else if (item.type === 'rtng') { + metadata.explicit = + contentLen > 0 && new Uint8Array(view.buffer, contentOffset, contentLen)[0] === 1; + } + } + } + + if (artistStr) { + metadata.artists = artistStr.split(/; |\/|\\/).map((name) => ({ name: name.trim() })); + } + } catch (e) { + console.warn('Error parsing M4A:', e); + } +} + +/** + * Adds metadata to M4A files using MP4 atoms + */ +export async function addM4aMetadata(m4aBlob, track, api) { + try { + const arrayBuffer = await m4aBlob.arrayBuffer(); + const dataView = new DataView(arrayBuffer); + + // Parse MP4 atoms + const atoms = parseMp4Atoms(dataView); + + // Create metadata atoms + const metadataAtoms = createMp4MetadataAtoms(track); + + // Fetch album artwork if available + if (track.album?.cover) { + try { + const imageBlob = await getCoverBlob(api, track.album.cover); + if (imageBlob) { + const imageBytes = new Uint8Array(await imageBlob.arrayBuffer()); + metadataAtoms.cover = { + type: 'covr', + data: imageBytes, + }; + } + } catch (error) { + console.warn('Failed to embed album art in M4A:', error); + } + } + + // Rebuild MP4 file with metadata + const newMp4Data = rebuildMp4WithMetadata(dataView, atoms, metadataAtoms); + + return new Blob([newMp4Data], { type: 'audio/mp4' }); + } catch (error) { + console.error('Failed to add M4A metadata:', error); + return m4aBlob; + } +} + +export function parseMp4Atoms(dataView) { + const atoms = []; + let offset = 0; + + while (offset + 8 <= dataView.byteLength) { + // MP4 atoms use big-endian byte order + let size = dataView.getUint32(offset, false); + + // Handle special size values + if (size === 0) { + // Size 0 means the atom extends to the end of the file + size = dataView.byteLength - offset; + } else if (size === 1) { + // Size 1 means 64-bit extended size follows (after the type field) + if (offset + 16 > dataView.byteLength) { + break; + } + // Read 64-bit size from offset+8 (big-endian) + const sizeHigh = dataView.getUint32(offset + 8, false); + const sizeLow = dataView.getUint32(offset + 12, false); + if (sizeHigh !== 0) { + console.warn('64-bit MP4 atoms larger than 4GB are not supported - file may be processed incompletely'); + break; + } + size = sizeLow; + } + + if (size < 8 || offset + size > dataView.byteLength) { + break; + } + + const type = String.fromCharCode( + dataView.getUint8(offset + 4), + dataView.getUint8(offset + 5), + dataView.getUint8(offset + 6), + dataView.getUint8(offset + 7) + ); + + atoms.push({ + type: type, + offset: offset, + size: size, + }); + + offset += size; + } + + return atoms; +} + +export function createMp4MetadataAtoms(track) { + // MP4 metadata atoms are more complex than FLAC + // We'll create basic iTunes-style metadata + + /** + * Array of arrays: [namespace, name, value] + */ + const userTags = []; + const tags = { + '©nam': getTrackTitle(track) || DEFAULT_TITLE, + '©ART': getFullArtistString(track) || DEFAULT_ARTIST, + '©alb': track.album?.title || DEFAULT_ALBUM, + aART: track.album?.artist?.name || track.artist?.name || DEFAULT_ARTIST, + }; + + if (track.isrc) { + tags['ISRC'] = track.isrc; + tags['xid '] = ':isrc:' + track.isrc; + } + + if (track.copyright) { + tags['cprt'] = track.copyright; + } + + if (track.trackNumber) { + tags['trkn'] = { + current: track.trackNumber, + total: track.album?.numberOfTracks, + }; + } + if (track.explicit) { + tags['rtng'] = 1; // 1 = Explicit, 2 = Clean, 0 = Unknown + } + + const discNumber = track.volumeNumber ?? track.discNumber; + if (discNumber) { + tags['disk'] = { + current: discNumber, + total: 0, + }; + } + + if (track.bpm) { + tags['tmpo'] = Math.round(track.bpm); + } + + const releaseDateStr = + track.album?.releaseDate || (track.streamStartDate ? track.streamStartDate.split('T')[0] : ''); + if (releaseDateStr) { + try { + const year = new Date(releaseDateStr).getFullYear(); + if (!isNaN(year)) { + tags['©day'] = String(year); + } + } catch { + // Invalid date, skip + } + } + + if (track.replayGain) { + const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = track.replayGain; + let trackPeakAmplitudeString = String(trackPeakAmplitude); + let albumPeakAmplitudeString = String(albumPeakAmplitude); + + if (trackPeakAmplitudeString.indexOf('.') === -1) { + trackPeakAmplitudeString += '.000000'; + } + if (albumPeakAmplitudeString.indexOf('.') === -1) { + albumPeakAmplitudeString += '.000000'; + } + + if (trackPeakAmplitude) userTags.push(['com.apple.iTunes', 'replaygain_track_peak', trackPeakAmplitudeString]); + if (trackReplayGain) userTags.push(['com.apple.iTunes', 'replaygain_track_gain', `${trackReplayGain} dB`]); + if (albumPeakAmplitude) userTags.push(['com.apple.iTunes', 'replaygain_album_peak', albumPeakAmplitudeString]); + if (albumReplayGain) userTags.push(['com.apple.iTunes', 'replaygain_album_gain', `${albumReplayGain} dB`]); + } + + return { tags, userTags }; +} + +export function rebuildMp4WithMetadata(dataView, atoms, metadataAtoms) { + const originalArray = new Uint8Array(dataView.buffer); + + // Find moov atom + const moovAtom = atoms.find((a) => a.type === 'moov'); + if (!moovAtom) { + console.warn('No moov atom found in M4A file'); + return originalArray; + } + + // Construct the new metadata block (udta -> meta -> ilst) + const newMetadataBytes = createMetadataBlock(metadataAtoms); + + // We need to insert this into the moov atom. + // If udta exists, we merge/replace. For simplicity, we'll append/create. + // Ideally, we should parse moov children to find udta. + + // 1. Calculate new sizes + // New file size = Original size + Metadata block size + // Note: If we are replacing existing metadata, this calculation would be different, + // but here we are assuming we are adding fresh or appending. + // A robust implementation would parse moov children, remove existing udta, and add new one. + + // Let's try to do it right: parse moov children + const moovChildren = parseMp4Atoms(new DataView(originalArray.buffer, moovAtom.offset + 8, moovAtom.size - 8)); + + // Filter out existing udta to replace it + const filteredMoovChildren = moovChildren.filter((a) => a.type !== 'udta'); + + // Calculate new moov size + // Header (8) + Sum of other children sizes + New Metadata Block Size + let newMoovSize = 8; + for (const child of filteredMoovChildren) { + newMoovSize += child.size; + } + newMoovSize += newMetadataBytes.length; + + const sizeDiff = newMoovSize - moovAtom.size; + const newFileSize = originalArray.length + sizeDiff; + + const newFile = new Uint8Array(newFileSize); + let offset = 0; + let originalOffset = 0; + + // Copy atoms before moov + const atomsBeforeMoov = atoms.filter((a) => a.offset < moovAtom.offset); + for (const atom of atomsBeforeMoov) { + newFile.set(originalArray.subarray(atom.offset, atom.offset + atom.size), offset); + offset += atom.size; + originalOffset += atom.size; + } + + // Write new moov atom + // Size + newFile[offset++] = (newMoovSize >> 24) & 0xff; + newFile[offset++] = (newMoovSize >> 16) & 0xff; + newFile[offset++] = (newMoovSize >> 8) & 0xff; + newFile[offset++] = newMoovSize & 0xff; + + // Type 'moov' + newFile[offset++] = 0x6d; + newFile[offset++] = 0x6f; + newFile[offset++] = 0x6f; + newFile[offset++] = 0x76; + + // Write preserved children of moov + for (const child of filteredMoovChildren) { + const absoluteChildStart = moovAtom.offset + 8 + child.offset; + newFile.set(originalArray.subarray(absoluteChildStart, absoluteChildStart + child.size), offset); + offset += child.size; + } + + // Write new metadata block (udta) + newFile.set(newMetadataBytes, offset); + offset += newMetadataBytes.length; + + // Update originalOffset to skip old moov + originalOffset = moovAtom.offset + moovAtom.size; + + // Copy atoms after moov + // Adjust offsets in stco/co64 atoms if necessary? + // Changing the size of moov (or atoms before mdat) shifts the mdat offsets. + // If moov comes before mdat, we MUST update the Chunk Offset Atom (stco or co64). + // This is complex. + + // Safe strategy: If moov is AFTER mdat, we don't need to update offsets. + // If moov is BEFORE mdat, we need to shift offsets. + // Most streaming optimized files have moov before mdat. + + const mdatAtom = atoms.find((a) => a.type === 'mdat'); + const moovBeforeMdat = mdatAtom && moovAtom.offset < mdatAtom.offset; + + if (moovBeforeMdat) { + // We need to update stco/co64 atoms inside the copied moov children content in newFile. + // This is getting very complicated for a simple "add metadata" feature without a proper library. + // However, we can try to find 'stco' or 'co64' in the new buffer we just wrote and offset values. + + // Let's assume we need to shift by sizeDiff. + updateChunkOffsets(newFile, offset - newMoovSize, newMoovSize, sizeDiff); + } + + // Copy remaining data (mdat etc.) + if (originalOffset < originalArray.length) { + newFile.set(originalArray.subarray(originalOffset), offset); + } + + return newFile; +} + +export function createMetadataBlock(metadataAtoms) { + const { tags, userTags, cover } = metadataAtoms; + + const ilstChildren = []; + + // Text tags + for (const [key, value] of Object.entries(tags)) { + if (key === 'trkn' || key === 'disk') { + ilstChildren.push(createIntAtom(key, value)); + } else if (key === 'rtng') { + ilstChildren.push(createUintAtom(key, value, 1)); + } else if (key === 'tmpo') { + ilstChildren.push(createUintAtom(key, value, 2)); + } else { + ilstChildren.push(createStringAtom(key, value)); + } + } + + // User tags + for (const [namespace, name, value] of userTags) { + ilstChildren.push(createUserAtom(namespace, name, value)); + } + + // Cover art + if (cover) { + ilstChildren.push(createCoverAtom(cover.data)); + } + + // Construct ilst atom + const ilstSize = 8 + ilstChildren.reduce((acc, buf) => acc + buf.length, 0); + const ilst = new Uint8Array(ilstSize); + let offset = 0; + + writeAtomHeader(ilst, offset, ilstSize, 'ilst'); + offset += 8; + + for (const child of ilstChildren) { + ilst.set(child, offset); + offset += child.length; + } + + // Construct meta atom (FullAtom, version+flags = 4 bytes) + const metaSize = 12 + ilstSize; + const meta = new Uint8Array(metaSize); + offset = 0; + + writeAtomHeader(meta, offset, metaSize, 'meta'); + offset += 8; + + meta[offset++] = 0; // Version + meta[offset++] = 0; // Flags + meta[offset++] = 0; + meta[offset++] = 0; + + meta.set(ilst, offset); + + // Construct hdlr atom (required for meta) + // "mdir" subtype, "appl" manufacturer, 0 flags/masks, empty name + // hdlr size: 4 (size) + 4 (type) + 4 (ver/flags) + 4 (pre_defined) + 4 (handler_type) + 12 (reserved) + name (string) + // Minimal valid hdlr for iTunes metadata: + const hdlrContent = new Uint8Array([ + 0, + 0, + 0, + 0, // Version/Flags + 0, + 0, + 0, + 0, // Pre-defined + 0x6d, + 0x64, + 0x69, + 0x72, // 'mdir' + 0x61, + 0x70, + 0x70, + 0x6c, // 'appl' + 0, + 0, + 0, + 0, // Reserved + 0, + 0, + 0, + 0, + 0, + 0, // Name (empty null-term) check spec? usually simple 0 is enough + ]); + const hdlrSize = 8 + hdlrContent.length; + const hdlr = new Uint8Array(hdlrSize); + writeAtomHeader(hdlr, 0, hdlrSize, 'hdlr'); + hdlr.set(hdlrContent, 8); + + // Construct udta atom + // udta contains meta. meta usually should contain hdlr before ilst? + // Actually, QuickTime spec says meta contains hdlr then ilst. + + const finalMetaSize = 12 + hdlrSize + ilstSize; + const finalMeta = new Uint8Array(finalMetaSize); + offset = 0; + writeAtomHeader(finalMeta, offset, finalMetaSize, 'meta'); + offset += 8; + finalMeta[offset++] = 0; // Version + finalMeta[offset++] = 0; // Flags + finalMeta[offset++] = 0; + finalMeta[offset++] = 0; + + finalMeta.set(hdlr, offset); + offset += hdlrSize; + finalMeta.set(ilst, offset); + + const udtaSize = 8 + finalMetaSize; + const udta = new Uint8Array(udtaSize); + writeAtomHeader(udta, 0, udtaSize, 'udta'); + udta.set(finalMeta, 8); + + return udta; +} + +export function createStringAtom(type, value, truncateType = true) { + const typeLength = truncateType ? 4 : type.length; + const textBytes = new TextEncoder().encode(value); + const dataSize = 16 + textBytes.length; // 8 (data atom header) + 8 (flags/null) + text + const atomSize = 4 + typeLength + dataSize; + + const buf = new Uint8Array(atomSize); + let offset = 0; + + // Wrapper atom (e.g., ©nam) + writeAtomHeader(buf, offset, atomSize, type, truncateType); + offset += 4 + typeLength; + + // Data atom + writeAtomHeader(buf, offset, dataSize, 'data'); + offset += 8; + + // Data Type (1 = UTF-8 text) + Locale (0) + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 1; // Type 1 + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + + buf.set(textBytes, offset); + + return buf; +} + +export function createUserAtom(namespace, name, value) { + const encoder = new TextEncoder(); + const dashBytes = encoder.encode('----'); // User-defined atom type + const namespaceBytes = encoder.encode(namespace); + const meanBytes = encoder.encode('mean'); // Standard 'mean' atom for namespace + const nameBytes = encoder.encode(name); + const valueBytes = encoder.encode('\x00\x00\x00\x01\x00\x00\x00\x00' + value); + + /** + * Atom structure: + * [----] (atom header) + * [mean] (namespace) + * [name] (name) + * [data] (value) + */ + const atomSize = 8 + 12 + namespaceBytes.length + 12 + nameBytes.length + 8 + valueBytes.length; + + const buf = new Uint8Array(atomSize); + let offset = 0; + writeAtomHeader(buf, offset, atomSize, '----'); + offset += 8; // Skip header + writeAtomHeader(buf, offset, namespaceBytes.length + 12, 'mean'); + offset += 12; + buf.set(namespaceBytes, offset); + offset += namespaceBytes.length; + writeAtomHeader(buf, offset, nameBytes.length + 12, 'name'); + offset += 12; + buf.set(nameBytes, offset); + offset += nameBytes.length; + writeAtomHeader(buf, offset, valueBytes.length + 8, 'data'); + offset += 8; + buf.set(valueBytes, offset); + + return buf; +} + +/** + * Converts a number or BigInt value to a big-endian byte array. + * @param {number|BigInt|null} value - The value to convert to bytes. If null, returns null. + * @param {number|null} [byteLength=null] - Optional fixed byte length. If provided, the result will be padded or truncated to this length. If not provided, returns the minimal byte representation. + * @returns {Uint8Array} A Uint8Array representing the value in big-endian format, or null if value is null. + * @throws {Error} If the value is a negative number. + * @example + * // Variable length (minimal bytes) + * toBigEndianBytes(256); // Uint8Array [ 1, 0 ] + * toBigEndianBytes(0); // Uint8Array [ 0 ] + * + * // Fixed length with padding + * toBigEndianBytes(1, 4); // Uint8Array [ 0, 0, 0, 1 ] + * + * // With BigInt + * toBigEndianBytes(0xDEADBEEFn, 4); // Uint8Array [ 222, 173, 190, 239 ] + */ +export function toBigEndianBytes(value, byteLength = null) { + if (value == null) return new Uint8Array(0); + + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error('Value must be a non-negative safe integer.'); + } + + // Fixed-length mode + if (byteLength != null) { + const bytes = new Uint8Array(byteLength); + for (let i = byteLength - 1; i >= 0; i--) { + bytes[i] = value & 0xff; + value = Math.floor(value / 256); + } + return bytes; + } + + // Variable (minimal) mode + if (value === 0) return new Uint8Array([0]); + + const result = []; + while (value > 0) { + result.push(value & 0xff); + value = Math.floor(value / 256); + } + + result.reverse(); + + return new Uint8Array(result); +} + +export function createUintAtom(key, value, intByteLength = 1) { + const numberBytes = toBigEndianBytes(value, intByteLength); + const dataSize = 16 + intByteLength; // Atom header (8) + number bytes + const atomSize = 8 + dataSize; + + const buf = new Uint8Array(atomSize); + let offset = 0; + + // Wrapper atom (e.g., ©nam) + writeAtomHeader(buf, offset, atomSize, key); + offset += 8; + + // Data atom + writeAtomHeader(buf, offset, dataSize, 'data'); + offset += 8; + + // Data Type ((Big Endian Unsigned Integer) + Locale (0)) + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 21; // Type 21 + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf.set(numberBytes, offset++); + + return buf; +} + +export function createIntAtom(type, value) { + // trkn/disk are special: data is 8 bytes. + // reserved(2) + track(2) + total(2) + reserved(2) + const dataSize = 16 + 8; + const atomSize = 8 + dataSize; + + const buf = new Uint8Array(atomSize); + let offset = 0; + + writeAtomHeader(buf, offset, atomSize, type); + offset += 8; + + writeAtomHeader(buf, offset, dataSize, 'data'); + offset += 8; + + // Data Type (0 = implicit/int) + Locale + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; // Type 0 + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + + const current = typeof value === 'object' ? value.current : value; + const total = typeof value === 'object' ? value.total : 0; + + // Numbering payload (track/disc number + total) + buf[offset++] = 0; + buf[offset++] = 0; + const numberValue = parseInt(current, 10) || 0; + buf[offset++] = (numberValue >> 8) & 0xff; + buf[offset++] = numberValue & 0xff; + const totalValue = parseInt(total, 10) || 0; + buf[offset++] = (totalValue >> 8) & 0xff; + buf[offset++] = totalValue & 0xff; + buf[offset++] = 0; + buf[offset++] = 0; + + return buf; +} + +export function createCoverAtom(imageBytes) { + const dataSize = 16 + imageBytes.length; + const atomSize = 8 + dataSize; + + const buf = new Uint8Array(atomSize); + let offset = 0; + + writeAtomHeader(buf, offset, atomSize, 'covr'); + offset += 8; + + writeAtomHeader(buf, offset, dataSize, 'data'); + offset += 8; + + // Data Type (13 = JPEG, 14 = PNG) + // We try to detect or default to JPEG (13) + let type = 13; + if (imageBytes[0] === 0x89 && imageBytes[1] === 0x50) { + // PNG signature + type = 14; + } + + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = type; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + buf[offset++] = 0; + + buf.set(imageBytes, offset); + + return buf; +} + +/** + * Creates an atom header for MP4 metadata. + * @param {number} size - The size of the atom in bytes. + * @param {string} type - The 4-character atom type identifier. + * @param {boolean} [truncate=false] - Whether to truncate the type to 4 characters or use full length. + * @returns {Uint8Array} A byte array containing the atom header with size and type information. + */ +export function getAtomHeader(size, type, truncate = false) { + const buf = new Uint8Array(4 + (truncate ? 4 : type.length)); + buf[0] = (size >> 24) & 0xff; + buf[1] = (size >> 16) & 0xff; + buf[2] = (size >> 8) & 0xff; + buf[3] = size & 0xff; + + for (let i = 0; i < (truncate ? 4 : type.length); i++) { + buf[4 + i] = type.charCodeAt(i); + } + + return buf; +} + +/** + * Writes an atom header to a buffer at the specified offset. + * @param {Uint8Array} buf - The buffer to write the atom header to. + * @param {number} offset - The offset in the buffer where the atom header should be written. + * @param {number} size - The size of the atom. + * @param {string} type - The type of the atom (typically a 4-character code). + * @param {boolean} [truncate=true] - Whether to truncate the atom header. Defaults to true. + * @returns {void} + */ +export function writeAtomHeader(buf, offset, size, type, truncate = true) { + buf.set(getAtomHeader(size, type, truncate), offset); +} + +export function updateChunkOffsets(buffer, moovOffset, moovSize, shift) { + const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); + + // Scan moov for stco/co64 + // This is a naive recursive search restricted to the known moov range + + // We parse atoms starting from moov content + let offset = moovOffset + 8; // Skip moov header + const end = moovOffset + moovSize; + + findAndShiftOffsets(view, offset, end, shift); +} + +export function findAndShiftOffsets(view, start, end, shift) { + let offset = start; + + while (offset + 8 <= end) { + const size = view.getUint32(offset, false); + const type = String.fromCharCode( + view.getUint8(offset + 4), + view.getUint8(offset + 5), + view.getUint8(offset + 6), + view.getUint8(offset + 7) + ); + + if (size < 8) break; + + if (type === 'trak' || type === 'mdia' || type === 'minf' || type === 'stbl') { + // Container atoms, recurse + findAndShiftOffsets(view, offset + 8, offset + size, shift); + } else if (type === 'stco') { + // Chunk Offset Atom (32-bit) + // Header (8) + Version(1) + Flags(3) + Count(4) + Entries(Count * 4) + const count = view.getUint32(offset + 12, false); + for (let i = 0; i < count; i++) { + const entryOffset = offset + 16 + i * 4; + const oldVal = view.getUint32(entryOffset, false); + view.setUint32(entryOffset, oldVal + shift, false); + } + } else if (type === 'co64') { + // Chunk Offset Atom (64-bit) + // Header (8) + Version(1) + Flags(3) + Count(4) + Entries(Count * 8) + const count = view.getUint32(offset + 12, false); + for (let i = 0; i < count; i++) { + const entryOffset = offset + 16 + i * 8; + // Read 64-bit int + const oldHigh = view.getUint32(entryOffset, false); + const oldLow = view.getUint32(entryOffset + 4, false); + + // Add shift (assuming shift is small enough not to overflow low 32 in a way that affects high simply?) + // Shift is Javascript number (double), up to 9007199254740991. + // 32-bit uint max is 4294967295. + + // Proper 64-bit addition + // Construct BigInt + // Note: BigInt might not be available in all older environments, but modern browsers support it. + // Fallback: simpler logic + + let newLow = oldLow + shift; + let carry = 0; + if (newLow > 0xffffffff) { + carry = Math.floor(newLow / 0x100000000); + newLow = newLow >>> 0; + } + const newHigh = oldHigh + carry; + + view.setUint32(entryOffset, newHigh, false); + view.setUint32(entryOffset + 4, newLow, false); + } + } + + offset += size; + } +} diff --git a/js/taglib.worker.ts b/js/taglib.worker.ts index 0ef3891..cd04a09 100644 --- a/js/taglib.worker.ts +++ b/js/taglib.worker.ts @@ -303,22 +303,28 @@ async function getMetadataFromAudio(message: GetMetadataMessage): Promise) => { + const transfer: Transferable[] = [event.data.audioData.buffer]; + switch (event.data.type) { case 'Add': try { const result = await addMetadataToAudio(event.data as AddMetadataMessage); + transfer.push(result.buffer); self.postMessage( { type: event.data.type, data: result, } satisfies TagLibFileResponse, - [result.buffer, event.data.audioData.buffer] + transfer ); } catch (error) { - self.postMessage({ - type: event.data.type, - error: error instanceof Error ? error.message : String(error), - } satisfies TagLibWorkerResponse); + self.postMessage( + { + type: event.data.type, + error: error instanceof Error ? error.message : String(error), + } satisfies TagLibWorkerResponse, + transfer + ); } break; @@ -330,13 +336,16 @@ self.onmessage = async (event: MessageEvent) => { type: event.data.type, data: result, } satisfies TagLibMetadataResponse, - [event.data.audioData.buffer] + transfer ); } catch (error) { - self.postMessage({ - type: event.data.type, - error: error instanceof Error ? error.message : String(error), - } satisfies TagLibWorkerResponse); + self.postMessage( + { + type: event.data.type, + error: error instanceof Error ? error.message : String(error), + } satisfies TagLibWorkerResponse, + transfer + ); } break; } diff --git a/js/utils.js b/js/utils.js index 84b7711..edf1e93 100644 --- a/js/utils.js +++ b/js/utils.js @@ -557,6 +557,40 @@ export const getShareUrl = (path) => { return `${baseUrl}${safePath}`; }; +/** + * Builds a full artist string by combining the track's listed artists + * with any featured artists parsed from the title (feat./with). + */ +export function getFullArtistString(track) { + const knownArtists = + Array.isArray(track.artists) && track.artists.length > 0 + ? track.artists.map((a) => (typeof a === 'string' ? a : a.name) || '').filter(Boolean) + : track.artist?.name + ? [track.artist.name] + : []; + + // Parse featured artists from title, e.g. "Song (feat. A, B & C)" or "(with X & Y)" + // Note: splitting on '&' may incorrectly fragment compound artist names like "Simon & Garfunkel". + const featPattern = /\(\s*(?:feat\.?|ft\.?|with)\s+(.+?)\s*\)/gi; + const allFeatArtists = [...(track.title?.matchAll(featPattern) ?? [])].flatMap((m) => + m[1] + .split(/\s*[,&]\s*/) + .map((s) => s.trim()) + .filter(Boolean) + ); + if (allFeatArtists.length > 0) { + const knownLower = new Set(knownArtists.map((n) => n.toLowerCase())); + for (const feat of allFeatArtists) { + if (!knownLower.has(feat.toLowerCase())) { + knownArtists.push(feat); + knownLower.add(feat.toLowerCase()); + } + } + } + + return knownArtists.join('; ') || null; +} + export function fetchBlob(url) { return fetch(url).then((d) => d.blob()); } @@ -564,3 +598,10 @@ export function fetchBlob(url) { export async function fetchBlobURL(url) { return await URL.createObjectURL(await fetchBlob(url)); } + +export function getMimeType(data) { + if (data.length >= 2 && data[0] === 0xff && data[1] === 0xd8) return 'image/jpeg'; + if (data.length >= 8 && data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) + return 'image/png'; + return 'image/jpeg'; +} From 37f70f5390e260d7544df848c5edb63ee030b154 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:56:42 +0000 Subject: [PATCH 09/15] Temporarily force FLAC files to go through ffmpeg Something is wrong with the structure of the downloaded files and taglib is NOT happy with them --- js/api.js | 2 +- js/downloads.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/api.js b/js/api.js index dd0f551..9d83f96 100644 --- a/js/api.js +++ b/js/api.js @@ -1400,7 +1400,7 @@ export class LosslessAPI { try { switch (losslessContainerSettings.getContainer()) { case 'flac': - if ((await getExtensionFromBlob(blob)) != 'flac') { + if ((await getExtensionFromBlob(blob)) != 'flac' || true) { blob = await ffmpeg( blob, { args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] }, diff --git a/js/downloads.js b/js/downloads.js index b984c4c..a6e71c7 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -380,7 +380,7 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign try { switch (losslessContainerSettings.getContainer()) { case 'flac': - if ((await getExtensionFromBlob(blob)) != 'flac') { + if ((await getExtensionFromBlob(blob)) != 'flac' || true) { blob = await ffmpeg( blob, { args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] }, From 65e1b4e98da5a14b9a7b42cdad4c41fb355d7dcb Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:58:53 +0000 Subject: [PATCH 10/15] fix(metadata): remove unnecessary audio buffer type check in addMetadataToAudio --- js/metadata.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/js/metadata.js b/js/metadata.js index c6c37ec..f119f79 100644 --- a/js/metadata.js +++ b/js/metadata.js @@ -105,10 +105,6 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet console.warn('Error setting lyrics metadata', track, e); } - if (!(audioBuffer instanceof Uint8Array)) { - throw new Error('Audio buffer is not a Uint8Array'); - } - const newAudioBuffer = await addMetadataWithTagLib(audioBuffer, { ...data, }); From 56038a97ffd240332f6857ec7c293afcdca01e22 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:58:10 +0000 Subject: [PATCH 11/15] fix(workers): utilize vite ?worker imports. --- js/ffmpeg.js | 3 ++- js/taglib.ts | 6 ++--- js/taglib.types.ts | 55 +++++++++++++++++++++++++++++++++++++ js/taglib.worker.ts | 66 +++++++-------------------------------------- vite.config.js | 3 +++ 5 files changed, 73 insertions(+), 60 deletions(-) create mode 100644 js/taglib.types.ts diff --git a/js/ffmpeg.js b/js/ffmpeg.js index f9fbd62..40345f7 100644 --- a/js/ffmpeg.js +++ b/js/ffmpeg.js @@ -1,4 +1,5 @@ import { fetchBlobURL } from './utils'; +import FfmpegWorker from './ffmpeg.worker.js?worker' const ffmpegBase = 'https://unpkg.com/@ffmpeg/core/dist/esm'; const coreJs = `${ffmpegBase}/ffmpeg-core.js`; const coreWasm = `${ffmpegBase}/ffmpeg-core.wasm`; @@ -37,7 +38,7 @@ async function ffmpegWorker( const assets = loadFfmpeg(); return new Promise((resolve, reject) => { - const worker = new Worker(new URL('./ffmpeg.worker.js', import.meta.url), { type: 'module' }); + const worker = new FfmpegWorker(); // Handle abort signal const abortHandler = () => { diff --git a/js/taglib.ts b/js/taglib.ts index c485c99..c70ef53 100644 --- a/js/taglib.ts +++ b/js/taglib.ts @@ -9,8 +9,8 @@ import type { TagLibMetadataResponse, TagLibMetadata, TagLibReadMetadata, -} from './taglib.worker'; -import TagLibWorker from './taglib.worker.ts?url'; +} from './taglib.types'; +import TagLibWorker from './taglib.worker?worker'; let tagLib: Promise | null = null; @@ -32,7 +32,7 @@ export async function addMetadataWithTagLib( audioData = new Uint8Array(audioData); } - const worker = new Worker(new URL(TagLibWorker, import.meta.url), { type: 'module' }); + const worker = new TagLibWorker(); const wasmUrl = await fetchTagLib(); return new Promise((resolve, reject) => { diff --git a/js/taglib.types.ts b/js/taglib.types.ts new file mode 100644 index 0000000..5af538d --- /dev/null +++ b/js/taglib.types.ts @@ -0,0 +1,55 @@ +export type TagLibWorkerMessageType = 'Add' | 'Get'; + +export interface TagLibWorkerMessage { + type: TagLibWorkerMessageType; + wasmUrl: string; + audioData: Uint8Array; +} + +export interface TagLibWorkerResponse { + type: TagLibWorkerMessageType; + data?: T; + error?: string; +} + +export interface TagLibMetadata { + title?: string; + artist?: string; + albumTitle?: string; + albumArtist?: string; + trackNumber?: number; + totalTracks?: number; + discNumber?: number; + totalDiscs?: number; + bpm?: number; + replayGain?: { + albumReplayGain?: string; + albumPeakAmplitude?: number; + trackReplayGain?: string; + trackPeakAmplitude?: number; + }; + cover?: { + data: Uint8Array; + type: string; + }; + releaseDate?: string; + copyright?: string; + isrc?: string; + explicit?: boolean; + lyrics?: string; +} + +export interface TagLibReadMetadata extends TagLibMetadata { + duration: number; +} + +export type TagLibFileResponse = TagLibWorkerResponse; +export type TagLibMetadataResponse = TagLibWorkerResponse; + +export type AddMetadataMessage = TagLibWorkerMessage & { + type: 'Add'; +} & TagLibMetadata; + +export type GetMetadataMessage = TagLibWorkerMessage & { + type: 'Get'; +}; diff --git a/js/taglib.worker.ts b/js/taglib.worker.ts index cd04a09..44ec706 100644 --- a/js/taglib.worker.ts +++ b/js/taglib.worker.ts @@ -3,67 +3,21 @@ declare var self: DedicatedWorkerGlobalScope; import { TagLib, type PictureType } from 'taglib-wasm'; import { doTimed, doTimedAsync } from './doTimed'; +import type { + AddMetadataMessage, + GetMetadataMessage, + TagLibFileResponse, + TagLibMetadata, + TagLibMetadataResponse, + TagLibReadMetadata, + TagLibWorkerMessage, + TagLibWorkerResponse, +} from './taglib.types'; const PICTURE_TYPE_VALUES = { FrontCover: 3, }; -export type TagLibWorkerMessageType = 'Add' | 'Get'; - -export interface TagLibWorkerMessage { - type: TagLibWorkerMessageType; - wasmUrl: string; - audioData: Uint8Array; -} - -interface TagLibWorkerResponse { - type: TagLibWorkerMessageType; - data?: T; - error?: string; -} - -export interface TagLibMetadata { - title?: string; - artist?: string; - albumTitle?: string; - albumArtist?: string; - trackNumber?: number; - totalTracks?: number; - discNumber?: number; - totalDiscs?: number; - bpm?: number; - replayGain?: { - albumReplayGain?: string; - albumPeakAmplitude?: number; - trackReplayGain?: string; - trackPeakAmplitude?: number; - }; - cover?: { - data: Uint8Array; - type: string; - }; - releaseDate?: string; - copyright?: string; - isrc?: string; - explicit?: boolean; - lyrics?: string; -} - -export interface TagLibReadMetadata extends TagLibMetadata { - duration: number; -} - -export type TagLibFileResponse = TagLibWorkerResponse; -export type TagLibMetadataResponse = TagLibWorkerResponse; - -export type AddMetadataMessage = TagLibWorkerMessage & { - type: 'Add'; -} & TagLibMetadata; - -export type GetMetadataMessage = TagLibWorkerMessage & { - type: 'Get'; -}; - async function addMetadataToAudio(message: AddMetadataMessage): Promise { const { wasmUrl, diff --git a/vite.config.js b/vite.config.js index 68fe1ec..9ecd444 100644 --- a/vite.config.js +++ b/vite.config.js @@ -10,6 +10,9 @@ export default defineConfig(({ mode }) => { return { base: './', + worker: { + format: 'es', + }, resolve: { alias: { '!': '/node_modules', From 71b65e70a8359827b35fe63379fc5da10c5bb7dc Mon Sep 17 00:00:00 2001 From: SamidyFR <168582143+SamidyFR@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:45:19 +0000 Subject: [PATCH 12/15] style: auto-fix linting issues --- js/ui.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/js/ui.js b/js/ui.js index ed60cdc..0b6ef28 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1631,28 +1631,27 @@ export class UIRenderer { this.updateGlobalTheme(); } - - const downloadsdisabled = true; - if (downloadsdisabled == true) { - if (pageId === 'download') { - const maintenanceModal = document.getElementById('maintenance-modal'); - const maintenanceHomeBtn = document.getElementById('maintenance-home-btn'); - if (maintenanceModal) { - maintenanceModal.classList.add('active'); - if (maintenanceHomeBtn) { - maintenanceHomeBtn.onclick = () => { - maintenanceModal.classList.remove('active'); - navigate('/'); - }; + const downloadsdisabled = true; + if (downloadsdisabled == true) { + if (pageId === 'download') { + const maintenanceModal = document.getElementById('maintenance-modal'); + const maintenanceHomeBtn = document.getElementById('maintenance-home-btn'); + if (maintenanceModal) { + maintenanceModal.classList.add('active'); + if (maintenanceHomeBtn) { + maintenanceHomeBtn.onclick = () => { + maintenanceModal.classList.remove('active'); + navigate('/'); + }; + } + } + } else { + const maintenanceModal = document.getElementById('maintenance-modal'); + if (maintenanceModal) { + maintenanceModal.classList.remove('active'); } } - } else { - const maintenanceModal = document.getElementById('maintenance-modal'); - if (maintenanceModal) { - maintenanceModal.classList.remove('active'); - } } - } if (pageId === 'settings') { this.renderApiSettings(); const savedTabName = settingsUiState.getActiveTab(); From b596cbe8d95cc8ade7d8cc5c5e030e7fc213386d Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:22:03 +0000 Subject: [PATCH 13/15] refactor(flac): replace magic numbers with FLAC_BLOCK_TYPES constants for better readability, and pad comment block to at least 1024 bytes --- js/metadata.flac.js | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/js/metadata.flac.js b/js/metadata.flac.js index 35e4271..7a3f768 100644 --- a/js/metadata.flac.js +++ b/js/metadata.flac.js @@ -3,6 +3,28 @@ import { getFullArtistString } from './utils.js'; import { METADATA_STRINGS } from './metadata.js'; export const FLAC_MIME_TYPE = 'audio/flac'; +const FLAC_BLOCK_TYPES = { + /** This block has information about the whole stream, like sample rate, number of channels, total number of samples, etc. It must be present as the first metadata block in the stream. Other metadata blocks may follow, and ones that the decoder doesn't understand, it will skip. */ + StreamInfo: 0, + + /** This block allows for an arbitrary amount of padding. The contents of a PADDING block have no meaning. This block is useful when it is known that metadata will be edited after encoding; the user can instruct the encoder to reserve a PADDING block of sufficient size so that when metadata is added, it will simply overwrite the padding (which is relatively quick) instead of having to insert it into the right place in the existing file (which would normally require rewriting the entire file). */ + Padding: 1, + + /** This block is for use by third-party applications. The only mandatory field is a 32-bit identifier. This ID is granted upon request to an application by the FLAC maintainers. The remainder is of the block is defined by the registered application. Visit the registration page if you would like to register an ID for your application with FLAC. */ + Application: 2, + + /** This is an optional block for storing seek points. It is possible to seek to any given sample in a FLAC stream without a seek table, but the delay can be unpredictable since the bitrate may vary widely within a stream. By adding seek points to a stream, this delay can be significantly reduced. Each seek point takes 18 bytes, so 1% resolution within a stream adds less than 2k. There can be only one SEEKTABLE in a stream, but the table can have any number of seek points. There is also a special 'placeholder' seekpoint which will be ignored by decoders but which can be used to reserve space for future seek point insertion. */ + SeekTable: 3, + + /** This block is for storing a list of human-readable name/value pairs. Values are encoded using UTF-8. It is an implementation of the Vorbis comment specification (without the framing bit). This is the only officially supported tagging mechanism in FLAC. There may be only one VORBIS_COMMENT block in a stream. In some external documentation, Vorbis comments are called FLAC tags to lessen confusion. */ + VorbisComment: 4, + + /** This block is for storing various information that can be used in a cue sheet. It supports track and index points, compatible with Red Book CD digital audio discs, as well as other CD-DA metadata such as media catalog number and track ISRCs. The CUESHEET block is especially useful for backing up CD-DA discs, but it can be used as a general purpose cueing mechanism for playback. */ + CueSheet: 5, + + /** This block is for storing pictures associated with the file, most commonly cover art from CDs. There may be more than one PICTURE block in a file. The picture format is similar to the APIC frame in ID3v2. The PICTURE block has a type, MIME type, and UTF-8 description like ID3v2, and supports external linking via URL (though this is discouraged). The differences are that there is no uniqueness constraint on the description field, and the MIME type is mandatory. The FLAC PICTURE block also includes the resolution, color depth, and palette size so that the client can search for a suitable picture without having to scan them all. */ + Picture: 6, +}; export async function readFlacMetadata(file, metadata) { const arrayBuffer = await file.arrayBuffer(); @@ -11,9 +33,9 @@ export async function readFlacMetadata(file, metadata) { if (!isFlacFile(dataView)) return; const blocks = parseFlacBlocks(dataView); - const vorbisBlock = blocks.find((b) => b.type === 4); - const pictureBlock = blocks.find((b) => b.type === 6); - const streamInfo = blocks.find((b) => b.type === 0); + const vorbisBlock = blocks.find((b) => b.type === FLAC_BLOCK_TYPES.VorbisComment); + const pictureBlock = blocks.find((b) => b.type === FLAC_BLOCK_TYPES.Picture); + const streamInfo = blocks.find((b) => b.type === FLAC_BLOCK_TYPES.StreamInfo); const artists = []; if (vorbisBlock) { @@ -344,6 +366,13 @@ export function createVorbisCommentBlock(comments = []) { offset += commentBytes.length; } + // Pad to at least 1024 bytes for future metadata edits without needing to rewrite the whole file + if (uint8Array.length < 1024) { + const newArray = new Uint8Array(1024); + newArray.set(uint8Array); + return newArray; + } + return uint8Array; } @@ -443,8 +472,10 @@ export function rebuildFlacWithMetadata( ) { const originalArray = new Uint8Array(dataView.buffer); - // Remove old Vorbis comment and picture blocks - const filteredBlocks = blocks.filter((b) => b.type !== 4 && b.type !== 6); // 4 = Vorbis, 6 = Picture + // Remove seek table, old Vorbis comment, and picture blocks + const filteredBlocks = blocks.filter( + (b) => ![FLAC_BLOCK_TYPES.SeekTable, FLAC_BLOCK_TYPES.VorbisComment, FLAC_BLOCK_TYPES.Picture].includes(b.type) + ); // Calculate new file size let newSize = 4; // "fLaC" signature @@ -504,7 +535,7 @@ export function rebuildFlacWithMetadata( if (vorbisCommentBlock) { // Write new Vorbis comment block const vorbisHeaderOffset = offset; - const vorbisHeader = 0x04; // Vorbis comment type + const vorbisHeader = FLAC_BLOCK_TYPES.VorbisComment; // Vorbis comment type newFile[offset++] = vorbisHeader; newFile[offset++] = (vorbisCommentBlock.length >> 16) & 0xff; newFile[offset++] = (vorbisCommentBlock.length >> 8) & 0xff; @@ -517,7 +548,7 @@ export function rebuildFlacWithMetadata( // Write picture block if available if (pictureBlock) { const pictureHeaderOffset = offset; - const pictureHeader = 0x06; // Picture type + const pictureHeader = FLAC_BLOCK_TYPES.Picture; // Picture type newFile[offset++] = pictureHeader; newFile[offset++] = (pictureBlock.length >> 16) & 0xff; newFile[offset++] = (pictureBlock.length >> 8) & 0xff; From 2e1367e5c27dba554982b5e8334d5588fffd0d96 Mon Sep 17 00:00:00 2001 From: edideaur Date: Mon, 9 Mar 2026 21:54:32 +0000 Subject: [PATCH 14/15] video covers --- index.html | 26 +++++++++++-- js/api.js | 16 ++++++++ js/music-api.js | 13 +++++++ js/player.js | 81 +++++++++++++++++++++++++++++++++++++- js/ui-interactions.js | 8 +++- js/ui.js | 64 +++++++++++++----------------- styles.css | 91 +++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 250 insertions(+), 49 deletions(-) diff --git a/index.html b/index.html index 9954827..6b0a084 100644 --- a/index.html +++ b/index.html @@ -376,12 +376,30 @@ stroke-linejoin="round" class="lucide lucide-repeat-icon lucide-repeat" > - - - - + + + + + +
`; + }) + .join(''); + + qualityMenu.querySelectorAll('.fs-quality-option').forEach((btn) => { + btn.onclick = (e) => { + e.stopPropagation(); + const level = parseInt(btn.dataset.level); + this.hls.currentLevel = level; + const labelSpan = qualityBtn.querySelector('.fs-quality-label'); + if (labelSpan) labelSpan.textContent = level === -1 ? 'Auto' : qualityLabels[level + 1] || 'Auto'; + qualityMenu.style.display = 'none'; + }; + }); + }; + + qualityBtn.style.display = 'flex'; + qualityBtn.onclick = (e) => { + e.stopPropagation(); + const isVisible = qualityMenu.style.display === 'block'; + qualityMenu.style.display = isVisible ? 'none' : 'block'; + if (!isVisible) { + updateQualityMenu(); + } + }; + + this.hls.on(Hls.Events.LEVEL_SWITCHED, () => { + updateQualityMenu(); + const labelSpan = qualityBtn.querySelector('.fs-quality-label'); + if (labelSpan) { + const currentLevel = this.hls.currentLevel; + labelSpan.textContent = currentLevel === -1 ? 'Auto' : qualityLabels[currentLevel + 1] || 'Auto'; + } + }); + + document.addEventListener('click', () => { + qualityMenu.style.display = 'none'; + }); + + qualityMenu.onclick = (e) => e.stopPropagation(); + } + async playVideo(video) { if (!video) return; const videoTrack = { diff --git a/js/ui-interactions.js b/js/ui-interactions.js index 6b7e08c..95cd7bc 100644 --- a/js/ui-interactions.js +++ b/js/ui-interactions.js @@ -251,6 +251,12 @@ export function initializeUIInteractions(player, api, ui) { ? `title="Blocked: ${contentBlockingSettings.isTrackBlocked(track.id) ? 'Track blocked' : contentBlockingSettings.isArtistBlocked(track.artist?.id) ? 'Artist blocked' : 'Album blocked'}"` : ''; + const isVideo = track.type === 'video'; + const coverUrl = + isVideo && track.imageId + ? api.getVideoCoverUrl(track.imageId) + : api.getCoverUrl(track.album?.cover); + return `
@@ -260,7 +266,7 @@ export function initializeUIInteractions(player, api, ui) {
-
${escapeHtml(trackTitle)} ${qualityBadge}
diff --git a/js/ui.js b/js/ui.js index 0b6ef28..891ff56 100644 --- a/js/ui.js +++ b/js/ui.js @@ -348,9 +348,19 @@ export class UIRenderer { let trackImageHTML = ''; if (showCover) { if (isVideo && this.currentPage === 'playlist') { - trackImageHTML = `
`; + const videoCoverUrl = this.api.getVideoCoverUrl(track.imageId); + if (videoCoverUrl) { + trackImageHTML = ``; + } else { + trackImageHTML = `
`; + } } else if (isVideo && (this.currentPage === 'search' || this.currentPage === 'library')) { - trackImageHTML = `
`; + const videoCoverUrl = this.api.getVideoCoverUrl(track.imageId); + if (videoCoverUrl) { + trackImageHTML = ``; + } else { + trackImageHTML = `
`; + } } else { trackImageHTML = this.getCoverHTML( track.image || track.cover || track.album?.cover, @@ -670,10 +680,13 @@ export class UIRenderer { const duration = formatTime(video.duration); const artistName = getTrackArtists(video); + const videoCoverUrl = this.api.getVideoCoverUrl(video.imageId); const cover = video.image || video.cover; let imageHTML; - if (cover) { + if (videoCoverUrl) { + imageHTML = `${escapeHtml(video.title)}`; + } else if (cover) { imageHTML = this.getCoverHTML(cover, escapeHtml(video.title)); } else { imageHTML = `
`; @@ -1012,6 +1025,11 @@ export class UIRenderer { if (image) image.style.display = 'block'; if (visualizerContainer) visualizerContainer.style.display = 'block'; + const qualityBtn = document.getElementById('fs-quality-btn'); + const qualityMenu = document.getElementById('fs-quality-menu'); + if (qualityBtn) qualityBtn.style.display = 'none'; + if (qualityMenu) qualityMenu.style.display = 'none'; + const videoCoverUrl = track.videoUrl || track.videoCoverUrl || track.album?.videoCoverUrl || null; const coverUrl = videoCoverUrl || this.api.getCoverUrl(track.album?.cover, '1280'); @@ -1227,50 +1245,20 @@ export class UIRenderer { // Mouse move handler const handleMouseMove = (e) => { const rect = overlay.getBoundingClientRect(); - // Check if mouse is near the top-right corner (within 150px from right, 100px from top) const isNearTopRight = e.clientY < 100 && e.clientX > rect.width - 150; if (isUIHidden) { if (overlay.classList.contains('is-video-mode')) { - toggleUI(); + if (isNearTopRight) { + showButton(); + } else { + hideButton(); + } } else if (isNearTopRight) { showButton(); } else { hideButton(); } - } else if (overlay.classList.contains('is-video-mode')) { - resetVideoHideTimer(); - } - }; - - let videoHideTimer = null; - const resetVideoHideTimer = () => { - if (videoHideTimer) clearTimeout(videoHideTimer); - if (!overlay.classList.contains('is-video-mode') || isUIHidden) return; - - videoHideTimer = setTimeout(() => { - if (!isUIHidden && overlay.classList.contains('is-video-mode')) { - toggleUI(); - } - }, 3000); - }; - - resetVideoHideTimer(); - - // Toggle UI visibility - const toggleUI = () => { - isUIHidden = !isUIHidden; - overlay.classList.toggle('ui-hidden', isUIHidden); - toggleBtn.classList.toggle('active', isUIHidden); - toggleBtn.title = isUIHidden ? 'Show UI' : 'Hide UI'; - - if (isUIHidden) { - // When UI is hidden, immediately hide the button - // It will reappear when mouse nears top-right - hideButton(); - } else { - // When UI is shown, keep button visible - showButton(); } }; diff --git a/styles.css b/styles.css index dfc3a65..d0c3942 100644 --- a/styles.css +++ b/styles.css @@ -2113,6 +2113,10 @@ input[type='search']::-webkit-search-cancel-button { color: var(--highlight); } +.track-item.video-track-item { + gap: var(--spacing-xl); +} + .track-item.unavailable { opacity: 0.5; cursor: not-allowed; @@ -5436,12 +5440,12 @@ img[src=''] { right: 2rem; max-width: 500px; margin: 0 auto; - background: rgb(15, 15, 15, 0.5); - backdrop-filter: blur(12px); + background: transparent; + backdrop-filter: none; padding: 0.6rem 1rem; border-radius: 10px; - border: 1px solid rgb(255, 255, 255, 0.05); - box-shadow: 0 4px 20px rgb(0, 0, 0, 0.4); + border: none; + box-shadow: none; transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease; @@ -5478,6 +5482,84 @@ img[src=''] { display: none; } +#fullscreen-cover-overlay.is-video-mode .fs-quality-btn { + display: flex !important; + width: 28px; + height: 28px; + padding: 0.25rem; +} + +#fullscreen-cover-overlay.is-video-mode .fs-quality-btn svg { + width: 18px; + height: 18px; +} + +#fullscreen-cover-overlay.is-video-mode .fs-quality-label { + display: none; +} + +.fs-quality-btn { + background: transparent; + border: none; + color: white; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 0.75rem; + display: flex; + align-items: center; + gap: 4px; + opacity: 0.7; + transition: opacity 0.2s; + position: relative; +} + +.fs-quality-btn:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +.fullscreen-volume-container { + position: relative; +} + +.fs-quality-menu { + position: absolute; + bottom: 100%; + right: 0; + background: rgb(20, 20, 20); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 4px; + min-width: 120px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + z-index: 1000; + margin-bottom: 8px; +} + +.fs-quality-option { + display: block; + width: 100%; + padding: 8px 12px; + border: none; + background: transparent; + color: white; + text-align: left; + cursor: pointer; + font-size: 0.85rem; + border-radius: 4px; + transition: background 0.2s; +} + +.fs-quality-option:hover { + background: rgba(255, 255, 255, 0.1); +} + +.fs-quality-option.active { + background: var(--primary); + color: white; +} + #fullscreen-cover-overlay.is-video-mode .fullscreen-volume-container { margin-top: 0.5rem; } @@ -8193,6 +8275,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { .video-card .card-image-container { aspect-ratio: 16 / 9 !important; + margin-bottom: var(--spacing-sm); } .video-card .card-image { From 6c4032a9779f50fe841ceb0f26c9453b0eb00529 Mon Sep 17 00:00:00 2001 From: edideaur <182119792+edideaur@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:58:27 +0000 Subject: [PATCH 15/15] style: auto-fix linting issues --- styles.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/styles.css b/styles.css index d0c3942..c8e345b 100644 --- a/styles.css +++ b/styles.css @@ -5516,7 +5516,7 @@ img[src=''] { .fs-quality-btn:hover { opacity: 1; - background: rgba(255, 255, 255, 0.1); + background: rgb(255, 255, 255, 0.1); } .fullscreen-volume-container { @@ -5528,11 +5528,11 @@ img[src=''] { bottom: 100%; right: 0; background: rgb(20, 20, 20); - border: 1px solid rgba(255, 255, 255, 0.1); + border: 1px solid rgb(255, 255, 255, 0.1); border-radius: 8px; padding: 4px; min-width: 120px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + box-shadow: 0 4px 20px rgb(0, 0, 0, 0.5); z-index: 1000; margin-bottom: 8px; } @@ -5552,7 +5552,7 @@ img[src=''] { } .fs-quality-option:hover { - background: rgba(255, 255, 255, 0.1); + background: rgb(255, 255, 255, 0.1); } .fs-quality-option.active {