From 7eaf8df8f0726b0c8985f58a35f1b49d3277b326 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Tue, 14 Jan 2025 02:37:26 +0000 Subject: [PATCH 01/61] chore: update public repo From e0109980c010dfdab87b86fb585624072aa3cc1a Mon Sep 17 00:00:00 2001 From: desu-bot Date: Tue, 14 Jan 2025 02:37:26 +0000 Subject: [PATCH 02/61] chore: update public repo From e75d7f7f2920efa0993872d5b1901cbeb5ccd0ed Mon Sep 17 00:00:00 2001 From: desu-bot Date: Tue, 14 Jan 2025 02:38:00 +0000 Subject: [PATCH 03/61] chore: update public repo --- .gitignore | 13 + LICENSE | 26 + eslint.config.js | 22 + package.json | 38 + pnpm-lock.yaml | 3931 ++++++++++++++++++++ scripts/auth/mtcute-login.ts | 20 + scripts/infra/navidrome-find-duplicates.ts | 105 + scripts/infra/navidrome-remux-m4a.ts | 66 + scripts/infra/slskd-total-upload.ts | 39 + scripts/media/deezer-art-fetcher.ts | 58 + scripts/media/ffmpeg-clipper.ts | 129 + scripts/media/itunes-art-fetcher.ts | 46 + scripts/media/itunes-artist-art-fetcher.ts | 63 + scripts/misc/update-forkgram.ts | 51 + tsconfig.json | 26 + utils/captcha.ts | 87 + utils/currency.ts | 113 + utils/fetch.ts | 37 + utils/fs.ts | 19 + utils/misc.ts | 10 + utils/navidrome.ts | 32 + utils/oauth.ts | 78 + utils/telegram.ts | 11 + utils/webdav.ts | 324 ++ utils/xml.ts | 20 + 25 files changed, 5364 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 scripts/auth/mtcute-login.ts create mode 100644 scripts/infra/navidrome-find-duplicates.ts create mode 100644 scripts/infra/navidrome-remux-m4a.ts create mode 100644 scripts/infra/slskd-total-upload.ts create mode 100644 scripts/media/deezer-art-fetcher.ts create mode 100644 scripts/media/ffmpeg-clipper.ts create mode 100644 scripts/media/itunes-art-fetcher.ts create mode 100644 scripts/media/itunes-artist-art-fetcher.ts create mode 100644 scripts/misc/update-forkgram.ts create mode 100644 tsconfig.json create mode 100644 utils/captcha.ts create mode 100644 utils/currency.ts create mode 100644 utils/fetch.ts create mode 100644 utils/fs.ts create mode 100644 utils/misc.ts create mode 100644 utils/navidrome.ts create mode 100644 utils/oauth.ts create mode 100644 utils/telegram.ts create mode 100644 utils/webdav.ts create mode 100644 utils/xml.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3b4c83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +private/ +.nyc_output/ +**/.DS_Store +.idea +.vscode +*.log +/assets + +coverage +.rollup.cache +*.tsbuildinfo +.env \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a38f8d --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +# DON'T BE A DICK PUBLIC LICENSE + +> Version 1.1, December 2016 + +> Copyright (C) 2024 alina sireneva + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document. + +> DON'T BE A DICK PUBLIC LICENSE +> TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +1. Do whatever you like with the original work, just don't be a dick. + + Being a dick includes - but is not limited to - the following instances: + + 1a. Outright copyright infringement - Don't just copy this and change the name. + 1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick. + 1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick. + +2. If you become rich through modifications, related works/services, or supporting the original work, +share the love. Only a dick would make loads off this work and not buy the original work's +creator(s) a pint. + +3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes +you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back. \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a159663 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import antfu from '@antfu/eslint-config' + +export default antfu({ + ignores: ['assets/'], + typescript: true, + rules: { + 'curly': ['error', 'multi-line'], + 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'n/prefer-global/buffer': 'off', + 'no-restricted-globals': ['error', 'Buffer', '__dirname', 'require'], + 'style/quotes': ['error', 'single', { avoidEscape: true }], + 'test/consistent-test-it': 'off', + 'test/prefer-lowercase-title': 'off', + 'antfu/if-newline': 'off', + 'style/max-statements-per-line': ['error', { max: 2 }], + 'ts/no-redeclare': 'off', + 'no-alert': 'off', + 'no-console': 'off', + 'node/prefer-global/process': 'off', + 'unused-imports/no-unused-vars': 'off', + }, +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..4336100 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "teidesu-scripts", + "type": "module", + "packageManager": "pnpm@9.5.0", + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@faker-js/faker": "^9.3.0", + "@fuman/io": "^0.0.4", + "@fuman/node": "^0.0.4", + "@mtcute/node": "^0.19.1", + "@types/plist": "^3.0.5", + "cheerio": "^1.0.0", + "es-main": "^1.3.0", + "filesize": "^10.1.6", + "json5": "^2.2.3", + "kuromoji": "^0.1.2", + "nanoid": "^5.0.9", + "plist": "^3.1.0", + "qrcode-terminal": "^0.12.0", + "tough-cookie": "^5.0.0", + "tough-cookie-file-store": "^2.0.3", + "undici": "^7.2.0", + "wanakana": "^5.3.1" + }, + "devDependencies": { + "@antfu/eslint-config": "3.10.0", + "@fuman/fetch": "0.0.7", + "@fuman/utils": "0.0.4", + "@types/node": "22.10.0", + "domhandler": "^5.0.3", + "dotenv": "16.4.5", + "htmlparser2": "^10.0.0", + "zod": "3.23.8", + "zx": "8.2.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7616233 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3931 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@faker-js/faker': + specifier: ^9.3.0 + version: 9.3.0 + '@fuman/io': + specifier: ^0.0.4 + version: 0.0.4 + '@fuman/node': + specifier: ^0.0.4 + version: 0.0.4 + '@mtcute/node': + specifier: ^0.19.1 + version: 0.19.1 + '@types/plist': + specifier: ^3.0.5 + version: 3.0.5 + cheerio: + specifier: ^1.0.0 + version: 1.0.0 + es-main: + specifier: ^1.3.0 + version: 1.3.0 + filesize: + specifier: ^10.1.6 + version: 10.1.6 + json5: + specifier: ^2.2.3 + version: 2.2.3 + kuromoji: + specifier: ^0.1.2 + version: 0.1.2 + nanoid: + specifier: ^5.0.9 + version: 5.0.9 + plist: + specifier: ^3.1.0 + version: 3.1.0 + qrcode-terminal: + specifier: ^0.12.0 + version: 0.12.0 + tough-cookie: + specifier: ^5.0.0 + version: 5.0.0 + tough-cookie-file-store: + specifier: ^2.0.3 + version: 2.0.3 + typescript: + specifier: ^5.0.0 + version: 5.7.2 + undici: + specifier: ^7.2.0 + version: 7.2.0 + wanakana: + specifier: ^5.3.1 + version: 5.3.1 + devDependencies: + '@antfu/eslint-config': + specifier: 3.10.0 + version: 3.10.0(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0)(typescript@5.7.2) + '@fuman/fetch': + specifier: 0.0.7 + version: 0.0.7(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) + '@fuman/utils': + specifier: 0.0.4 + version: 0.0.4 + '@types/node': + specifier: 22.10.0 + version: 22.10.0 + domhandler: + specifier: ^5.0.3 + version: 5.0.3 + dotenv: + specifier: 16.4.5 + version: 16.4.5 + htmlparser2: + specifier: ^10.0.0 + version: 10.0.0 + zod: + specifier: 3.23.8 + version: 3.23.8 + zx: + specifier: 8.2.2 + version: 8.2.2 + +packages: + + '@antfu/eslint-config@3.10.0': + resolution: {integrity: sha512-rTl9BA42RIaC2l9iol1+uinO1alqVchAr8Vg2WDnXiAJPDaqiwRnbXIWM1fCZVNF4lwgqv71NIsusd67EpTPqA==} + hasBin: true + peerDependencies: + '@eslint-react/eslint-plugin': ^1.5.8 + '@prettier/plugin-xml': ^3.4.1 + '@unocss/eslint-plugin': '>=0.50.0' + astro-eslint-parser: ^1.0.2 + eslint: ^9.10.0 + eslint-plugin-astro: ^1.2.0 + eslint-plugin-format: '>=0.1.0' + eslint-plugin-react-hooks: ^5.0.0 + eslint-plugin-react-refresh: ^0.4.4 + eslint-plugin-solid: ^0.14.3 + eslint-plugin-svelte: '>=2.35.1' + prettier-plugin-astro: ^0.13.0 + prettier-plugin-slidev: ^1.0.5 + svelte-eslint-parser: '>=0.37.0' + peerDependenciesMeta: + '@eslint-react/eslint-plugin': + optional: true + '@prettier/plugin-xml': + optional: true + '@unocss/eslint-plugin': + optional: true + astro-eslint-parser: + optional: true + eslint-plugin-astro: + optional: true + eslint-plugin-format: + optional: true + eslint-plugin-react-hooks: + optional: true + eslint-plugin-react-refresh: + optional: true + eslint-plugin-solid: + optional: true + eslint-plugin-svelte: + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-slidev: + optional: true + svelte-eslint-parser: + optional: true + + '@antfu/install-pkg@0.4.1': + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@badrap/valita@0.4.2': + resolution: {integrity: sha512-Mwmr7k2iK0Yy0POLnAFUgab2mxKYeIsYXHY7sg3jo8XFsFHbG0SBmTcktXD0uW8N4WZePKf8s68QV7QDTGSdHA==} + engines: {node: '>= 18'} + + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + + '@clack/prompts@0.8.2': + resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + + '@es-joy/jsdoccomment@0.48.0': + resolution: {integrity: sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==} + engines: {node: '>=16'} + + '@es-joy/jsdoccomment@0.49.0': + resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==} + engines: {node: '>=16'} + + '@eslint-community/eslint-plugin-eslint-comments@4.4.1': + resolution: {integrity: sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@1.2.3': + resolution: {integrity: sha512-wlZhwlDFxkxIZ571aH0FoK4h4Vwx7P3HJx62Gp8hTc10bfpwT2x0nULuAHmQSJBOWPgPeVf+9YtnD4j50zVHmA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.10.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.19.0': + resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.0': + resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.15.0': + resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/markdown@6.2.1': + resolution: {integrity: sha512-cKVd110hG4ICHmWhIwZJfKmmJBvbiDWyrHODJknAtudKgZtlROGoLX9UEOA0o746zC0hCY4UV4vR+aOGW9S6JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.3': + resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@faker-js/faker@9.3.0': + resolution: {integrity: sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@fuman/fetch@0.0.7': + resolution: {integrity: sha512-yzyQI9ssqBrx04zKlfZwRHvJ9sw44RvfForjhaMhy7yA9jtghgVAjceeLTvF5xWEMgvIlIMfZRmSzEtCgGlfcg==} + peerDependencies: + '@badrap/valita': '>=0.4.0' + tough-cookie: ^5.0.0 || ^4.0.0 + valibot: ^0.42.0 + yup: ^1.0.0 + zod: ^3.0.0 + peerDependenciesMeta: + '@badrap/valita': + optional: true + tough-cookie: + optional: true + valibot: + optional: true + yup: + optional: true + zod: + optional: true + + '@fuman/io@0.0.4': + resolution: {integrity: sha512-IXzBJjHTVKyi04WaGtSXE0dhL3QK45ekrEZNfH/V59XQ1WupSqWevfSWd9T07rdagc2jtaeu8aJY6bwaiJpdYg==} + + '@fuman/net@0.0.4': + resolution: {integrity: sha512-a8Isnj+qgRNaqqmBCT6lZ9GZj5F3vQdygN5AzB6GGCbLKcOeH+1u5Twh5CUAW/dM7oogrTWOwCqgvS2XHbjzaQ==} + + '@fuman/node@0.0.4': + resolution: {integrity: sha512-tgwbIceUHWuwh4RTwJRQ1sLjzuIGrWx0SeCrqYhGF+IkI/B7DY0FP2SZykWImkVDtW8IzmdZskPZqiDINRGcNg==} + + '@fuman/utils@0.0.4': + resolution: {integrity: sha512-YBZIlGDbM8s9G85pWFZJ9wQrDY4511XLHZ06/uxZfXBY0eSStwje8JFNmRMNF0SjRk4D3iRmPl9wirHKTkg89w==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@mtcute/core@0.19.1': + resolution: {integrity: sha512-Xfq1T03kNRcyomkQl1d426piCnbgKZiYV2z/rM4BBrTIMDwGdxUpp66FPTj7N+MHm1/bVoJNFTUO67nBrLmqdA==} + + '@mtcute/file-id@0.19.0': + resolution: {integrity: sha512-r9r5JxchoVtYYMLPsf/wSnd5+KB4KmilWHywKQenf0DgKD+LCEN2FJpzY44RFE5dpy+eV5OHZ85zxA6EyYz/mA==} + + '@mtcute/html-parser@0.19.0': + resolution: {integrity: sha512-AVYD2/FT5g/a2m1o8+9Lg/0H/VIkxAwd9uqvXcR+aMpTQlQExWVwNi805yOtzs2sv8QK0yyVbN5AL7STBu+x+g==} + + '@mtcute/markdown-parser@0.19.0': + resolution: {integrity: sha512-sc8Yl5eDjEZ975/UMJNsHoiIjW6oKA2m1j4bi966ZxZ1L1SYjlsgnm1b/Vg+rsbeYoCjz1xU9eZMkPZh2jIVWA==} + + '@mtcute/node@0.19.1': + resolution: {integrity: sha512-YeG+HbYkp43K4VeB9wFHAEdj081sHwOJd3hWRctwoJUsv0IFMqQ6m6g2dXd6xyroJ91ewNjnfmaKbrM41Gdmuw==} + + '@mtcute/tl-runtime@0.19.0': + resolution: {integrity: sha512-5nqzQexx4/at+lWnd8G5zCsuI7k+vw/cr3IecRaZ+cOyNKLYQX9zqK2n6Lx+A9K1/M9L33YSAVPSPQnC+q0QjQ==} + + '@mtcute/tl@196.0.0': + resolution: {integrity: sha512-/FEoLfGk7VatEMkBWn4naRQnolodJX0CL0C2rPJr+gmhLSVMQs1a9HqLMIUFYRlEvZ4ayk4ujOWvF9G64xvW5g==} + + '@mtcute/wasm@0.19.0': + resolution: {integrity: sha512-QvSCjNR3/Y4knRMfccNXAdPUCgnQfjmOD/LGGHgQq/5/7SfA7f4/wk7G/TdlnBPArqk58EvyrC5qeKEy81y9Xw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@stylistic/eslint-plugin@2.11.0': + resolution: {integrity: sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.40.0' + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/events@3.0.0': + resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/node@22.10.0': + resolution: {integrity: sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript-eslint/eslint-plugin@8.16.0': + resolution: {integrity: sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.16.0': + resolution: {integrity: sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.16.0': + resolution: {integrity: sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.16.0': + resolution: {integrity: sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.16.0': + resolution: {integrity: sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.16.0': + resolution: {integrity: sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.16.0': + resolution: {integrity: sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/visitor-keys@8.16.0': + resolution: {integrity: sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/eslint-plugin@1.1.10': + resolution: {integrity: sha512-uScH5Kz5v32vvtQYB2iodpoPg2mGASK+VKpjlc2IUgE0+16uZKqVKi2vQxjxJ6sMCQLBs4xhBFZlmZBszsmfKQ==} + peerDependencies: + '@typescript-eslint/utils': '>= 8.0' + eslint: '>= 8.57.0' + typescript: '>= 5.0.0' + vitest: '*' + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@11.3.0: + resolution: {integrity: sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001684: + resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0: + resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + engines: {node: '>=18.17'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + ci-info@4.1.0: + resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + engines: {node: '>=8'} + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + doublearray@0.0.2: + resolution: {integrity: sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==} + + electron-to-chromium@1.5.65: + resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encoding-sniffer@0.2.0: + resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-main@1.3.0: + resolution: {integrity: sha512-AzORKdz1Zt97TzbYQnIrI3ZiibWpRXUfpo/w0xOJ20GpNYd2bd3MU9m31zS/aJ1TJl6JfLTok83Y8HjNunYT0A==} + + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-compat-utils@0.6.3: + resolution: {integrity: sha512-9IDdksh5pUYP2ZLi7mOdROxVjLY8gY2qKxprmrJ/5Dyqud7M/IFKxF3o0VLlRhITm1pK6Fk7NiBxE39M/VlUcw==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-flat-gitignore@0.3.0: + resolution: {integrity: sha512-0Ndxo4qGhcewjTzw52TK06Mc00aDtHNTdeeW2JfONgDcLkRO/n/BteMRzNVpLQYxdCC/dFEilfM9fjjpGIJ9Og==} + peerDependencies: + eslint: ^9.5.0 + + eslint-flat-config-utils@0.4.0: + resolution: {integrity: sha512-kfd5kQZC+BMO0YwTol6zxjKX1zAsk8JfSAopbKjKqmENTJcew+yBejuvccAg37cvOrN0Mh+DVbeyznuNWEjt4A==} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-json-compat-utils@0.2.1: + resolution: {integrity: sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==} + engines: {node: '>=12'} + peerDependencies: + '@eslint/json': '*' + eslint: '*' + jsonc-eslint-parser: ^2.4.0 + peerDependenciesMeta: + '@eslint/json': + optional: true + + eslint-merge-processors@0.1.0: + resolution: {integrity: sha512-IvRXXtEajLeyssvW4wJcZ2etxkR9mUf4zpNwgI+m/Uac9RfXHskuJefkHUcawVzePnd6xp24enp5jfgdHzjRdQ==} + peerDependencies: + eslint: '*' + + eslint-plugin-antfu@2.7.0: + resolution: {integrity: sha512-gZM3jq3ouqaoHmUNszb1Zo2Ux7RckSvkGksjLWz9ipBYGSv1EwwBETN6AdiUXn+RpVHXTbEMPAPlXJazcA6+iA==} + peerDependencies: + eslint: '*' + + eslint-plugin-command@0.2.6: + resolution: {integrity: sha512-T0bHZ1oblW1xUHUVoBKZJR2osSNNGkfZuK4iqboNwuNS/M7tdp3pmURaJtTi/XDzitxaQ02lvOdFH0mUd5QLvQ==} + peerDependencies: + eslint: '*' + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-import-x@4.4.3: + resolution: {integrity: sha512-QBprHvhLsfDhP++2T1NnjsOUt6bLDX3NMHaYwAB1FD3xmYTkdFH+HS1OamGhz28jLkRyIZa6UNAzTxbHnJwz5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + eslint-plugin-jsdoc@50.6.0: + resolution: {integrity: sha512-tCNp4fR79Le3dYTPB0dKEv7yFyvGkUCa+Z3yuTrrNGGOxBlXo9Pn0PEgroOZikUQOGjxoGMVKNjrOHcYEdfszg==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-jsonc@2.18.2: + resolution: {integrity: sha512-SDhJiSsWt3nItl/UuIv+ti4g3m4gpGkmnUJS9UWR3TrpyNsIcnJoBRD7Kof6cM4Rk3L0wrmY5Tm3z7ZPjR2uGg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-plugin-n@17.14.0: + resolution: {integrity: sha512-maxPLMEA0rPmRpoOlxEclKng4UpDe+N5BJS4t24I3UKnN109Qcivnfs37KMy84G0af3bxjog5lKctP5ObsvcTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-plugin-no-only-tests@3.3.0: + resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} + engines: {node: '>=5.0.0'} + + eslint-plugin-perfectionist@4.1.2: + resolution: {integrity: sha512-YjXPWB/rKe/gPUsyuxw75wTUrzN5MuJnRV0PH9NoonFvgcdVIXk551mkBKPr59nRZCbu7S3dFHwfo4gA42DB2w==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + eslint: '>=8.0.0' + + eslint-plugin-regexp@2.7.0: + resolution: {integrity: sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==} + engines: {node: ^18 || >=20} + peerDependencies: + eslint: '>=8.44.0' + + eslint-plugin-toml@0.11.1: + resolution: {integrity: sha512-Y1WuMSzfZpeMIrmlP1nUh3kT8p96mThIq4NnHrYUhg10IKQgGfBZjAWnrg9fBqguiX4iFps/x/3Hb5TxBisfdw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-plugin-unicorn@56.0.1: + resolution: {integrity: sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==} + engines: {node: '>=18.18'} + peerDependencies: + eslint: '>=8.56.0' + + eslint-plugin-unused-imports@4.1.4: + resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-plugin-vue@9.31.0: + resolution: {integrity: sha512-aYMUCgivhz1o4tLkRHj5oq9YgYPM4/EJc0M7TAKRLCUA5OYxRLAhYEVD2nLtTwLyixEFI+/QXSvKU9ESZFgqjQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-yml@1.15.0: + resolution: {integrity: sha512-leC8APYVOsKyWUlvRwVhewytK5wS70BfMqIaUplFstRfzCoVp0YoEroV4cUEvQrBj93tQ3M9LcjO/ewr6D4kjA==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-processor-vue-blocks@0.1.2: + resolution: {integrity: sha512-PfpJ4uKHnqeL/fXUnzYkOax3aIenlwewXRX8jFinA1a2yCFnLgMuiH3xvCgvHHUlV2xJWQHbCTdiJWGwb3NqpQ==} + peerDependencies: + '@vue/compiler-sfc': ^3.3.0 + eslint: ^8.50.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.15.0: + resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filesize@10.1.6: + resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} + engines: {node: '>= 10.4.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.12.0: + resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + htmlparser2@10.0.0: + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-eslint-parser@2.4.0: + resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kuromoji@0.1.2: + resolution: {integrity: sha512-V0dUf+C2LpcPEXhoHLMAop/bOht16Dyr+mDiIE39yX3vqau7p80De/koFqpiTcL1zzdZlc3xuHZ8u5gjYRfFaQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + magic-string@0.30.14: + resolution: {integrity: sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.0: + resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.0.3: + resolution: {integrity: sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.0.9: + resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} + engines: {node: ^18 || >=20} + hasBin: true + + napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + + node-abi@3.71.0: + resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} + engines: {node: '>=10'} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.5: + resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-gitignore@2.0.0: + resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} + engines: {node: '>=14'} + + parse-imports@2.2.1: + resolution: {integrity: sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==} + engines: {node: '>= 18'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pkg-types@1.2.1: + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode-terminal@0.12.0: + resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==} + hasBin: true + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regjsparser@0.10.0: + resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slashes@3.0.12: + resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.20: + resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} + + stable-hash@0.0.4: + resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.6.2: + resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==} + engines: {node: '>=12.20'} + + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + tldts-core@6.1.66: + resolution: {integrity: sha512-s07jJruSwndD2X8bVjwioPfqpIc1pDTzszPe9pL1Skbh4bjytL85KNQ3tolqLbCvpQHawIsGfFi9dgerWjqW4g==} + + tldts@6.1.66: + resolution: {integrity: sha512-l3ciXsYFel/jSRfESbyKYud1nOw7WfhrBEF9I3UiarYk/qEaOOwu3qXNECHw4fHGHGTEOuhf/VdKgoDX5M/dhQ==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toml-eslint-parser@0.10.0: + resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + tough-cookie-file-store@2.0.3: + resolution: {integrity: sha512-sMpZVcmFf6EYFHFFl+SYH4W1/OnXBYMGDsv2IlbQ2caHyFElW/UR/gpj/KYU1JwmP4dE9xqwv2+vWcmlXHojSw==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} + + ts-api-utils@1.4.2: + resolution: {integrity: sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + undici@6.21.0: + resolution: {integrity: sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==} + engines: {node: '>=18.17'} + + undici@7.2.0: + resolution: {integrity: sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==} + engines: {node: '>=20.18.1'} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + wanakana@5.3.1: + resolution: {integrity: sha512-OSDqupzTlzl2LGyqTdhcXcl6ezMiFhcUwLBP8YKaBIbMYW1wAwDvupw2T9G9oVaKT9RmaSpyTXjxddFPUcFFIw==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml-eslint-parser@1.2.3: + resolution: {integrity: sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==} + engines: {node: ^14.17.0 || >=16.0.0} + + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zlibjs@0.3.1: + resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==} + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + + zx@8.2.2: + resolution: {integrity: sha512-HSIdpU5P2ONI0nssnhsUZNCH9Sd/Z8LIFk9n8QTbu6JufzJx7qR7ajrMN21s06JqWSApcN012377iWsv8Vs5bg==} + engines: {node: '>= 12.17.0'} + hasBin: true + +snapshots: + + '@antfu/eslint-config@3.10.0(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@antfu/install-pkg': 0.4.1 + '@clack/prompts': 0.8.2 + '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.15.0) + '@eslint/markdown': 6.2.1 + '@stylistic/eslint-plugin': 2.11.0(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/parser': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + '@vitest/eslint-plugin': 1.1.10(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2) + eslint: 9.15.0 + eslint-config-flat-gitignore: 0.3.0(eslint@9.15.0) + eslint-flat-config-utils: 0.4.0 + eslint-merge-processors: 0.1.0(eslint@9.15.0) + eslint-plugin-antfu: 2.7.0(eslint@9.15.0) + eslint-plugin-command: 0.2.6(eslint@9.15.0) + eslint-plugin-import-x: 4.4.3(eslint@9.15.0)(typescript@5.7.2) + eslint-plugin-jsdoc: 50.6.0(eslint@9.15.0) + eslint-plugin-jsonc: 2.18.2(eslint@9.15.0) + eslint-plugin-n: 17.14.0(eslint@9.15.0) + eslint-plugin-no-only-tests: 3.3.0 + eslint-plugin-perfectionist: 4.1.2(eslint@9.15.0)(typescript@5.7.2) + eslint-plugin-regexp: 2.7.0(eslint@9.15.0) + eslint-plugin-toml: 0.11.1(eslint@9.15.0) + eslint-plugin-unicorn: 56.0.1(eslint@9.15.0) + eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0) + eslint-plugin-vue: 9.31.0(eslint@9.15.0) + eslint-plugin-yml: 1.15.0(eslint@9.15.0) + eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.5.13)(eslint@9.15.0) + globals: 15.12.0 + jsonc-eslint-parser: 2.4.0 + local-pkg: 0.5.1 + parse-gitignore: 2.0.0 + picocolors: 1.1.1 + toml-eslint-parser: 0.10.0 + vue-eslint-parser: 9.4.3(eslint@9.15.0) + yaml-eslint-parser: 1.2.3 + yargs: 17.7.2 + transitivePeerDependencies: + - '@eslint/json' + - '@typescript-eslint/utils' + - '@vue/compiler-sfc' + - supports-color + - typescript + - vitest + + '@antfu/install-pkg@0.4.1': + dependencies: + package-manager-detector: 0.2.5 + tinyexec: 0.3.1 + + '@antfu/utils@0.7.10': {} + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@badrap/valita@0.4.2': + optional: true + + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.8.2': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@es-joy/jsdoccomment@0.48.0': + dependencies: + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 4.1.0 + + '@es-joy/jsdoccomment@0.49.0': + dependencies: + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 4.1.0 + + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.15.0)': + dependencies: + escape-string-regexp: 4.0.0 + eslint: 9.15.0 + ignore: 5.3.2 + + '@eslint-community/eslint-utils@4.4.1(eslint@9.15.0)': + dependencies: + eslint: 9.15.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/compat@1.2.3(eslint@9.15.0)': + optionalDependencies: + eslint: 9.15.0 + + '@eslint/config-array@0.19.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.0': {} + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.15.0': {} + + '@eslint/markdown@6.2.1': + dependencies: + '@eslint/plugin-kit': 0.2.3 + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.2.3': + dependencies: + levn: 0.4.1 + + '@faker-js/faker@9.3.0': {} + + '@fuman/fetch@0.0.7(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': + dependencies: + '@fuman/utils': 0.0.4 + optionalDependencies: + '@badrap/valita': 0.4.2 + tough-cookie: 5.0.0 + zod: 3.23.8 + + '@fuman/io@0.0.4': + dependencies: + '@fuman/utils': 0.0.4 + + '@fuman/net@0.0.4': + dependencies: + '@fuman/io': 0.0.4 + '@fuman/utils': 0.0.4 + + '@fuman/node@0.0.4': + dependencies: + '@fuman/io': 0.0.4 + '@fuman/net': 0.0.4 + '@fuman/utils': 0.0.4 + + '@fuman/utils@0.0.4': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@mtcute/core@0.19.1': + dependencies: + '@fuman/io': 0.0.4 + '@fuman/net': 0.0.4 + '@fuman/utils': 0.0.4 + '@mtcute/file-id': 0.19.0 + '@mtcute/tl': 196.0.0 + '@mtcute/tl-runtime': 0.19.0 + '@types/events': 3.0.0 + long: 5.2.3 + + '@mtcute/file-id@0.19.0': + dependencies: + '@fuman/utils': 0.0.4 + '@mtcute/tl-runtime': 0.19.0 + long: 5.2.3 + + '@mtcute/html-parser@0.19.0': + dependencies: + '@mtcute/core': 0.19.1 + htmlparser2: 6.1.0 + long: 5.2.3 + + '@mtcute/markdown-parser@0.19.0': + dependencies: + '@mtcute/core': 0.19.1 + long: 5.2.3 + + '@mtcute/node@0.19.1': + dependencies: + '@fuman/net': 0.0.4 + '@fuman/node': 0.0.4 + '@fuman/utils': 0.0.4 + '@mtcute/core': 0.19.1 + '@mtcute/html-parser': 0.19.0 + '@mtcute/markdown-parser': 0.19.0 + '@mtcute/wasm': 0.19.0 + better-sqlite3: 11.3.0 + + '@mtcute/tl-runtime@0.19.0': + dependencies: + '@fuman/utils': 0.0.4 + long: 5.2.3 + + '@mtcute/tl@196.0.0': + dependencies: + long: 5.2.3 + + '@mtcute/wasm@0.19.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@pkgr/core@0.1.1': {} + + '@stylistic/eslint-plugin@2.11.0(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + eslint: 9.15.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + estraverse: 5.3.0 + picomatch: 4.0.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/estree@1.0.6': {} + + '@types/events@3.0.0': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.10.0 + optional: true + + '@types/json-schema@7.0.15': {} + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.10.0 + optional: true + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@0.7.34': {} + + '@types/node@22.10.0': + dependencies: + undici-types: 6.20.0 + + '@types/normalize-package-data@2.4.4': {} + + '@types/plist@3.0.5': + dependencies: + '@types/node': 22.10.0 + xmlbuilder: 15.1.1 + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/type-utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.16.0 + eslint: 9.15.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.2(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.16.0 + debug: 4.3.7 + eslint: 9.15.0 + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.16.0': + dependencies: + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/visitor-keys': 8.16.0 + + '@typescript-eslint/type-utils@8.16.0(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + debug: 4.3.7 + eslint: 9.15.0 + ts-api-utils: 1.4.2(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.16.0': {} + + '@typescript-eslint/typescript-estree@8.16.0(typescript@5.7.2)': + dependencies: + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/visitor-keys': 8.16.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.2(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + eslint: 9.15.0 + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.16.0': + dependencies: + '@typescript-eslint/types': 8.16.0 + eslint-visitor-keys: 4.2.0 + + '@vitest/eslint-plugin@1.1.10(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + eslint: 9.15.0 + optionalDependencies: + typescript: 5.7.2 + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.26.2 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.26.2 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.14 + postcss: 8.4.49 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/shared@3.5.13': {} + + '@xmldom/xmldom@0.8.10': {} + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + are-docs-informative@0.0.2: {} + + argparse@2.0.1: {} + + async@2.6.4: + dependencies: + lodash: 4.17.21 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + better-sqlite3@11.3.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.2 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + boolbase@1.0.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001684 + electron-to-chromium: 1.5.65 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtin-modules@3.3.0: {} + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001684: {} + + ccount@2.0.1: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-entities@2.0.2: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + + cheerio@1.0.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + encoding-sniffer: 0.2.0 + htmlparser2: 9.1.0 + parse5: 7.2.1 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 6.21.0 + whatwg-mimetype: 4.0.0 + + chownr@1.1.4: {} + + ci-info@4.1.0: {} + + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + comment-parser@1.4.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + core-js-compat@3.39.0: + dependencies: + browserslist: 4.24.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + + css-what@6.1.0: {} + + cssesc@3.0.0: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + dequal@2.0.3: {} + + detect-libc@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@16.4.5: {} + + doublearray@0.0.2: {} + + electron-to-chromium@1.5.65: {} + + emoji-regex@8.0.0: {} + + encoding-sniffer@0.2.0: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.17.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-main@1.3.0: {} + + es-module-lexer@1.5.4: {} + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-compat-utils@0.5.1(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + semver: 7.6.3 + + eslint-compat-utils@0.6.3(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + semver: 7.6.3 + + eslint-config-flat-gitignore@0.3.0(eslint@9.15.0): + dependencies: + '@eslint/compat': 1.2.3(eslint@9.15.0) + eslint: 9.15.0 + find-up-simple: 1.0.0 + + eslint-flat-config-utils@0.4.0: + dependencies: + pathe: 1.1.2 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.15.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-json-compat-utils@0.2.1(eslint@9.15.0)(jsonc-eslint-parser@2.4.0): + dependencies: + eslint: 9.15.0 + esquery: 1.6.0 + jsonc-eslint-parser: 2.4.0 + + eslint-merge-processors@0.1.0(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + + eslint-plugin-antfu@2.7.0(eslint@9.15.0): + dependencies: + '@antfu/utils': 0.7.10 + eslint: 9.15.0 + + eslint-plugin-command@0.2.6(eslint@9.15.0): + dependencies: + '@es-joy/jsdoccomment': 0.48.0 + eslint: 9.15.0 + + eslint-plugin-es-x@7.8.0(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/regexpp': 4.12.1 + eslint: 9.15.0 + eslint-compat-utils: 0.5.1(eslint@9.15.0) + + eslint-plugin-import-x@4.4.3(eslint@9.15.0)(typescript@5.7.2): + dependencies: + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + debug: 4.3.7 + doctrine: 3.0.0 + eslint: 9.15.0 + eslint-import-resolver-node: 0.3.9 + get-tsconfig: 4.8.1 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + stable-hash: 0.0.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-jsdoc@50.6.0(eslint@9.15.0): + dependencies: + '@es-joy/jsdoccomment': 0.49.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint: 9.15.0 + espree: 10.3.0 + esquery: 1.6.0 + parse-imports: 2.2.1 + semver: 7.6.3 + spdx-expression-parse: 4.0.0 + synckit: 0.9.2 + transitivePeerDependencies: + - supports-color + + eslint-plugin-jsonc@2.18.2(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + eslint: 9.15.0 + eslint-compat-utils: 0.6.3(eslint@9.15.0) + eslint-json-compat-utils: 0.2.1(eslint@9.15.0)(jsonc-eslint-parser@2.4.0) + espree: 9.6.1 + graphemer: 1.4.0 + jsonc-eslint-parser: 2.4.0 + natural-compare: 1.4.0 + synckit: 0.6.2 + transitivePeerDependencies: + - '@eslint/json' + + eslint-plugin-n@17.14.0(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + enhanced-resolve: 5.17.1 + eslint: 9.15.0 + eslint-plugin-es-x: 7.8.0(eslint@9.15.0) + get-tsconfig: 4.8.1 + globals: 15.12.0 + ignore: 5.3.2 + minimatch: 9.0.5 + semver: 7.6.3 + + eslint-plugin-no-only-tests@3.3.0: {} + + eslint-plugin-perfectionist@4.1.2(eslint@9.15.0)(typescript@5.7.2): + dependencies: + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + eslint: 9.15.0 + natural-orderby: 5.0.0 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-regexp@2.7.0(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/regexpp': 4.12.1 + comment-parser: 1.4.1 + eslint: 9.15.0 + jsdoc-type-pratt-parser: 4.1.0 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + + eslint-plugin-toml@0.11.1(eslint@9.15.0): + dependencies: + debug: 4.3.7 + eslint: 9.15.0 + eslint-compat-utils: 0.5.1(eslint@9.15.0) + lodash: 4.17.21 + toml-eslint-parser: 0.10.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-unicorn@56.0.1(eslint@9.15.0): + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + ci-info: 4.1.0 + clean-regexp: 1.0.0 + core-js-compat: 3.39.0 + eslint: 9.15.0 + esquery: 1.6.0 + globals: 15.12.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + jsesc: 3.0.2 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.27 + regjsparser: 0.10.0 + semver: 7.6.3 + strip-indent: 3.0.0 + + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2) + + eslint-plugin-vue@9.31.0(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + eslint: 9.15.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@9.15.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-yml@1.15.0(eslint@9.15.0): + dependencies: + debug: 4.3.7 + eslint: 9.15.0 + eslint-compat-utils: 0.5.1(eslint@9.15.0) + lodash: 4.17.21 + natural-compare: 1.4.0 + yaml-eslint-parser: 1.2.3 + transitivePeerDependencies: + - supports-color + + eslint-processor-vue-blocks@0.1.2(@vue/compiler-sfc@3.5.13)(eslint@9.15.0): + dependencies: + '@vue/compiler-sfc': 3.5.13 + eslint: 9.15.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.15.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.0 + '@eslint/core': 0.9.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.15.0 + '@eslint/plugin-kit': 0.2.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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.4 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + espree@9.6.1: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + expand-template@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-uri-to-path@1.0.0: {} + + filesize@10.1.6: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + + flatted@3.3.2: {} + + fs-constants@1.0.0: {} + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.12.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@2.8.9: {} + + htmlparser2@10.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 6.0.0 + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdoc-type-pratt-parser@4.1.0: {} + + jsesc@0.5.0: {} + + jsesc@3.0.2: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-eslint-parser@2.4.0: + dependencies: + acorn: 8.14.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + semver: 7.6.3 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kuromoji@0.1.2: + dependencies: + async: 2.6.4 + doublearray: 0.0.2 + zlibjs: 0.3.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + local-pkg@0.5.1: + dependencies: + mlly: 1.7.3 + pkg-types: 1.2.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + long@5.2.3: {} + + longest-streak@3.1.0: {} + + magic-string@0.30.14: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.2: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-table@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.1 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.1 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.1 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.0.3: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + micromark@4.0.1: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-response@3.1.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + mlly@1.7.3: + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 + + ms@2.1.3: {} + + nanoid@3.3.8: {} + + nanoid@5.0.9: {} + + napi-build-utils@1.0.2: {} + + natural-compare@1.4.0: {} + + natural-orderby@5.0.0: {} + + node-abi@3.71.0: + dependencies: + semver: 7.6.3 + + node-releases@2.0.18: {} + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-manager-detector@0.2.5: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-gitignore@2.0.0: {} + + parse-imports@2.2.1: + dependencies: + es-module-lexer: 1.5.4 + slashes: 3.0.12 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.2.1 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.2.1 + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@1.1.2: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pkg-types@1.2.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.3 + pathe: 1.1.2 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pluralize@8.0.0: {} + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.4.49: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.2: + dependencies: + detect-libc: 2.0.3 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.71.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + + prelude-ls@1.2.1: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode@2.3.1: {} + + qrcode-terminal@0.12.0: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + refa@0.12.1: + dependencies: + '@eslint-community/regexpp': 4.12.1 + + regexp-ast-analysis@0.7.1: + dependencies: + '@eslint-community/regexpp': 4.12.1 + refa: 0.12.1 + + regexp-tree@0.1.27: {} + + regjsparser@0.10.0: + dependencies: + jsesc: 0.5.0 + + require-directory@2.1.1: {} + + requires-port@1.0.0: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.0.4: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.12.1 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + + semver@5.7.2: {} + + semver@7.6.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + sisteransi@1.0.5: {} + + slashes@3.0.12: {} + + source-map-js@1.2.1: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.20 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.20 + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.20 + + spdx-license-ids@3.0.20: {} + + stable-hash@0.0.4: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + synckit@0.6.2: + dependencies: + tslib: 2.8.1 + + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + + tapable@2.2.1: {} + + tar-fs@2.1.1: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tinyexec@0.3.1: {} + + tldts-core@6.1.66: {} + + tldts@6.1.66: + dependencies: + tldts-core: 6.1.66 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toml-eslint-parser@0.10.0: + dependencies: + eslint-visitor-keys: 3.4.3 + + tough-cookie-file-store@2.0.3: + dependencies: + tough-cookie: 4.1.4 + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tough-cookie@5.0.0: + dependencies: + tldts: 6.1.66 + + ts-api-utils@1.4.2(typescript@5.7.2): + dependencies: + typescript: 5.7.2 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.6.0: {} + + type-fest@0.8.1: {} + + typescript@5.7.2: {} + + ufo@1.5.4: {} + + undici-types@6.20.0: {} + + undici@6.21.0: {} + + undici@7.2.0: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@0.2.0: {} + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vue-eslint-parser@9.4.3(eslint@9.15.0): + dependencies: + debug: 4.3.7 + eslint: 9.15.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + wanakana@5.3.1: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xml-name-validator@4.0.0: {} + + xmlbuilder@15.1.1: {} + + y18n@5.0.8: {} + + yaml-eslint-parser@1.2.3: + dependencies: + eslint-visitor-keys: 3.4.3 + lodash: 4.17.21 + yaml: 2.6.1 + + yaml@2.6.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zlibjs@0.3.1: {} + + zod@3.23.8: {} + + zwitch@2.0.4: {} + + zx@8.2.2: + optionalDependencies: + '@types/fs-extra': 11.0.4 + '@types/node': 22.10.0 diff --git a/scripts/auth/mtcute-login.ts b/scripts/auth/mtcute-login.ts new file mode 100644 index 0000000..0bf4af9 --- /dev/null +++ b/scripts/auth/mtcute-login.ts @@ -0,0 +1,20 @@ +import qrTerminal from 'qrcode-terminal' +import { createTg } from '../../utils/telegram.ts' + +const sessionName = process.argv[2] +if (!sessionName) { + console.error('Usage: mtcute-login.ts ') + process.exit(1) +} + +const tg = createTg(sessionName) + +const self = await tg.start({ + qrCodeHandler(url, expires) { + console.log(qrTerminal.generate(url, { small: true })) + }, +}) + +console.log(`Logged in as ${self.displayName} (${self.id})`) + +await tg.close() diff --git a/scripts/infra/navidrome-find-duplicates.ts b/scripts/infra/navidrome-find-duplicates.ts new file mode 100644 index 0000000..3c89971 --- /dev/null +++ b/scripts/infra/navidrome-find-duplicates.ts @@ -0,0 +1,105 @@ +import type { NavidromeSong } from '../../utils/navidrome.ts' +import { createRequire } from 'node:module' + +import { join } from 'node:path' +import kuromoji from 'kuromoji' +import { isKana, toRomaji } from 'wanakana' + +import { fetchSongs, navidromeFfetch as ffetch } from '../../utils/navidrome.ts' + +const WHITELIST_KEYS = new Set([ + // actual different tracks with the same title + '["sorry about my face","untitled track"]', + '["kooeetekumogeemusu","neko bushou sengoku emaki"]', + '["eve","merufuakutorii"]', + // todo + '["arm","legend of zelda"]', + '["arm","tomorrow heart beat ~ ashita anata ni dokkidokiā˜† ~"]', + '["dwat","rotladatormarf"]', + '["fujiwara mari sai","zenbuatashinokawaiino"]', +]) + +const moji = await new Promise((resolve, reject) => { + kuromoji.builder({ + dicPath: join(createRequire(import.meta.url).resolve('kuromoji/'), '../../dict'), + }).build((err, tokenizer) => { + if (err) return reject(err) + resolve(tokenizer) + }) +}) + +function clean(s: string) { + const str = s.toLowerCase() + .replace(/\(Explicit\)/i, '') + .replace(/[!@#$%^&*()_+=[\]{}\\|/,.;':"<>`~-]/g, '') + + if (str.match(/[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFF9F\u4E00-\u9FAF\u3400-\u4DBF]/)) { + // has japanese + const tokens = moji.tokenize(str) + + let res = '' + + for (const token of tokens) { + if (token.word_type === 'UNKNOWN') { + res += isKana(token.surface_form) ? toRomaji(token.surface_form) : token.surface_form + } else if (token.word_type === 'KNOWN') { + res += `${toRomaji(token.reading)} ` + } + } + + return res.trimEnd() + } + + return str +} + +const CHUNK_SIZE = 1000 + +function getSongKey(song: NavidromeSong) { + return JSON.stringify([ + clean(song.artist), + clean(song.title), + ]) +} + +const seen = new Map() + +for (let offset = 0; ; offset += CHUNK_SIZE) { + const songs = await fetchSongs(offset, CHUNK_SIZE) + if (songs.length === 0) break + + for (const song of songs) { + const key = getSongKey(song) + if (WHITELIST_KEYS.has(key)) continue + let arr = seen.get(key) + if (!arr) { + arr = [] + seen.set(key, arr) + } + + arr.push(song) + } + + console.log('āŒ› fetched chunk %d (%d items)', Math.floor(offset / CHUNK_SIZE), songs.length) +} + +const keysSorted = Array.from(seen.keys()).sort() + +let duplicates = 0 +for (const key of keysSorted) { + const arr = seen.get(key)! + if (arr.length === 1) continue + + duplicates += 1 + console.log() + console.log('found duplicates for %s:', key) + for (const song of arr) { + console.log(' %s - %s (from %s - %s) (at %s)', song.artist, song.title, song.albumArtist, song.album, song.path) + } +} + +if (duplicates === 0) { + console.log('āœ… no duplicates found') +} else { + console.log('🚨 %d duplicates found', duplicates) +} diff --git a/scripts/infra/navidrome-remux-m4a.ts b/scripts/infra/navidrome-remux-m4a.ts new file mode 100644 index 0000000..92b1e30 --- /dev/null +++ b/scripts/infra/navidrome-remux-m4a.ts @@ -0,0 +1,66 @@ +import { readFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { $ } from 'zx' +import { downloadStream } from '../../utils/fetch.ts' +import { getEnv } from '../../utils/misc.ts' +import { fetchSongs } from '../../utils/navidrome.ts' +import { WebdavClient } from '../../utils/webdav.ts' + +const webdav = new WebdavClient({ + baseUrl: getEnv('NAVIDROME_WEBDAV_ENDPOINT'), + username: getEnv('NAVIDROME_WEBDAV_USERNAME'), + password: getEnv('NAVIDROME_WEBDAV_PASSWORD'), +}) + +const CHUNK_SIZE = 1000 +for (let offset = 0; ; offset += CHUNK_SIZE) { + const songs = await fetchSongs(offset, CHUNK_SIZE) + if (songs.length === 0) break + + for (const song of songs) { + const ext = song.path.split('.').pop()! + if (ext !== 'm4a') continue + + console.log('āŒ song %s is m4a, remuxing...', song.path) + const webdavPath = song.path.replace('/music/s3/', '/') + const res = await webdav.get(webdavPath).catch(() => null) + + if (!res) { + console.log(' āŒ failed to get %s', webdavPath) + continue + } + + const tmpfile = join('assets', `${song.id}.m4a`) + await downloadStream(res.body!, tmpfile) + console.log(' - downloaded to %s', tmpfile) + + const probe = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${tmpfile}`.json() + const audioStream = probe.streams.find(stream => stream.codec_type === 'audio') + if (!audioStream) { + console.log(' āŒ no audio stream found') + await rm(tmpfile) + continue + } + const codec = audioStream.codec_name + + if (codec !== 'flac') { + console.log(` āŒ audio stream is ${codec}, not flac, skipping`) + await rm(tmpfile) + continue + } + + console.log(' - audio stream is flac, remuxing') + + // remux + const remuxed = join('assets', `${song.id}.flac`) + await rm(remuxed, { force: true }) + await $`ffmpeg -i ${tmpfile} -c:a copy ${remuxed}`.quiet(true) + console.log(' - remuxed to %s', remuxed) + await rm(tmpfile) + + await webdav.put(webdavPath.replace('.m4a', '.flac'), await readFile(remuxed)) + await webdav.delete(webdavPath) + console.log(' - uploaded to %s', webdavPath.replace('.m4a', '.flac')) + await rm(remuxed) + } +} diff --git a/scripts/infra/slskd-total-upload.ts b/scripts/infra/slskd-total-upload.ts new file mode 100644 index 0000000..8ed9680 --- /dev/null +++ b/scripts/infra/slskd-total-upload.ts @@ -0,0 +1,39 @@ +import { filesize } from 'filesize' +import { z } from 'zod' + +import { ffetch } from '../../utils/fetch.ts' +import { getEnv } from '../../utils/misc.ts' + +const res = await ffetch('/api/v0/transfers/uploads', { + baseUrl: getEnv('SLSKD_ENDPOINT'), + headers: { + cookie: getEnv('SLSKD_COOKIE'), + }, +}).parsedJson(z.array( + z.object({ + username: z.string(), + directories: z.array(z.object({ + directory: z.string(), + fileCount: z.number(), + files: z.array(z.object({ + id: z.string(), + filename: z.string(), + state: z.string(), + bytesTransferred: z.number(), + })), + })), + }), +)) + +let total = 0 + +for (const user of res) { + for (const dir of user.directories) { + for (const file of dir.files) { + if (file.state !== 'Completed, Succeeded') continue + total += file.bytesTransferred + } + } +} + +console.log(filesize(total)) diff --git a/scripts/media/deezer-art-fetcher.ts b/scripts/media/deezer-art-fetcher.ts new file mode 100644 index 0000000..fc7e4ef --- /dev/null +++ b/scripts/media/deezer-art-fetcher.ts @@ -0,0 +1,58 @@ +import { iter } from '@fuman/utils' +import { z } from 'zod' +import { minimist, question } from 'zx' + +import { downloadFile, ffetch } from '../../utils/fetch.ts' + +const args = minimist(process.argv.slice(2), { + string: ['filename'], +}) + +const query = args._[0] ?? await question('Search query (Artist - Album): ') + +const data = await ffetch('https://api.deezer.com/search', { + query: { + q: query, + limit: 15, + }, +}).parsedJson(z.object({ + data: z.array(z.object({ + type: z.literal('track'), + title: z.string(), + artist: z.object({ + name: z.string(), + }), + album: z.object({ + id: z.number(), + title: z.string(), + cover_xl: z.string(), + }), + })), +})) + +const groupedByAlbum = new Map() +for (const result of data.data) { + const albumId = result.album.id + if (!groupedByAlbum.has(albumId)) { + groupedByAlbum.set(albumId, []) + } + + groupedByAlbum.get(albumId)!.push(result) +} + +const idxToAlbum = new Map() +for (const [idx, [id, tracks]] of iter.enumerate(groupedByAlbum.entries())) { + idxToAlbum.set(idx, id) + console.log(`${idx + 1}. ${tracks[0].artist.name} - ${tracks[0].album.title}`) + for (const track of tracks) { + console.log(` ${track.title}`) + } +} + +console.log('Enter number to download album art:') + +const number = Number.parseInt(await question('[1] > ') || '1') + +const artworkUrl = groupedByAlbum.get(idxToAlbum.get(number - 1)!)![0].album.cover_xl + +await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`) diff --git a/scripts/media/ffmpeg-clipper.ts b/scripts/media/ffmpeg-clipper.ts new file mode 100644 index 0000000..bfa3b4e --- /dev/null +++ b/scripts/media/ffmpeg-clipper.ts @@ -0,0 +1,129 @@ +import { rm } from 'node:fs/promises' + +import { $, question } from 'zx' + +import { fileExists } from '../../utils/fs.ts' + +let filename = await question('filename >')! +const startTs = await question('start timestamp >') +const endTs = await question('end timestamp >') +const outputFilename = await question('output filename [output.mp4] >') || 'assets/output.mp4' + +if (filename[0] === '\'' && filename[filename.length - 1] === '\'') { + filename = filename.slice(1, -1) +} + +const ffprobe = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${filename}`.json() + +async function chooseStream(type: string, options: any[], allowNone = false) { + console.log(`Found ${type} streams:`) + for (let i = 0; i < options.length; i++) { + const stream = options[i] + console.log(`[${i + 1}] (${stream.codec_name}, ${stream.tags.language}) ${stream.tags.title}`) + } + + if (allowNone) { + console.log(`[0] No ${type}`) + } + + const res = await question(`select ${type} >`) || '0' + if (res === '0' && allowNone) { + return null + } + + const streamIndex = Number.parseInt(res) + if (Number.isNaN(streamIndex) || streamIndex < 1 || streamIndex > options.length) { + console.error('Invalid input') + process.exit(1) + } + + return streamIndex - 1 +} + +const allVideos = ffprobe.streams.filter(stream => stream.codec_type === 'video') +const allAudios = ffprobe.streams.filter(stream => stream.codec_type === 'audio') +const allSubtitles = ffprobe.streams.filter(stream => stream.codec_type === 'subtitle') + +let videoStream: number | null = null +let audioStream: number | null = null +let subtitleStream: number | null = null + +if (allVideos.length > 1) { + videoStream = await chooseStream('video', allVideos) +} else if (allVideos.length > 0) { + videoStream = 0 +} else { + console.error('No video streams found') + process.exit(1) +} + +if (allAudios.length > 1) { + audioStream = await chooseStream('audio', allAudios) +} else if (allAudios.length > 0) { + audioStream = 0 +} else { + console.warn('No audio streams found, proceeding without audio') +} + +if (allSubtitles.length > 0) { + subtitleStream = await chooseStream('subtitle', allSubtitles, true) +} + +const args: string[] = [ + '-i', + filename, + '-c:v', + 'libx264', + '-map', + `0:v:${videoStream}`, + '-c:v', + 'libx264', +] + +if (audioStream !== null) { + args.push('-map', `0:a:${audioStream}`) +} + +if (subtitleStream !== null) { + const filenameEscaped = filename.replace(/'/g, "'\\\\\\''") + args.push('-vf', `format=yuv420p,subtitles='${filenameEscaped}':si=${subtitleStream}`) +} else { + args.push('-vf', 'format=yuv420p') +} + +if (audioStream !== null) { + args.push('-c:a', 'libopus') + + if (allAudios[audioStream].codec_name === 'flac') { + args.push('-b:a', '320k') + } +} + +args.push( + '-ss', + startTs!, + '-to', + endTs!, + outputFilename, +) + +if (await fileExists(outputFilename)) { + const overwrite = await question('Output file already exists, overwrite? [y/N] >') + if (overwrite?.toLowerCase() !== 'y') { + process.exit(0) + } + + await rm(outputFilename) +} + +try { + $.env.AV_LOG_FORCE_COLOR = 'true' + await $`ffmpeg ${args}` +} catch (e) { + process.exit(1) +} + +const openDir = await question('open output directory? [Y/n] >') +if (!openDir || openDir?.toLowerCase() === 'y') { + await $`open -R ${outputFilename}` +} diff --git a/scripts/media/itunes-art-fetcher.ts b/scripts/media/itunes-art-fetcher.ts new file mode 100644 index 0000000..eef531e --- /dev/null +++ b/scripts/media/itunes-art-fetcher.ts @@ -0,0 +1,46 @@ +import { iter } from '@fuman/utils' +import { z } from 'zod' +import { minimist, question } from 'zx' + +import { downloadFile, ffetch } from '../../utils/fetch.ts' + +const args = minimist(process.argv.slice(2), { + string: ['entity', 'filename'], +}) + +const entity = args.entity ?? 'album' +const query = args._[0] ?? await question('Search query (Artist - Album): ') + +const data = await ffetch('https://itunes.apple.com/search', { + query: { + term: query, + entity, + limit: 15, + }, +}).parsedJson(z.object({ + results: z.array(z.object({ + kind: z.literal('song').optional(), + artistName: z.string(), + collectionName: z.string(), + artworkUrl100: z.string(), + releaseDate: z.string(), + trackName: z.string().optional(), + }).passthrough()), +})) + +for (const [i, result] of iter.enumerate(data.results)) { + if (result.kind === 'song') { + console.log(`${i + 1}. ${result.artistName} - ${result.trackName} (${result.collectionName}, ${new Date(result.releaseDate).toLocaleDateString('ru-RU')})`) + continue + } + + console.log(`${i + 1}. ${result.artistName} - ${result.collectionName} (${new Date(result.releaseDate).toLocaleDateString('ru-RU')})`) +} + +console.log('Enter number to download album art:') + +const number = Number.parseInt(await question('[1] > ') || '1') + +const artworkUrl = data.results[number - 1].artworkUrl100.replace('100x100', '1500x1500') + +await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`) diff --git a/scripts/media/itunes-artist-art-fetcher.ts b/scripts/media/itunes-artist-art-fetcher.ts new file mode 100644 index 0000000..024298d --- /dev/null +++ b/scripts/media/itunes-artist-art-fetcher.ts @@ -0,0 +1,63 @@ +import { iter } from '@fuman/utils' +import { z } from 'zod' +import { minimist, question } from 'zx' + +import { downloadFile, ffetch } from '../../utils/fetch.ts' + +const args = minimist(process.argv.slice(2), { + string: ['filename'], +}) + +const query = args._[0] ?? await question('Search query: ') + +const data = await ffetch('https://itunes.apple.com/search', { + query: { + term: query, + entity: 'musicArtist', + limit: 15, + }, +}).parsedJson(z.object({ + results: z.array(z.object({ + wrapperType: z.literal('artist'), + artistName: z.string(), + artistLinkUrl: z.string(), + primaryGenreName: z.string().default('Unknown'), + }).passthrough()), +})) + +for (const [i, result] of iter.enumerate(data.results)) { + console.log(`${i + 1}. ${result.artistName} (${result.primaryGenreName})`) + continue +} + +console.log('Enter number to download artist art:') + +const number = Number.parseInt(await question('[1] > ') || '1') + +const pageUrl = data.results[number - 1].artistLinkUrl +const $ = await ffetch(pageUrl).cheerio() +const pageData = JSON.parse($('#serialized-server-data').html()!) + +const pageDataValidated = z.tuple([ + z.object({ + data: z.object({ + seoData: z.object({ + artworkUrl: z.string(), + }), + }), + }), +]).parse(pageData) + +// {w}x{h}{c}.{f} +const artworkUrl = pageDataValidated[0].data.seoData.artworkUrl + .replace('{w}', '2500') + .replace('{h}', '2500') + .replace('{c}', 'cc') + .replace('{f}', 'jpg') + +if (artworkUrl === '/assets/meta/apple-music.png') { + console.log('No artwork available') + process.exit(1) +} + +await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`) diff --git a/scripts/misc/update-forkgram.ts b/scripts/misc/update-forkgram.ts new file mode 100644 index 0000000..878def9 --- /dev/null +++ b/scripts/misc/update-forkgram.ts @@ -0,0 +1,51 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import plist from 'plist' +import { z } from 'zod' +import { $ } from 'zx' +import { ffetch } from '../../utils/fetch.ts' + +const latestVerInfo = await ffetch('https://api.github.com/repos/forkgram/tdesktop/releases/latest').parsedJson( + z.object({ + tag_name: z.string().transform(v => v.replace(/^v/, '')), + assets: z.array(z.object({ + name: z.string(), + browser_download_url: z.string(), + })), + }), +) + +const INSTALL_PATH = '/Applications/Forkgram.app' + +console.log('latest version:', latestVerInfo.tag_name) + +const installedPlist = await readFile(join(INSTALL_PATH, 'Contents/Info.plist'), 'utf8') +const installedPlistParsed = z.object({ + CFBundleShortVersionString: z.string(), +}).parse(plist.parse(installedPlist)) + +console.log('installed version:', installedPlistParsed.CFBundleShortVersionString) + +if (installedPlistParsed.CFBundleShortVersionString === latestVerInfo.tag_name) { + console.log('āœ… no update needed') + process.exit(0) +} + +const arm64Asset = latestVerInfo.assets.find(asset => asset.name === 'Forkgram.macOS.no.auto-update_arm64.zip') +if (!arm64Asset) { + console.error('āŒ no arm64 asset found') + process.exit(1) +} + +console.log('installing new version...') +await $`curl -L ${arm64Asset.browser_download_url} -o /tmp/forkgram.zip` +await $`unzip -o /tmp/forkgram.zip -d /tmp/forkgram` +await $`kill -9 $(pgrep -f /Applications/Forkgram.app/Contents/MacOS/Telegram)` +await $`rm -rf ${INSTALL_PATH}` +await $`mv /tmp/forkgram/Telegram.app ${INSTALL_PATH}` +await $`rm -rf /tmp/forkgram` +await $`xattr -cr ${INSTALL_PATH}` + +await $`open ${INSTALL_PATH}` + +console.log('āœ… done') diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b75efba --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleDetection": "force", + "module": "ESNext", + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "allowJs": true, + + // Best practices + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": false, + + "noPropertyAccessFromIndexSignature": false, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noEmit": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + } +} diff --git a/utils/captcha.ts b/utils/captcha.ts new file mode 100644 index 0000000..11d4f60 --- /dev/null +++ b/utils/captcha.ts @@ -0,0 +1,87 @@ +import { sleep } from '@fuman/utils' +import { z } from 'zod' +import { ffetch } from './fetch.ts' +import { getEnv } from './misc.ts' + +const CreateTaskResponse = z.object({ + errorId: z.number(), + errorCode: z.string().optional().nullable(), + taskId: z.number(), +}) + +const GetTaskResultResponse = z.object({ + errorId: z.number(), + errorCode: z.string().optional().nullable(), + status: z.enum(['ready', 'processing']), + solution: z.unknown().optional(), +}) + +export async function solveCaptcha(task: unknown) { + const res = await ffetch.post('https://api.capmonster.cloud/createTask', { + json: { + clientKey: getEnv('CAPMONSTER_API_TOKEN'), + task, + }, + }).parsedJson(CreateTaskResponse) + + if (res.errorId) { + throw new Error(`createTask error ${res.errorId}: ${res.errorCode}`) + } + + const taskId = res.taskId + + await sleep(5_000) + + let requestCount = 0 + + while (true) { + requestCount += 1 + if (requestCount > 100) { + // "Limit: 120 requests per task. If the limit is exceeded, the user's account may be temporarily locked." + // just to be safe + throw new Error('captcha request count exceeded') + } + + const res = await ffetch.post('https://api.capmonster.cloud/getTaskResult', { + json: { + clientKey: getEnv('CAPMONSTER_API_TOKEN'), + taskId, + }, + }).parsedJson(GetTaskResultResponse) + + if (res.errorId) { + throw new Error(`getTaskResult error ${res.errorId}: ${res.errorCode}`) + } + + if (res.status === 'ready') { + return res.solution + } + + await sleep(2_000) + } +} + +export async function solveRecaptcha(params?: { + url: string + siteKey: string + s?: string + userAgent?: string + cookies?: string + isInvisible?: boolean +}) { + const res = await solveCaptcha({ + type: 'RecaptchaV2TaskProxyless', + websiteURL: params?.url, + websiteKey: params?.siteKey, + recaptchaDataSValue: params?.s, + userAgent: params?.userAgent, + cookies: params?.cookies, + isInvisible: params?.isInvisible, + }) + + if (typeof res !== 'object' || !res || !('gRecaptchaResponse' in res) || typeof res.gRecaptchaResponse !== 'string') { + throw new Error('invalid recaptcha response') + } + + return res.gRecaptchaResponse +} diff --git a/utils/currency.ts b/utils/currency.ts new file mode 100644 index 0000000..f345557 --- /dev/null +++ b/utils/currency.ts @@ -0,0 +1,113 @@ +import { asyncPool } from '@fuman/utils' + +import { z } from 'zod' +import { ffetch } from './fetch.ts' +import { getEnv } from './misc.ts' + +// token management +const TOKENS = getEnv('OXR_TOKENS').split(',') +// api token => requests remaining +const usageAvailable = new Map() +function getToken() { + // find token with the most requests remaining + const token = TOKENS.find(t => usageAvailable.get(t)! > 0) + if (!token) throw new Error('no tokens available') + + // consume 1 request + usageAvailable.set(token, usageAvailable.get(token)! - 1) + + return token +} + +// base => other => value +// NB: ideally we should have expiration and persistence on this +const data = new Map>() + +async function fetchMissingPairs(list: { from: string, to: string }[]) { + const missing = list.filter(c => !data.has(c.from) && !data.has(c.to) && c.from !== c.to) + if (missing.length === 0) return + + const basesToFetch = new Set() + + for (const { from, to } of missing) { + if (!basesToFetch.has(from) && !basesToFetch.has(to)) { + basesToFetch.add(from) + } + } + + if (!usageAvailable.size) { + // NB: ideally we should lock here for a production-ready implementation + + // fetch usage for all tokens + await asyncPool(TOKENS, async (token) => { + const res = await ffetch('https://openexchangerates.org/api/usage.json', { + query: { + app_id: token, + }, + }).parsedJson(z.object({ + status: z.literal(200), + data: z.object({ + app_id: z.string(), + status: z.literal('active'), + usage: z.object({ + requests_remaining: z.number(), + }), + }), + })) + + usageAvailable.set(token, res.data.usage.requests_remaining) + }, { onError: () => 'ignore' }) + + if (!usageAvailable.size) { + throw new Error('failed to fetch usage, are all tokens dead?') + } + } + + // console.log('will fetch bases:', [...basesToFetch]) + + await asyncPool(basesToFetch, async (base) => { + const res = await ffetch('https://openexchangerates.org/api/latest.json', { + query: { + app_id: getToken(), + }, + }).parsedJson(z.object({ + rates: z.record(z.string(), z.number()), + })) + + data.set(base, res.rates) + }) +} + +export async function convertCurrenciesBatch(list: { from: string, to: string, amount: number }[]) { + await fetchMissingPairs(list) + const ret: { from: string, to: string, amount: number, converted: number }[] = [] + + for (const { from, to, amount } of list) { + let result: number + + if (from === to) { + result = amount + } else if (data.has(from)) { + const rate = data.get(from)![to]! + if (!rate) throw new Error(`rate unavailable: ${from} -> ${to}`) + result = amount * rate + // console.log('converted from', from, 'to', to, 'amount', amount, 'result', result, 'rate', rate) + } else if (data.has(to)) { + const rate = data.get(to)![from]! + if (!rate) throw new Error(`rate unavailable: ${from} -> ${to}`) + result = amount / rate + // console.log('converted rev from', from, 'to', to, 'amount', amount, 'result', result, 'rate', rate) + } else { + throw new Error(`rate unavailable: ${from} -> ${to}`) + } + + ret.push({ + from, + to, + amount, + converted: result, + }) + } + + return ret +} diff --git a/utils/fetch.ts b/utils/fetch.ts new file mode 100644 index 0000000..6a8de33 --- /dev/null +++ b/utils/fetch.ts @@ -0,0 +1,37 @@ +import { createWriteStream } from 'node:fs' + +import { type FfetchAddon, ffetchAddons, ffetchBase, type FfetchResultInternals } from '@fuman/fetch' +import { toughCookieAddon } from '@fuman/fetch/tough' +import { ffetchZodAdapter } from '@fuman/fetch/zod' +import { webReadableToFuman, write } from '@fuman/io' +import { nodeWritableToFuman } from '@fuman/node' +import { type CheerioAPI, load } from 'cheerio' + +const cheerioAddon: FfetchAddon Promise }> = { + response: { + async cheerio(this: FfetchResultInternals) { + this._headers ??= {} + this._headers.Accept ??= 'text/html; charset=utf-8' + return load(await this.text()) + }, + }, +} + +export const ffetch = ffetchBase.extend({ + addons: [ + ffetchAddons.parser(ffetchZodAdapter()), + cheerioAddon, + toughCookieAddon(), + ], +}) + +export async function downloadStream(stream: ReadableStream, path: string) { + const file = nodeWritableToFuman(createWriteStream(path)) + await write.pipe(file, webReadableToFuman(stream)) + file.close() +} + +export async function downloadFile(url: string, path: string, extra?: Parameters[1]) { + const stream = await ffetch(url, extra).stream() + await downloadStream(stream, path) +} diff --git a/utils/fs.ts b/utils/fs.ts new file mode 100644 index 0000000..90834ab --- /dev/null +++ b/utils/fs.ts @@ -0,0 +1,19 @@ +import * as fsp from 'node:fs/promises' + +export async function fileExists(path: string): Promise { + try { + const stat = await fsp.stat(path) + return stat.isFile() + } catch { + return false + } +} + +export async function directoryExists(path: string): Promise { + try { + const stat = await fsp.stat(path) + return stat.isDirectory() + } catch { + return false + } +} diff --git a/utils/misc.ts b/utils/misc.ts new file mode 100644 index 0000000..e38398d --- /dev/null +++ b/utils/misc.ts @@ -0,0 +1,10 @@ +import 'dotenv/config' + +export function getEnv(key: string): string +export function getEnv(key: string, parser: (value: string) => T): T +export function getEnv(key: string, parser?: (value: string) => T): T | string { + const value = process.env[key] + if (!value) throw new Error(`env variable ${key} not found`) + if (!parser) return value + return parser(value) +} diff --git a/utils/navidrome.ts b/utils/navidrome.ts new file mode 100644 index 0000000..be66efb --- /dev/null +++ b/utils/navidrome.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' +import { ffetch as ffetchBase } from './fetch.ts' +import { getEnv } from './misc.ts' + +export const navidromeFfetch = ffetchBase.extend({ + baseUrl: getEnv('NAVIDROME_ENDPOINT'), + headers: { + 'x-nd-authorization': `Bearer ${getEnv('NAVIDROME_TOKEN')}`, + }, +}) + +export const NavidromeSong = z.object({ + id: z.string(), + title: z.string(), + album: z.string(), + albumArtist: z.string(), + artist: z.string(), + path: z.string(), + duration: z.number(), +}) +export type NavidromeSong = z.infer + +export function fetchSongs(offset: number, pageSize: number) { + return navidromeFfetch('/api/song', { + query: { + _start: offset, + _end: offset + pageSize, + _order: 'ASC', + _sort: 'title', + }, + }).parsedJson(z.array(NavidromeSong)) +} diff --git a/utils/oauth.ts b/utils/oauth.ts new file mode 100644 index 0000000..98c31f6 --- /dev/null +++ b/utils/oauth.ts @@ -0,0 +1,78 @@ +import type { MaybePromise } from '@fuman/utils' +import * as fsp from 'node:fs/promises' +import { z } from 'zod' + +export interface OauthStorage { + write: (value: string) => MaybePromise + read: () => MaybePromise +} + +export class LocalOauthStorage implements OauthStorage { + constructor(private filename: string) {} + + async write(value: string) { + await fsp.writeFile(this.filename, value) + } + + async read() { + try { + return await fsp.readFile(this.filename, 'utf8') + } catch (e) { + return null + } + } +} + +const OauthState = z.object({ + accessToken: z.string(), + refreshToken: z.string().optional(), + expiresAt: z.number(), +}) +type OauthState = z.infer + +export class OauthHandler { + constructor(private params: { + storage: OauthStorage + refreshToken: (refreshToken: string) => MaybePromise<{ + accessToken: string + refreshToken: string + expiresIn: number + }> + /** number of milliseconds to subtract from token expiration time */ + jitter?: number + }) { + this.params.jitter = this.params.jitter ?? 5000 + } + + #cache: OauthState | null = null + async readOauthState() { + if (this.#cache) return this.#cache + const value = await this.params.storage.read() + if (!value) return null + + return OauthState.parse(JSON.parse(value)) + } + + async writeOauthState(value: OauthState) { + this.#cache = value + await this.params.storage.write(JSON.stringify(value)) + } + + async getAccessToken() { + const state = await this.readOauthState() + if (!state) return null + + if (state.expiresAt < Date.now() + this.params.jitter!) { + if (!state.refreshToken) return null + const { accessToken, refreshToken, expiresIn } = await this.params.refreshToken(state.refreshToken) + await this.writeOauthState({ + accessToken, + refreshToken, + expiresAt: Date.now() + expiresIn * 1000, + }) + return accessToken + } + + return state.accessToken + } +} diff --git a/utils/telegram.ts b/utils/telegram.ts new file mode 100644 index 0000000..d10733f --- /dev/null +++ b/utils/telegram.ts @@ -0,0 +1,11 @@ +import { TelegramClient, type TelegramClientOptions } from '@mtcute/node' +import { getEnv } from './misc.ts' + +export function createTg(session: string, extra?: Partial) { + return new TelegramClient({ + apiId: getEnv('TELEGRAM_API_ID', Number), + apiHash: getEnv('TELEGRAM_API_HASH'), + storage: `assets/${session}.session`, + ...extra, + }) +} diff --git a/utils/webdav.ts b/utils/webdav.ts new file mode 100644 index 0000000..fe7aad2 --- /dev/null +++ b/utils/webdav.ts @@ -0,0 +1,324 @@ +import { ffetchBase, type FfetchResult } from '@fuman/fetch' +import { asNonNull, assert, base64, utf8 } from '@fuman/utils' +import { Parser } from 'htmlparser2' +import { z } from 'zod' + +const XML_HEADER = '' + +export interface WebdavClientOptions { + baseUrl: string + username?: string + password?: string + headers?: Record +} + +export interface WebdavResourceBase { + href: string + name: string + status: string + lastModified?: Date + raw: Record + // todo: lockdiscovery + // todo: supportedlock +} + +export interface WebdavCollection extends WebdavResourceBase { + type: 'collection' +} + +export interface WebdavFile extends WebdavResourceBase { + type: 'file' + size: number + etag?: string + contentType?: string +} + +export type WebdavResource = WebdavCollection | WebdavFile + +const DResponseSchema = z.object({ + 'd:href': z.string(), + 'd:propstat': z.object({ + 'd:prop': z.object({ + 'd:resourcetype': z.union([ + z.literal(true), + z.object({ + 'd:collection': z.literal(true), + }), + ]), + 'd:displayname': z.union([z.literal(true), z.string()]), + 'd:getcontentlength': z.coerce.number().optional(), + 'd:getlastmodified': z.string().transform(v => new Date(v)).optional(), + 'd:getetag': z.string().optional(), + 'd:getcontenttype': z.string().optional(), + }).passthrough(), + 'd:status': z.string(), + }), +}) + +const DMultistatusSchema = z.object({ + 'd:multistatus': z.tuple([z.object({ + 'd:response': z.array(DResponseSchema), + })]), +}) + +function escapeXml(str: string) { + return str.replace(//g, '>') +} + +function xmlToJson(xml: string) { + const res: Record = {} + + const stack: any[] = [res] + + const parser = new Parser({ + onopentag(name) { + name = name.toLowerCase() + + const node: any = {} + const top = stack[stack.length - 1] + if (!top[name]) { + top[name] = [] + } + top[name].push(node) + stack.push(node) + }, + onclosetag(name) { + const obj = stack.pop() + const top = stack[stack.length - 1] + const ourIdx = top[name].length - 1 + + const keys = Object.keys(obj) + if (keys.length === 1 && keys[0] === '_text') { + top[name][ourIdx] = obj._text + } else if (keys.length === 0) { + top[name][ourIdx] = true + } else { + // replace one-element arrays with the element itself + for (const key of keys) { + if (key === 'd:response') continue + const val = obj[key] + if (Array.isArray(val) && val.length === 1) { + obj[key] = val[0] + } + } + } + }, + ontext(text) { + const top = stack[stack.length - 1] + if (top._text === undefined) { + top._text = '' + } + top._text += text + }, + }) + + parser.write(xml) + parser.end() + + return res +} + +export class WebdavClient { + readonly ffetch: typeof ffetchBase + readonly basePath + + constructor(options: WebdavClientOptions) { + const headers: Record = { + 'Content-Type': 'application/xml; charset="utf-8"', + ...options.headers, + } + if (options.username) { + let authStr = options.username + if (options.password) { + authStr += `:${options.password}` + } + headers.Authorization = `Basic ${base64.encode(utf8.encoder.encode(authStr))}` + } + + this.ffetch = ffetchBase.extend({ + baseUrl: options.baseUrl, + headers, + }) + this.basePath = new URL(options.baseUrl).pathname + if (this.basePath[this.basePath.length - 1] !== '/') { + this.basePath += '/' + } + } + + mapPropfindResponse = (obj: z.infer): WebdavResource => { + const name = obj['d:propstat']['d:prop']['d:displayname'] + const base: WebdavResourceBase = { + href: obj['d:href'], + name: name === true ? '' : name, + status: obj['d:propstat']['d:status'], + lastModified: obj['d:propstat']['d:prop']['d:getlastmodified'], + raw: obj['d:propstat']['d:prop'], + } + if (base.href.startsWith(this.basePath)) { + base.href = base.href.slice(this.basePath.length) + if (base.href !== '/') { + base.href = `/${base.href}` + } + } + + if (typeof obj['d:propstat']['d:prop']['d:resourcetype'] === 'object' && obj['d:propstat']['d:prop']['d:resourcetype']['d:collection']) { + const res = base as WebdavCollection + res.type = 'collection' + return res + } else { + const res = base as WebdavFile + res.type = 'file' + res.size = asNonNull(obj['d:propstat']['d:prop']['d:getcontentlength']) + res.etag = obj['d:propstat']['d:prop']['d:getetag'] + res.contentType = obj['d:propstat']['d:prop']['d:getcontenttype'] + return res + } + } + + async propfind( + path: string, + params?: { + depth?: number | 'infinity' + properties?: string[] + }, + ): Promise { + const body = params?.properties + ? [ + XML_HEADER, + '', + '', + ...params.properties.map(prop => `<${prop}/>`), + '', + '', + ].join('\n') + : undefined + const res = await this.ffetch(path, { + method: 'PROPFIND', + headers: { + Depth: params?.depth ? String(params.depth) : '1', + }, + body, + }).text() + + const json = DMultistatusSchema.parse(xmlToJson(res)) + return json['d:multistatus'][0]['d:response'].map(this.mapPropfindResponse) + } + + async proppatch(path: string, params: { + set?: Record + remove?: string[] + }): Promise { + if (!params.set && !params.remove) return + + const lines: string[] = [ + XML_HEADER, + '', + ] + if (params.set) { + lines.push('') + for (const [key, value] of Object.entries(params.set ?? {})) { + lines.push(`<${key}>${ + typeof value === 'object' ? value._xml : escapeXml(value) + }`) + } + lines.push('') + } + if (params.remove) { + lines.push('') + for (const key of params.remove) { + lines.push(`<${key}/>`) + } + lines.push('') + } + lines.push('') + + const body = lines.join('\n') + await this.ffetch(path, { + method: 'PROPPATCH', + body, + }) + } + + async mkcol(path: string): Promise { + const res = await this.ffetch(path, { + method: 'MKCOL', + }) + if (res.status !== 201) throw new Error(`mkcol failed: ${res.status}`) + } + + async delete(path: string): Promise { + const res = await this.ffetch(path, { + method: 'DELETE', + }) + if (res.status !== 204) throw new Error(`delete failed: ${res.status}`) + } + + get(path: string): FfetchResult { + return this.ffetch(path, { + method: 'GET', + }) + } + + async put(path: string, body: BodyInit): Promise { + await this.ffetch(path, { + method: 'PUT', + body, + }) + } + + async copy( + source: string, + destination: string, + params?: { + /** whether to overwrite the destination if it exists */ + overwrite?: boolean + depth?: number | 'infinity' + }, + ): Promise { + if (destination[0] === '/') destination = destination.slice(1) + if (this.basePath) destination = this.basePath + destination + const headers: Record = { + Destination: destination, + } + if (params?.overwrite !== true) { + headers.Overwrite = 'F' + } + if (params?.depth) { + headers.Depth = String(params.depth) + } + + const res = await this.ffetch(source, { + method: 'COPY', + headers, + }) + if (res.status !== 201) throw new Error(`copy failed: ${res.status}`) + } + + async move( + source: string, + destination: string, + params?: { + /** whether to overwrite the destination if it exists */ + overwrite?: boolean + depth?: number | 'infinity' + }, + ): Promise { + if (destination[0] === '/') destination = destination.slice(1) + if (this.basePath) destination = this.basePath + destination + const headers: Record = { + Destination: destination, + } + if (params?.overwrite !== true) { + headers.Overwrite = 'F' + } + if (params?.depth) { + headers.Depth = String(params.depth) + } + + const res = await this.ffetch(source, { + method: 'MOVE', + headers, + }) + if (res.status !== 201) throw new Error(`move failed: ${res.status}`) + } +} diff --git a/utils/xml.ts b/utils/xml.ts new file mode 100644 index 0000000..b9c928d --- /dev/null +++ b/utils/xml.ts @@ -0,0 +1,20 @@ +import type { ChildNode } from 'domhandler' +import { DomHandler } from 'domhandler' +import { Parser } from 'htmlparser2' + +export function xmlToDom(xml: string) { + let _error: Error | null = null + let _dom: ChildNode[] | null = null + + const handler = new DomHandler((error, dom) => { + _error = error + _dom = dom + }) + const parser = new Parser(handler) + parser.write(xml) + parser.end() + + if (_error) throw _error + + return _dom! +} From e7c950724744149e3baf07ecb275c6e07622386f Mon Sep 17 00:00:00 2001 From: desu-bot Date: Tue, 14 Jan 2025 02:38:00 +0000 Subject: [PATCH 04/61] chore: update public repo --- .gitignore | 13 + LICENSE | 26 + eslint.config.js | 22 + package.json | 38 + pnpm-lock.yaml | 3931 ++++++++++++++++++++ scripts/auth/mtcute-login.ts | 20 + scripts/infra/navidrome-find-duplicates.ts | 105 + scripts/infra/navidrome-remux-m4a.ts | 66 + scripts/infra/slskd-total-upload.ts | 39 + scripts/media/deezer-art-fetcher.ts | 58 + scripts/media/ffmpeg-clipper.ts | 129 + scripts/media/itunes-art-fetcher.ts | 46 + scripts/media/itunes-artist-art-fetcher.ts | 63 + scripts/misc/update-forkgram.ts | 51 + tsconfig.json | 26 + utils/captcha.ts | 87 + utils/currency.ts | 113 + utils/fetch.ts | 37 + utils/fs.ts | 19 + utils/misc.ts | 10 + utils/navidrome.ts | 32 + utils/oauth.ts | 78 + utils/telegram.ts | 11 + utils/webdav.ts | 324 ++ utils/xml.ts | 20 + 25 files changed, 5364 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 scripts/auth/mtcute-login.ts create mode 100644 scripts/infra/navidrome-find-duplicates.ts create mode 100644 scripts/infra/navidrome-remux-m4a.ts create mode 100644 scripts/infra/slskd-total-upload.ts create mode 100644 scripts/media/deezer-art-fetcher.ts create mode 100644 scripts/media/ffmpeg-clipper.ts create mode 100644 scripts/media/itunes-art-fetcher.ts create mode 100644 scripts/media/itunes-artist-art-fetcher.ts create mode 100644 scripts/misc/update-forkgram.ts create mode 100644 tsconfig.json create mode 100644 utils/captcha.ts create mode 100644 utils/currency.ts create mode 100644 utils/fetch.ts create mode 100644 utils/fs.ts create mode 100644 utils/misc.ts create mode 100644 utils/navidrome.ts create mode 100644 utils/oauth.ts create mode 100644 utils/telegram.ts create mode 100644 utils/webdav.ts create mode 100644 utils/xml.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3b4c83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +private/ +.nyc_output/ +**/.DS_Store +.idea +.vscode +*.log +/assets + +coverage +.rollup.cache +*.tsbuildinfo +.env \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a38f8d --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +# DON'T BE A DICK PUBLIC LICENSE + +> Version 1.1, December 2016 + +> Copyright (C) 2024 alina sireneva + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document. + +> DON'T BE A DICK PUBLIC LICENSE +> TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +1. Do whatever you like with the original work, just don't be a dick. + + Being a dick includes - but is not limited to - the following instances: + + 1a. Outright copyright infringement - Don't just copy this and change the name. + 1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick. + 1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick. + +2. If you become rich through modifications, related works/services, or supporting the original work, +share the love. Only a dick would make loads off this work and not buy the original work's +creator(s) a pint. + +3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes +you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back. \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a159663 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import antfu from '@antfu/eslint-config' + +export default antfu({ + ignores: ['assets/'], + typescript: true, + rules: { + 'curly': ['error', 'multi-line'], + 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'n/prefer-global/buffer': 'off', + 'no-restricted-globals': ['error', 'Buffer', '__dirname', 'require'], + 'style/quotes': ['error', 'single', { avoidEscape: true }], + 'test/consistent-test-it': 'off', + 'test/prefer-lowercase-title': 'off', + 'antfu/if-newline': 'off', + 'style/max-statements-per-line': ['error', { max: 2 }], + 'ts/no-redeclare': 'off', + 'no-alert': 'off', + 'no-console': 'off', + 'node/prefer-global/process': 'off', + 'unused-imports/no-unused-vars': 'off', + }, +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..4336100 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "teidesu-scripts", + "type": "module", + "packageManager": "pnpm@9.5.0", + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@faker-js/faker": "^9.3.0", + "@fuman/io": "^0.0.4", + "@fuman/node": "^0.0.4", + "@mtcute/node": "^0.19.1", + "@types/plist": "^3.0.5", + "cheerio": "^1.0.0", + "es-main": "^1.3.0", + "filesize": "^10.1.6", + "json5": "^2.2.3", + "kuromoji": "^0.1.2", + "nanoid": "^5.0.9", + "plist": "^3.1.0", + "qrcode-terminal": "^0.12.0", + "tough-cookie": "^5.0.0", + "tough-cookie-file-store": "^2.0.3", + "undici": "^7.2.0", + "wanakana": "^5.3.1" + }, + "devDependencies": { + "@antfu/eslint-config": "3.10.0", + "@fuman/fetch": "0.0.7", + "@fuman/utils": "0.0.4", + "@types/node": "22.10.0", + "domhandler": "^5.0.3", + "dotenv": "16.4.5", + "htmlparser2": "^10.0.0", + "zod": "3.23.8", + "zx": "8.2.2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7616233 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3931 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@faker-js/faker': + specifier: ^9.3.0 + version: 9.3.0 + '@fuman/io': + specifier: ^0.0.4 + version: 0.0.4 + '@fuman/node': + specifier: ^0.0.4 + version: 0.0.4 + '@mtcute/node': + specifier: ^0.19.1 + version: 0.19.1 + '@types/plist': + specifier: ^3.0.5 + version: 3.0.5 + cheerio: + specifier: ^1.0.0 + version: 1.0.0 + es-main: + specifier: ^1.3.0 + version: 1.3.0 + filesize: + specifier: ^10.1.6 + version: 10.1.6 + json5: + specifier: ^2.2.3 + version: 2.2.3 + kuromoji: + specifier: ^0.1.2 + version: 0.1.2 + nanoid: + specifier: ^5.0.9 + version: 5.0.9 + plist: + specifier: ^3.1.0 + version: 3.1.0 + qrcode-terminal: + specifier: ^0.12.0 + version: 0.12.0 + tough-cookie: + specifier: ^5.0.0 + version: 5.0.0 + tough-cookie-file-store: + specifier: ^2.0.3 + version: 2.0.3 + typescript: + specifier: ^5.0.0 + version: 5.7.2 + undici: + specifier: ^7.2.0 + version: 7.2.0 + wanakana: + specifier: ^5.3.1 + version: 5.3.1 + devDependencies: + '@antfu/eslint-config': + specifier: 3.10.0 + version: 3.10.0(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0)(typescript@5.7.2) + '@fuman/fetch': + specifier: 0.0.7 + version: 0.0.7(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) + '@fuman/utils': + specifier: 0.0.4 + version: 0.0.4 + '@types/node': + specifier: 22.10.0 + version: 22.10.0 + domhandler: + specifier: ^5.0.3 + version: 5.0.3 + dotenv: + specifier: 16.4.5 + version: 16.4.5 + htmlparser2: + specifier: ^10.0.0 + version: 10.0.0 + zod: + specifier: 3.23.8 + version: 3.23.8 + zx: + specifier: 8.2.2 + version: 8.2.2 + +packages: + + '@antfu/eslint-config@3.10.0': + resolution: {integrity: sha512-rTl9BA42RIaC2l9iol1+uinO1alqVchAr8Vg2WDnXiAJPDaqiwRnbXIWM1fCZVNF4lwgqv71NIsusd67EpTPqA==} + hasBin: true + peerDependencies: + '@eslint-react/eslint-plugin': ^1.5.8 + '@prettier/plugin-xml': ^3.4.1 + '@unocss/eslint-plugin': '>=0.50.0' + astro-eslint-parser: ^1.0.2 + eslint: ^9.10.0 + eslint-plugin-astro: ^1.2.0 + eslint-plugin-format: '>=0.1.0' + eslint-plugin-react-hooks: ^5.0.0 + eslint-plugin-react-refresh: ^0.4.4 + eslint-plugin-solid: ^0.14.3 + eslint-plugin-svelte: '>=2.35.1' + prettier-plugin-astro: ^0.13.0 + prettier-plugin-slidev: ^1.0.5 + svelte-eslint-parser: '>=0.37.0' + peerDependenciesMeta: + '@eslint-react/eslint-plugin': + optional: true + '@prettier/plugin-xml': + optional: true + '@unocss/eslint-plugin': + optional: true + astro-eslint-parser: + optional: true + eslint-plugin-astro: + optional: true + eslint-plugin-format: + optional: true + eslint-plugin-react-hooks: + optional: true + eslint-plugin-react-refresh: + optional: true + eslint-plugin-solid: + optional: true + eslint-plugin-svelte: + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-slidev: + optional: true + svelte-eslint-parser: + optional: true + + '@antfu/install-pkg@0.4.1': + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@badrap/valita@0.4.2': + resolution: {integrity: sha512-Mwmr7k2iK0Yy0POLnAFUgab2mxKYeIsYXHY7sg3jo8XFsFHbG0SBmTcktXD0uW8N4WZePKf8s68QV7QDTGSdHA==} + engines: {node: '>= 18'} + + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + + '@clack/prompts@0.8.2': + resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + + '@es-joy/jsdoccomment@0.48.0': + resolution: {integrity: sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==} + engines: {node: '>=16'} + + '@es-joy/jsdoccomment@0.49.0': + resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==} + engines: {node: '>=16'} + + '@eslint-community/eslint-plugin-eslint-comments@4.4.1': + resolution: {integrity: sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@1.2.3': + resolution: {integrity: sha512-wlZhwlDFxkxIZ571aH0FoK4h4Vwx7P3HJx62Gp8hTc10bfpwT2x0nULuAHmQSJBOWPgPeVf+9YtnD4j50zVHmA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.10.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.19.0': + resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.0': + resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.15.0': + resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/markdown@6.2.1': + resolution: {integrity: sha512-cKVd110hG4ICHmWhIwZJfKmmJBvbiDWyrHODJknAtudKgZtlROGoLX9UEOA0o746zC0hCY4UV4vR+aOGW9S6JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.3': + resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@faker-js/faker@9.3.0': + resolution: {integrity: sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@fuman/fetch@0.0.7': + resolution: {integrity: sha512-yzyQI9ssqBrx04zKlfZwRHvJ9sw44RvfForjhaMhy7yA9jtghgVAjceeLTvF5xWEMgvIlIMfZRmSzEtCgGlfcg==} + peerDependencies: + '@badrap/valita': '>=0.4.0' + tough-cookie: ^5.0.0 || ^4.0.0 + valibot: ^0.42.0 + yup: ^1.0.0 + zod: ^3.0.0 + peerDependenciesMeta: + '@badrap/valita': + optional: true + tough-cookie: + optional: true + valibot: + optional: true + yup: + optional: true + zod: + optional: true + + '@fuman/io@0.0.4': + resolution: {integrity: sha512-IXzBJjHTVKyi04WaGtSXE0dhL3QK45ekrEZNfH/V59XQ1WupSqWevfSWd9T07rdagc2jtaeu8aJY6bwaiJpdYg==} + + '@fuman/net@0.0.4': + resolution: {integrity: sha512-a8Isnj+qgRNaqqmBCT6lZ9GZj5F3vQdygN5AzB6GGCbLKcOeH+1u5Twh5CUAW/dM7oogrTWOwCqgvS2XHbjzaQ==} + + '@fuman/node@0.0.4': + resolution: {integrity: sha512-tgwbIceUHWuwh4RTwJRQ1sLjzuIGrWx0SeCrqYhGF+IkI/B7DY0FP2SZykWImkVDtW8IzmdZskPZqiDINRGcNg==} + + '@fuman/utils@0.0.4': + resolution: {integrity: sha512-YBZIlGDbM8s9G85pWFZJ9wQrDY4511XLHZ06/uxZfXBY0eSStwje8JFNmRMNF0SjRk4D3iRmPl9wirHKTkg89w==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@mtcute/core@0.19.1': + resolution: {integrity: sha512-Xfq1T03kNRcyomkQl1d426piCnbgKZiYV2z/rM4BBrTIMDwGdxUpp66FPTj7N+MHm1/bVoJNFTUO67nBrLmqdA==} + + '@mtcute/file-id@0.19.0': + resolution: {integrity: sha512-r9r5JxchoVtYYMLPsf/wSnd5+KB4KmilWHywKQenf0DgKD+LCEN2FJpzY44RFE5dpy+eV5OHZ85zxA6EyYz/mA==} + + '@mtcute/html-parser@0.19.0': + resolution: {integrity: sha512-AVYD2/FT5g/a2m1o8+9Lg/0H/VIkxAwd9uqvXcR+aMpTQlQExWVwNi805yOtzs2sv8QK0yyVbN5AL7STBu+x+g==} + + '@mtcute/markdown-parser@0.19.0': + resolution: {integrity: sha512-sc8Yl5eDjEZ975/UMJNsHoiIjW6oKA2m1j4bi966ZxZ1L1SYjlsgnm1b/Vg+rsbeYoCjz1xU9eZMkPZh2jIVWA==} + + '@mtcute/node@0.19.1': + resolution: {integrity: sha512-YeG+HbYkp43K4VeB9wFHAEdj081sHwOJd3hWRctwoJUsv0IFMqQ6m6g2dXd6xyroJ91ewNjnfmaKbrM41Gdmuw==} + + '@mtcute/tl-runtime@0.19.0': + resolution: {integrity: sha512-5nqzQexx4/at+lWnd8G5zCsuI7k+vw/cr3IecRaZ+cOyNKLYQX9zqK2n6Lx+A9K1/M9L33YSAVPSPQnC+q0QjQ==} + + '@mtcute/tl@196.0.0': + resolution: {integrity: sha512-/FEoLfGk7VatEMkBWn4naRQnolodJX0CL0C2rPJr+gmhLSVMQs1a9HqLMIUFYRlEvZ4ayk4ujOWvF9G64xvW5g==} + + '@mtcute/wasm@0.19.0': + resolution: {integrity: sha512-QvSCjNR3/Y4knRMfccNXAdPUCgnQfjmOD/LGGHgQq/5/7SfA7f4/wk7G/TdlnBPArqk58EvyrC5qeKEy81y9Xw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@stylistic/eslint-plugin@2.11.0': + resolution: {integrity: sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.40.0' + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/events@3.0.0': + resolution: {integrity: sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/node@22.10.0': + resolution: {integrity: sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript-eslint/eslint-plugin@8.16.0': + resolution: {integrity: sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.16.0': + resolution: {integrity: sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.16.0': + resolution: {integrity: sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.16.0': + resolution: {integrity: sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.16.0': + resolution: {integrity: sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.16.0': + resolution: {integrity: sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.16.0': + resolution: {integrity: sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/visitor-keys@8.16.0': + resolution: {integrity: sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/eslint-plugin@1.1.10': + resolution: {integrity: sha512-uScH5Kz5v32vvtQYB2iodpoPg2mGASK+VKpjlc2IUgE0+16uZKqVKi2vQxjxJ6sMCQLBs4xhBFZlmZBszsmfKQ==} + peerDependencies: + '@typescript-eslint/utils': '>= 8.0' + eslint: '>= 8.57.0' + typescript: '>= 5.0.0' + vitest: '*' + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + + '@vue/compiler-sfc@3.5.13': + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + + '@vue/compiler-ssr@3.5.13': + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@11.3.0: + resolution: {integrity: sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001684: + resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0: + resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + engines: {node: '>=18.17'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + ci-info@4.1.0: + resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + engines: {node: '>=8'} + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + doublearray@0.0.2: + resolution: {integrity: sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==} + + electron-to-chromium@1.5.65: + resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encoding-sniffer@0.2.0: + resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-main@1.3.0: + resolution: {integrity: sha512-AzORKdz1Zt97TzbYQnIrI3ZiibWpRXUfpo/w0xOJ20GpNYd2bd3MU9m31zS/aJ1TJl6JfLTok83Y8HjNunYT0A==} + + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-compat-utils@0.6.3: + resolution: {integrity: sha512-9IDdksh5pUYP2ZLi7mOdROxVjLY8gY2qKxprmrJ/5Dyqud7M/IFKxF3o0VLlRhITm1pK6Fk7NiBxE39M/VlUcw==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-flat-gitignore@0.3.0: + resolution: {integrity: sha512-0Ndxo4qGhcewjTzw52TK06Mc00aDtHNTdeeW2JfONgDcLkRO/n/BteMRzNVpLQYxdCC/dFEilfM9fjjpGIJ9Og==} + peerDependencies: + eslint: ^9.5.0 + + eslint-flat-config-utils@0.4.0: + resolution: {integrity: sha512-kfd5kQZC+BMO0YwTol6zxjKX1zAsk8JfSAopbKjKqmENTJcew+yBejuvccAg37cvOrN0Mh+DVbeyznuNWEjt4A==} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-json-compat-utils@0.2.1: + resolution: {integrity: sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==} + engines: {node: '>=12'} + peerDependencies: + '@eslint/json': '*' + eslint: '*' + jsonc-eslint-parser: ^2.4.0 + peerDependenciesMeta: + '@eslint/json': + optional: true + + eslint-merge-processors@0.1.0: + resolution: {integrity: sha512-IvRXXtEajLeyssvW4wJcZ2etxkR9mUf4zpNwgI+m/Uac9RfXHskuJefkHUcawVzePnd6xp24enp5jfgdHzjRdQ==} + peerDependencies: + eslint: '*' + + eslint-plugin-antfu@2.7.0: + resolution: {integrity: sha512-gZM3jq3ouqaoHmUNszb1Zo2Ux7RckSvkGksjLWz9ipBYGSv1EwwBETN6AdiUXn+RpVHXTbEMPAPlXJazcA6+iA==} + peerDependencies: + eslint: '*' + + eslint-plugin-command@0.2.6: + resolution: {integrity: sha512-T0bHZ1oblW1xUHUVoBKZJR2osSNNGkfZuK4iqboNwuNS/M7tdp3pmURaJtTi/XDzitxaQ02lvOdFH0mUd5QLvQ==} + peerDependencies: + eslint: '*' + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-import-x@4.4.3: + resolution: {integrity: sha512-QBprHvhLsfDhP++2T1NnjsOUt6bLDX3NMHaYwAB1FD3xmYTkdFH+HS1OamGhz28jLkRyIZa6UNAzTxbHnJwz5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + eslint-plugin-jsdoc@50.6.0: + resolution: {integrity: sha512-tCNp4fR79Le3dYTPB0dKEv7yFyvGkUCa+Z3yuTrrNGGOxBlXo9Pn0PEgroOZikUQOGjxoGMVKNjrOHcYEdfszg==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-jsonc@2.18.2: + resolution: {integrity: sha512-SDhJiSsWt3nItl/UuIv+ti4g3m4gpGkmnUJS9UWR3TrpyNsIcnJoBRD7Kof6cM4Rk3L0wrmY5Tm3z7ZPjR2uGg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-plugin-n@17.14.0: + resolution: {integrity: sha512-maxPLMEA0rPmRpoOlxEclKng4UpDe+N5BJS4t24I3UKnN109Qcivnfs37KMy84G0af3bxjog5lKctP5ObsvcTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-plugin-no-only-tests@3.3.0: + resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} + engines: {node: '>=5.0.0'} + + eslint-plugin-perfectionist@4.1.2: + resolution: {integrity: sha512-YjXPWB/rKe/gPUsyuxw75wTUrzN5MuJnRV0PH9NoonFvgcdVIXk551mkBKPr59nRZCbu7S3dFHwfo4gA42DB2w==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + eslint: '>=8.0.0' + + eslint-plugin-regexp@2.7.0: + resolution: {integrity: sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==} + engines: {node: ^18 || >=20} + peerDependencies: + eslint: '>=8.44.0' + + eslint-plugin-toml@0.11.1: + resolution: {integrity: sha512-Y1WuMSzfZpeMIrmlP1nUh3kT8p96mThIq4NnHrYUhg10IKQgGfBZjAWnrg9fBqguiX4iFps/x/3Hb5TxBisfdw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-plugin-unicorn@56.0.1: + resolution: {integrity: sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==} + engines: {node: '>=18.18'} + peerDependencies: + eslint: '>=8.56.0' + + eslint-plugin-unused-imports@4.1.4: + resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-plugin-vue@9.31.0: + resolution: {integrity: sha512-aYMUCgivhz1o4tLkRHj5oq9YgYPM4/EJc0M7TAKRLCUA5OYxRLAhYEVD2nLtTwLyixEFI+/QXSvKU9ESZFgqjQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-yml@1.15.0: + resolution: {integrity: sha512-leC8APYVOsKyWUlvRwVhewytK5wS70BfMqIaUplFstRfzCoVp0YoEroV4cUEvQrBj93tQ3M9LcjO/ewr6D4kjA==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + eslint-processor-vue-blocks@0.1.2: + resolution: {integrity: sha512-PfpJ4uKHnqeL/fXUnzYkOax3aIenlwewXRX8jFinA1a2yCFnLgMuiH3xvCgvHHUlV2xJWQHbCTdiJWGwb3NqpQ==} + peerDependencies: + '@vue/compiler-sfc': ^3.3.0 + eslint: ^8.50.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.15.0: + resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filesize@10.1.6: + resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} + engines: {node: '>= 10.4.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.12.0: + resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + htmlparser2@10.0.0: + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-eslint-parser@2.4.0: + resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kuromoji@0.1.2: + resolution: {integrity: sha512-V0dUf+C2LpcPEXhoHLMAop/bOht16Dyr+mDiIE39yX3vqau7p80De/koFqpiTcL1zzdZlc3xuHZ8u5gjYRfFaQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + magic-string@0.30.14: + resolution: {integrity: sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.0: + resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.0.3: + resolution: {integrity: sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.0.9: + resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} + engines: {node: ^18 || >=20} + hasBin: true + + napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + + node-abi@3.71.0: + resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} + engines: {node: '>=10'} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.5: + resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-gitignore@2.0.0: + resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} + engines: {node: '>=14'} + + parse-imports@2.2.1: + resolution: {integrity: sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==} + engines: {node: '>= 18'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pkg-types@1.2.1: + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode-terminal@0.12.0: + resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==} + hasBin: true + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regjsparser@0.10.0: + resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slashes@3.0.12: + resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.20: + resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} + + stable-hash@0.0.4: + resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.6.2: + resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==} + engines: {node: '>=12.20'} + + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + tldts-core@6.1.66: + resolution: {integrity: sha512-s07jJruSwndD2X8bVjwioPfqpIc1pDTzszPe9pL1Skbh4bjytL85KNQ3tolqLbCvpQHawIsGfFi9dgerWjqW4g==} + + tldts@6.1.66: + resolution: {integrity: sha512-l3ciXsYFel/jSRfESbyKYud1nOw7WfhrBEF9I3UiarYk/qEaOOwu3qXNECHw4fHGHGTEOuhf/VdKgoDX5M/dhQ==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toml-eslint-parser@0.10.0: + resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + tough-cookie-file-store@2.0.3: + resolution: {integrity: sha512-sMpZVcmFf6EYFHFFl+SYH4W1/OnXBYMGDsv2IlbQ2caHyFElW/UR/gpj/KYU1JwmP4dE9xqwv2+vWcmlXHojSw==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} + + ts-api-utils@1.4.2: + resolution: {integrity: sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + undici@6.21.0: + resolution: {integrity: sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==} + engines: {node: '>=18.17'} + + undici@7.2.0: + resolution: {integrity: sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==} + engines: {node: '>=20.18.1'} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + wanakana@5.3.1: + resolution: {integrity: sha512-OSDqupzTlzl2LGyqTdhcXcl6ezMiFhcUwLBP8YKaBIbMYW1wAwDvupw2T9G9oVaKT9RmaSpyTXjxddFPUcFFIw==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml-eslint-parser@1.2.3: + resolution: {integrity: sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==} + engines: {node: ^14.17.0 || >=16.0.0} + + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zlibjs@0.3.1: + resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==} + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + + zx@8.2.2: + resolution: {integrity: sha512-HSIdpU5P2ONI0nssnhsUZNCH9Sd/Z8LIFk9n8QTbu6JufzJx7qR7ajrMN21s06JqWSApcN012377iWsv8Vs5bg==} + engines: {node: '>= 12.17.0'} + hasBin: true + +snapshots: + + '@antfu/eslint-config@3.10.0(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@antfu/install-pkg': 0.4.1 + '@clack/prompts': 0.8.2 + '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.15.0) + '@eslint/markdown': 6.2.1 + '@stylistic/eslint-plugin': 2.11.0(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/parser': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + '@vitest/eslint-plugin': 1.1.10(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2) + eslint: 9.15.0 + eslint-config-flat-gitignore: 0.3.0(eslint@9.15.0) + eslint-flat-config-utils: 0.4.0 + eslint-merge-processors: 0.1.0(eslint@9.15.0) + eslint-plugin-antfu: 2.7.0(eslint@9.15.0) + eslint-plugin-command: 0.2.6(eslint@9.15.0) + eslint-plugin-import-x: 4.4.3(eslint@9.15.0)(typescript@5.7.2) + eslint-plugin-jsdoc: 50.6.0(eslint@9.15.0) + eslint-plugin-jsonc: 2.18.2(eslint@9.15.0) + eslint-plugin-n: 17.14.0(eslint@9.15.0) + eslint-plugin-no-only-tests: 3.3.0 + eslint-plugin-perfectionist: 4.1.2(eslint@9.15.0)(typescript@5.7.2) + eslint-plugin-regexp: 2.7.0(eslint@9.15.0) + eslint-plugin-toml: 0.11.1(eslint@9.15.0) + eslint-plugin-unicorn: 56.0.1(eslint@9.15.0) + eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0) + eslint-plugin-vue: 9.31.0(eslint@9.15.0) + eslint-plugin-yml: 1.15.0(eslint@9.15.0) + eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.5.13)(eslint@9.15.0) + globals: 15.12.0 + jsonc-eslint-parser: 2.4.0 + local-pkg: 0.5.1 + parse-gitignore: 2.0.0 + picocolors: 1.1.1 + toml-eslint-parser: 0.10.0 + vue-eslint-parser: 9.4.3(eslint@9.15.0) + yaml-eslint-parser: 1.2.3 + yargs: 17.7.2 + transitivePeerDependencies: + - '@eslint/json' + - '@typescript-eslint/utils' + - '@vue/compiler-sfc' + - supports-color + - typescript + - vitest + + '@antfu/install-pkg@0.4.1': + dependencies: + package-manager-detector: 0.2.5 + tinyexec: 0.3.1 + + '@antfu/utils@0.7.10': {} + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@badrap/valita@0.4.2': + optional: true + + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.8.2': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@es-joy/jsdoccomment@0.48.0': + dependencies: + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 4.1.0 + + '@es-joy/jsdoccomment@0.49.0': + dependencies: + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 4.1.0 + + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.15.0)': + dependencies: + escape-string-regexp: 4.0.0 + eslint: 9.15.0 + ignore: 5.3.2 + + '@eslint-community/eslint-utils@4.4.1(eslint@9.15.0)': + dependencies: + eslint: 9.15.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/compat@1.2.3(eslint@9.15.0)': + optionalDependencies: + eslint: 9.15.0 + + '@eslint/config-array@0.19.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.0': {} + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.15.0': {} + + '@eslint/markdown@6.2.1': + dependencies: + '@eslint/plugin-kit': 0.2.3 + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.2.3': + dependencies: + levn: 0.4.1 + + '@faker-js/faker@9.3.0': {} + + '@fuman/fetch@0.0.7(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': + dependencies: + '@fuman/utils': 0.0.4 + optionalDependencies: + '@badrap/valita': 0.4.2 + tough-cookie: 5.0.0 + zod: 3.23.8 + + '@fuman/io@0.0.4': + dependencies: + '@fuman/utils': 0.0.4 + + '@fuman/net@0.0.4': + dependencies: + '@fuman/io': 0.0.4 + '@fuman/utils': 0.0.4 + + '@fuman/node@0.0.4': + dependencies: + '@fuman/io': 0.0.4 + '@fuman/net': 0.0.4 + '@fuman/utils': 0.0.4 + + '@fuman/utils@0.0.4': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@mtcute/core@0.19.1': + dependencies: + '@fuman/io': 0.0.4 + '@fuman/net': 0.0.4 + '@fuman/utils': 0.0.4 + '@mtcute/file-id': 0.19.0 + '@mtcute/tl': 196.0.0 + '@mtcute/tl-runtime': 0.19.0 + '@types/events': 3.0.0 + long: 5.2.3 + + '@mtcute/file-id@0.19.0': + dependencies: + '@fuman/utils': 0.0.4 + '@mtcute/tl-runtime': 0.19.0 + long: 5.2.3 + + '@mtcute/html-parser@0.19.0': + dependencies: + '@mtcute/core': 0.19.1 + htmlparser2: 6.1.0 + long: 5.2.3 + + '@mtcute/markdown-parser@0.19.0': + dependencies: + '@mtcute/core': 0.19.1 + long: 5.2.3 + + '@mtcute/node@0.19.1': + dependencies: + '@fuman/net': 0.0.4 + '@fuman/node': 0.0.4 + '@fuman/utils': 0.0.4 + '@mtcute/core': 0.19.1 + '@mtcute/html-parser': 0.19.0 + '@mtcute/markdown-parser': 0.19.0 + '@mtcute/wasm': 0.19.0 + better-sqlite3: 11.3.0 + + '@mtcute/tl-runtime@0.19.0': + dependencies: + '@fuman/utils': 0.0.4 + long: 5.2.3 + + '@mtcute/tl@196.0.0': + dependencies: + long: 5.2.3 + + '@mtcute/wasm@0.19.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@pkgr/core@0.1.1': {} + + '@stylistic/eslint-plugin@2.11.0(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + eslint: 9.15.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + estraverse: 5.3.0 + picomatch: 4.0.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/estree@1.0.6': {} + + '@types/events@3.0.0': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.10.0 + optional: true + + '@types/json-schema@7.0.15': {} + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.10.0 + optional: true + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@0.7.34': {} + + '@types/node@22.10.0': + dependencies: + undici-types: 6.20.0 + + '@types/normalize-package-data@2.4.4': {} + + '@types/plist@3.0.5': + dependencies: + '@types/node': 22.10.0 + xmlbuilder: 15.1.1 + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/type-utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.16.0 + eslint: 9.15.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.2(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.16.0 + debug: 4.3.7 + eslint: 9.15.0 + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.16.0': + dependencies: + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/visitor-keys': 8.16.0 + + '@typescript-eslint/type-utils@8.16.0(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + debug: 4.3.7 + eslint: 9.15.0 + ts-api-utils: 1.4.2(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.16.0': {} + + '@typescript-eslint/typescript-estree@8.16.0(typescript@5.7.2)': + dependencies: + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/visitor-keys': 8.16.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.2(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + eslint: 9.15.0 + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.16.0': + dependencies: + '@typescript-eslint/types': 8.16.0 + eslint-visitor-keys: 4.2.0 + + '@vitest/eslint-plugin@1.1.10(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + eslint: 9.15.0 + optionalDependencies: + typescript: 5.7.2 + + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.26.2 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/compiler-sfc@3.5.13': + dependencies: + '@babel/parser': 7.26.2 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.14 + postcss: 8.4.49 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.13': + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/shared@3.5.13': {} + + '@xmldom/xmldom@0.8.10': {} + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + are-docs-informative@0.0.2: {} + + argparse@2.0.1: {} + + async@2.6.4: + dependencies: + lodash: 4.17.21 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + better-sqlite3@11.3.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.2 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + boolbase@1.0.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001684 + electron-to-chromium: 1.5.65 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtin-modules@3.3.0: {} + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001684: {} + + ccount@2.0.1: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-entities@2.0.2: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + + cheerio@1.0.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + encoding-sniffer: 0.2.0 + htmlparser2: 9.1.0 + parse5: 7.2.1 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 6.21.0 + whatwg-mimetype: 4.0.0 + + chownr@1.1.4: {} + + ci-info@4.1.0: {} + + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + comment-parser@1.4.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + core-js-compat@3.39.0: + dependencies: + browserslist: 4.24.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + + css-what@6.1.0: {} + + cssesc@3.0.0: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + dequal@2.0.3: {} + + detect-libc@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@16.4.5: {} + + doublearray@0.0.2: {} + + electron-to-chromium@1.5.65: {} + + emoji-regex@8.0.0: {} + + encoding-sniffer@0.2.0: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.17.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-main@1.3.0: {} + + es-module-lexer@1.5.4: {} + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-compat-utils@0.5.1(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + semver: 7.6.3 + + eslint-compat-utils@0.6.3(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + semver: 7.6.3 + + eslint-config-flat-gitignore@0.3.0(eslint@9.15.0): + dependencies: + '@eslint/compat': 1.2.3(eslint@9.15.0) + eslint: 9.15.0 + find-up-simple: 1.0.0 + + eslint-flat-config-utils@0.4.0: + dependencies: + pathe: 1.1.2 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.15.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-json-compat-utils@0.2.1(eslint@9.15.0)(jsonc-eslint-parser@2.4.0): + dependencies: + eslint: 9.15.0 + esquery: 1.6.0 + jsonc-eslint-parser: 2.4.0 + + eslint-merge-processors@0.1.0(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + + eslint-plugin-antfu@2.7.0(eslint@9.15.0): + dependencies: + '@antfu/utils': 0.7.10 + eslint: 9.15.0 + + eslint-plugin-command@0.2.6(eslint@9.15.0): + dependencies: + '@es-joy/jsdoccomment': 0.48.0 + eslint: 9.15.0 + + eslint-plugin-es-x@7.8.0(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/regexpp': 4.12.1 + eslint: 9.15.0 + eslint-compat-utils: 0.5.1(eslint@9.15.0) + + eslint-plugin-import-x@4.4.3(eslint@9.15.0)(typescript@5.7.2): + dependencies: + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + debug: 4.3.7 + doctrine: 3.0.0 + eslint: 9.15.0 + eslint-import-resolver-node: 0.3.9 + get-tsconfig: 4.8.1 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + stable-hash: 0.0.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-jsdoc@50.6.0(eslint@9.15.0): + dependencies: + '@es-joy/jsdoccomment': 0.49.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint: 9.15.0 + espree: 10.3.0 + esquery: 1.6.0 + parse-imports: 2.2.1 + semver: 7.6.3 + spdx-expression-parse: 4.0.0 + synckit: 0.9.2 + transitivePeerDependencies: + - supports-color + + eslint-plugin-jsonc@2.18.2(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + eslint: 9.15.0 + eslint-compat-utils: 0.6.3(eslint@9.15.0) + eslint-json-compat-utils: 0.2.1(eslint@9.15.0)(jsonc-eslint-parser@2.4.0) + espree: 9.6.1 + graphemer: 1.4.0 + jsonc-eslint-parser: 2.4.0 + natural-compare: 1.4.0 + synckit: 0.6.2 + transitivePeerDependencies: + - '@eslint/json' + + eslint-plugin-n@17.14.0(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + enhanced-resolve: 5.17.1 + eslint: 9.15.0 + eslint-plugin-es-x: 7.8.0(eslint@9.15.0) + get-tsconfig: 4.8.1 + globals: 15.12.0 + ignore: 5.3.2 + minimatch: 9.0.5 + semver: 7.6.3 + + eslint-plugin-no-only-tests@3.3.0: {} + + eslint-plugin-perfectionist@4.1.2(eslint@9.15.0)(typescript@5.7.2): + dependencies: + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) + eslint: 9.15.0 + natural-orderby: 5.0.0 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-regexp@2.7.0(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/regexpp': 4.12.1 + comment-parser: 1.4.1 + eslint: 9.15.0 + jsdoc-type-pratt-parser: 4.1.0 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + + eslint-plugin-toml@0.11.1(eslint@9.15.0): + dependencies: + debug: 4.3.7 + eslint: 9.15.0 + eslint-compat-utils: 0.5.1(eslint@9.15.0) + lodash: 4.17.21 + toml-eslint-parser: 0.10.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-unicorn@56.0.1(eslint@9.15.0): + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + ci-info: 4.1.0 + clean-regexp: 1.0.0 + core-js-compat: 3.39.0 + eslint: 9.15.0 + esquery: 1.6.0 + globals: 15.12.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + jsesc: 3.0.2 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.27 + regjsparser: 0.10.0 + semver: 7.6.3 + strip-indent: 3.0.0 + + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2) + + eslint-plugin-vue@9.31.0(eslint@9.15.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + eslint: 9.15.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@9.15.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-yml@1.15.0(eslint@9.15.0): + dependencies: + debug: 4.3.7 + eslint: 9.15.0 + eslint-compat-utils: 0.5.1(eslint@9.15.0) + lodash: 4.17.21 + natural-compare: 1.4.0 + yaml-eslint-parser: 1.2.3 + transitivePeerDependencies: + - supports-color + + eslint-processor-vue-blocks@0.1.2(@vue/compiler-sfc@3.5.13)(eslint@9.15.0): + dependencies: + '@vue/compiler-sfc': 3.5.13 + eslint: 9.15.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.15.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.0 + '@eslint/core': 0.9.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.15.0 + '@eslint/plugin-kit': 0.2.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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.4 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + espree@9.6.1: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + expand-template@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-uri-to-path@1.0.0: {} + + filesize@10.1.6: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.2 + keyv: 4.5.4 + + flatted@3.3.2: {} + + fs-constants@1.0.0: {} + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.12.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@2.8.9: {} + + htmlparser2@10.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 6.0.0 + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdoc-type-pratt-parser@4.1.0: {} + + jsesc@0.5.0: {} + + jsesc@3.0.2: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-eslint-parser@2.4.0: + dependencies: + acorn: 8.14.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + semver: 7.6.3 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kuromoji@0.1.2: + dependencies: + async: 2.6.4 + doublearray: 0.0.2 + zlibjs: 0.3.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + local-pkg@0.5.1: + dependencies: + mlly: 1.7.3 + pkg-types: 1.2.1 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + long@5.2.3: {} + + longest-streak@3.1.0: {} + + magic-string@0.30.14: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.2: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-table@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.1 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.1 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.1 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.0.3: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + micromark@4.0.1: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-response@3.1.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + mlly@1.7.3: + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 + + ms@2.1.3: {} + + nanoid@3.3.8: {} + + nanoid@5.0.9: {} + + napi-build-utils@1.0.2: {} + + natural-compare@1.4.0: {} + + natural-orderby@5.0.0: {} + + node-abi@3.71.0: + dependencies: + semver: 7.6.3 + + node-releases@2.0.18: {} + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.8 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-manager-detector@0.2.5: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-gitignore@2.0.0: {} + + parse-imports@2.2.1: + dependencies: + es-module-lexer: 1.5.4 + slashes: 3.0.12 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.2.1 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.2.1 + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@1.1.2: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pkg-types@1.2.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.3 + pathe: 1.1.2 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pluralize@8.0.0: {} + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.4.49: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.2: + dependencies: + detect-libc: 2.0.3 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.71.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + + prelude-ls@1.2.1: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode@2.3.1: {} + + qrcode-terminal@0.12.0: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + refa@0.12.1: + dependencies: + '@eslint-community/regexpp': 4.12.1 + + regexp-ast-analysis@0.7.1: + dependencies: + '@eslint-community/regexpp': 4.12.1 + refa: 0.12.1 + + regexp-tree@0.1.27: {} + + regjsparser@0.10.0: + dependencies: + jsesc: 0.5.0 + + require-directory@2.1.1: {} + + requires-port@1.0.0: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.0.4: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.12.1 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + + semver@5.7.2: {} + + semver@7.6.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + sisteransi@1.0.5: {} + + slashes@3.0.12: {} + + source-map-js@1.2.1: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.20 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.20 + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.20 + + spdx-license-ids@3.0.20: {} + + stable-hash@0.0.4: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + synckit@0.6.2: + dependencies: + tslib: 2.8.1 + + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + + tapable@2.2.1: {} + + tar-fs@2.1.1: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tinyexec@0.3.1: {} + + tldts-core@6.1.66: {} + + tldts@6.1.66: + dependencies: + tldts-core: 6.1.66 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toml-eslint-parser@0.10.0: + dependencies: + eslint-visitor-keys: 3.4.3 + + tough-cookie-file-store@2.0.3: + dependencies: + tough-cookie: 4.1.4 + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tough-cookie@5.0.0: + dependencies: + tldts: 6.1.66 + + ts-api-utils@1.4.2(typescript@5.7.2): + dependencies: + typescript: 5.7.2 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.6.0: {} + + type-fest@0.8.1: {} + + typescript@5.7.2: {} + + ufo@1.5.4: {} + + undici-types@6.20.0: {} + + undici@6.21.0: {} + + undici@7.2.0: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@0.2.0: {} + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vue-eslint-parser@9.4.3(eslint@9.15.0): + dependencies: + debug: 4.3.7 + eslint: 9.15.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + wanakana@5.3.1: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xml-name-validator@4.0.0: {} + + xmlbuilder@15.1.1: {} + + y18n@5.0.8: {} + + yaml-eslint-parser@1.2.3: + dependencies: + eslint-visitor-keys: 3.4.3 + lodash: 4.17.21 + yaml: 2.6.1 + + yaml@2.6.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zlibjs@0.3.1: {} + + zod@3.23.8: {} + + zwitch@2.0.4: {} + + zx@8.2.2: + optionalDependencies: + '@types/fs-extra': 11.0.4 + '@types/node': 22.10.0 diff --git a/scripts/auth/mtcute-login.ts b/scripts/auth/mtcute-login.ts new file mode 100644 index 0000000..0bf4af9 --- /dev/null +++ b/scripts/auth/mtcute-login.ts @@ -0,0 +1,20 @@ +import qrTerminal from 'qrcode-terminal' +import { createTg } from '../../utils/telegram.ts' + +const sessionName = process.argv[2] +if (!sessionName) { + console.error('Usage: mtcute-login.ts ') + process.exit(1) +} + +const tg = createTg(sessionName) + +const self = await tg.start({ + qrCodeHandler(url, expires) { + console.log(qrTerminal.generate(url, { small: true })) + }, +}) + +console.log(`Logged in as ${self.displayName} (${self.id})`) + +await tg.close() diff --git a/scripts/infra/navidrome-find-duplicates.ts b/scripts/infra/navidrome-find-duplicates.ts new file mode 100644 index 0000000..3c89971 --- /dev/null +++ b/scripts/infra/navidrome-find-duplicates.ts @@ -0,0 +1,105 @@ +import type { NavidromeSong } from '../../utils/navidrome.ts' +import { createRequire } from 'node:module' + +import { join } from 'node:path' +import kuromoji from 'kuromoji' +import { isKana, toRomaji } from 'wanakana' + +import { fetchSongs, navidromeFfetch as ffetch } from '../../utils/navidrome.ts' + +const WHITELIST_KEYS = new Set([ + // actual different tracks with the same title + '["sorry about my face","untitled track"]', + '["kooeetekumogeemusu","neko bushou sengoku emaki"]', + '["eve","merufuakutorii"]', + // todo + '["arm","legend of zelda"]', + '["arm","tomorrow heart beat ~ ashita anata ni dokkidokiā˜† ~"]', + '["dwat","rotladatormarf"]', + '["fujiwara mari sai","zenbuatashinokawaiino"]', +]) + +const moji = await new Promise((resolve, reject) => { + kuromoji.builder({ + dicPath: join(createRequire(import.meta.url).resolve('kuromoji/'), '../../dict'), + }).build((err, tokenizer) => { + if (err) return reject(err) + resolve(tokenizer) + }) +}) + +function clean(s: string) { + const str = s.toLowerCase() + .replace(/\(Explicit\)/i, '') + .replace(/[!@#$%^&*()_+=[\]{}\\|/,.;':"<>`~-]/g, '') + + if (str.match(/[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFF9F\u4E00-\u9FAF\u3400-\u4DBF]/)) { + // has japanese + const tokens = moji.tokenize(str) + + let res = '' + + for (const token of tokens) { + if (token.word_type === 'UNKNOWN') { + res += isKana(token.surface_form) ? toRomaji(token.surface_form) : token.surface_form + } else if (token.word_type === 'KNOWN') { + res += `${toRomaji(token.reading)} ` + } + } + + return res.trimEnd() + } + + return str +} + +const CHUNK_SIZE = 1000 + +function getSongKey(song: NavidromeSong) { + return JSON.stringify([ + clean(song.artist), + clean(song.title), + ]) +} + +const seen = new Map() + +for (let offset = 0; ; offset += CHUNK_SIZE) { + const songs = await fetchSongs(offset, CHUNK_SIZE) + if (songs.length === 0) break + + for (const song of songs) { + const key = getSongKey(song) + if (WHITELIST_KEYS.has(key)) continue + let arr = seen.get(key) + if (!arr) { + arr = [] + seen.set(key, arr) + } + + arr.push(song) + } + + console.log('āŒ› fetched chunk %d (%d items)', Math.floor(offset / CHUNK_SIZE), songs.length) +} + +const keysSorted = Array.from(seen.keys()).sort() + +let duplicates = 0 +for (const key of keysSorted) { + const arr = seen.get(key)! + if (arr.length === 1) continue + + duplicates += 1 + console.log() + console.log('found duplicates for %s:', key) + for (const song of arr) { + console.log(' %s - %s (from %s - %s) (at %s)', song.artist, song.title, song.albumArtist, song.album, song.path) + } +} + +if (duplicates === 0) { + console.log('āœ… no duplicates found') +} else { + console.log('🚨 %d duplicates found', duplicates) +} diff --git a/scripts/infra/navidrome-remux-m4a.ts b/scripts/infra/navidrome-remux-m4a.ts new file mode 100644 index 0000000..92b1e30 --- /dev/null +++ b/scripts/infra/navidrome-remux-m4a.ts @@ -0,0 +1,66 @@ +import { readFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { $ } from 'zx' +import { downloadStream } from '../../utils/fetch.ts' +import { getEnv } from '../../utils/misc.ts' +import { fetchSongs } from '../../utils/navidrome.ts' +import { WebdavClient } from '../../utils/webdav.ts' + +const webdav = new WebdavClient({ + baseUrl: getEnv('NAVIDROME_WEBDAV_ENDPOINT'), + username: getEnv('NAVIDROME_WEBDAV_USERNAME'), + password: getEnv('NAVIDROME_WEBDAV_PASSWORD'), +}) + +const CHUNK_SIZE = 1000 +for (let offset = 0; ; offset += CHUNK_SIZE) { + const songs = await fetchSongs(offset, CHUNK_SIZE) + if (songs.length === 0) break + + for (const song of songs) { + const ext = song.path.split('.').pop()! + if (ext !== 'm4a') continue + + console.log('āŒ song %s is m4a, remuxing...', song.path) + const webdavPath = song.path.replace('/music/s3/', '/') + const res = await webdav.get(webdavPath).catch(() => null) + + if (!res) { + console.log(' āŒ failed to get %s', webdavPath) + continue + } + + const tmpfile = join('assets', `${song.id}.m4a`) + await downloadStream(res.body!, tmpfile) + console.log(' - downloaded to %s', tmpfile) + + const probe = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${tmpfile}`.json() + const audioStream = probe.streams.find(stream => stream.codec_type === 'audio') + if (!audioStream) { + console.log(' āŒ no audio stream found') + await rm(tmpfile) + continue + } + const codec = audioStream.codec_name + + if (codec !== 'flac') { + console.log(` āŒ audio stream is ${codec}, not flac, skipping`) + await rm(tmpfile) + continue + } + + console.log(' - audio stream is flac, remuxing') + + // remux + const remuxed = join('assets', `${song.id}.flac`) + await rm(remuxed, { force: true }) + await $`ffmpeg -i ${tmpfile} -c:a copy ${remuxed}`.quiet(true) + console.log(' - remuxed to %s', remuxed) + await rm(tmpfile) + + await webdav.put(webdavPath.replace('.m4a', '.flac'), await readFile(remuxed)) + await webdav.delete(webdavPath) + console.log(' - uploaded to %s', webdavPath.replace('.m4a', '.flac')) + await rm(remuxed) + } +} diff --git a/scripts/infra/slskd-total-upload.ts b/scripts/infra/slskd-total-upload.ts new file mode 100644 index 0000000..8ed9680 --- /dev/null +++ b/scripts/infra/slskd-total-upload.ts @@ -0,0 +1,39 @@ +import { filesize } from 'filesize' +import { z } from 'zod' + +import { ffetch } from '../../utils/fetch.ts' +import { getEnv } from '../../utils/misc.ts' + +const res = await ffetch('/api/v0/transfers/uploads', { + baseUrl: getEnv('SLSKD_ENDPOINT'), + headers: { + cookie: getEnv('SLSKD_COOKIE'), + }, +}).parsedJson(z.array( + z.object({ + username: z.string(), + directories: z.array(z.object({ + directory: z.string(), + fileCount: z.number(), + files: z.array(z.object({ + id: z.string(), + filename: z.string(), + state: z.string(), + bytesTransferred: z.number(), + })), + })), + }), +)) + +let total = 0 + +for (const user of res) { + for (const dir of user.directories) { + for (const file of dir.files) { + if (file.state !== 'Completed, Succeeded') continue + total += file.bytesTransferred + } + } +} + +console.log(filesize(total)) diff --git a/scripts/media/deezer-art-fetcher.ts b/scripts/media/deezer-art-fetcher.ts new file mode 100644 index 0000000..fc7e4ef --- /dev/null +++ b/scripts/media/deezer-art-fetcher.ts @@ -0,0 +1,58 @@ +import { iter } from '@fuman/utils' +import { z } from 'zod' +import { minimist, question } from 'zx' + +import { downloadFile, ffetch } from '../../utils/fetch.ts' + +const args = minimist(process.argv.slice(2), { + string: ['filename'], +}) + +const query = args._[0] ?? await question('Search query (Artist - Album): ') + +const data = await ffetch('https://api.deezer.com/search', { + query: { + q: query, + limit: 15, + }, +}).parsedJson(z.object({ + data: z.array(z.object({ + type: z.literal('track'), + title: z.string(), + artist: z.object({ + name: z.string(), + }), + album: z.object({ + id: z.number(), + title: z.string(), + cover_xl: z.string(), + }), + })), +})) + +const groupedByAlbum = new Map() +for (const result of data.data) { + const albumId = result.album.id + if (!groupedByAlbum.has(albumId)) { + groupedByAlbum.set(albumId, []) + } + + groupedByAlbum.get(albumId)!.push(result) +} + +const idxToAlbum = new Map() +for (const [idx, [id, tracks]] of iter.enumerate(groupedByAlbum.entries())) { + idxToAlbum.set(idx, id) + console.log(`${idx + 1}. ${tracks[0].artist.name} - ${tracks[0].album.title}`) + for (const track of tracks) { + console.log(` ${track.title}`) + } +} + +console.log('Enter number to download album art:') + +const number = Number.parseInt(await question('[1] > ') || '1') + +const artworkUrl = groupedByAlbum.get(idxToAlbum.get(number - 1)!)![0].album.cover_xl + +await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`) diff --git a/scripts/media/ffmpeg-clipper.ts b/scripts/media/ffmpeg-clipper.ts new file mode 100644 index 0000000..bfa3b4e --- /dev/null +++ b/scripts/media/ffmpeg-clipper.ts @@ -0,0 +1,129 @@ +import { rm } from 'node:fs/promises' + +import { $, question } from 'zx' + +import { fileExists } from '../../utils/fs.ts' + +let filename = await question('filename >')! +const startTs = await question('start timestamp >') +const endTs = await question('end timestamp >') +const outputFilename = await question('output filename [output.mp4] >') || 'assets/output.mp4' + +if (filename[0] === '\'' && filename[filename.length - 1] === '\'') { + filename = filename.slice(1, -1) +} + +const ffprobe = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${filename}`.json() + +async function chooseStream(type: string, options: any[], allowNone = false) { + console.log(`Found ${type} streams:`) + for (let i = 0; i < options.length; i++) { + const stream = options[i] + console.log(`[${i + 1}] (${stream.codec_name}, ${stream.tags.language}) ${stream.tags.title}`) + } + + if (allowNone) { + console.log(`[0] No ${type}`) + } + + const res = await question(`select ${type} >`) || '0' + if (res === '0' && allowNone) { + return null + } + + const streamIndex = Number.parseInt(res) + if (Number.isNaN(streamIndex) || streamIndex < 1 || streamIndex > options.length) { + console.error('Invalid input') + process.exit(1) + } + + return streamIndex - 1 +} + +const allVideos = ffprobe.streams.filter(stream => stream.codec_type === 'video') +const allAudios = ffprobe.streams.filter(stream => stream.codec_type === 'audio') +const allSubtitles = ffprobe.streams.filter(stream => stream.codec_type === 'subtitle') + +let videoStream: number | null = null +let audioStream: number | null = null +let subtitleStream: number | null = null + +if (allVideos.length > 1) { + videoStream = await chooseStream('video', allVideos) +} else if (allVideos.length > 0) { + videoStream = 0 +} else { + console.error('No video streams found') + process.exit(1) +} + +if (allAudios.length > 1) { + audioStream = await chooseStream('audio', allAudios) +} else if (allAudios.length > 0) { + audioStream = 0 +} else { + console.warn('No audio streams found, proceeding without audio') +} + +if (allSubtitles.length > 0) { + subtitleStream = await chooseStream('subtitle', allSubtitles, true) +} + +const args: string[] = [ + '-i', + filename, + '-c:v', + 'libx264', + '-map', + `0:v:${videoStream}`, + '-c:v', + 'libx264', +] + +if (audioStream !== null) { + args.push('-map', `0:a:${audioStream}`) +} + +if (subtitleStream !== null) { + const filenameEscaped = filename.replace(/'/g, "'\\\\\\''") + args.push('-vf', `format=yuv420p,subtitles='${filenameEscaped}':si=${subtitleStream}`) +} else { + args.push('-vf', 'format=yuv420p') +} + +if (audioStream !== null) { + args.push('-c:a', 'libopus') + + if (allAudios[audioStream].codec_name === 'flac') { + args.push('-b:a', '320k') + } +} + +args.push( + '-ss', + startTs!, + '-to', + endTs!, + outputFilename, +) + +if (await fileExists(outputFilename)) { + const overwrite = await question('Output file already exists, overwrite? [y/N] >') + if (overwrite?.toLowerCase() !== 'y') { + process.exit(0) + } + + await rm(outputFilename) +} + +try { + $.env.AV_LOG_FORCE_COLOR = 'true' + await $`ffmpeg ${args}` +} catch (e) { + process.exit(1) +} + +const openDir = await question('open output directory? [Y/n] >') +if (!openDir || openDir?.toLowerCase() === 'y') { + await $`open -R ${outputFilename}` +} diff --git a/scripts/media/itunes-art-fetcher.ts b/scripts/media/itunes-art-fetcher.ts new file mode 100644 index 0000000..eef531e --- /dev/null +++ b/scripts/media/itunes-art-fetcher.ts @@ -0,0 +1,46 @@ +import { iter } from '@fuman/utils' +import { z } from 'zod' +import { minimist, question } from 'zx' + +import { downloadFile, ffetch } from '../../utils/fetch.ts' + +const args = minimist(process.argv.slice(2), { + string: ['entity', 'filename'], +}) + +const entity = args.entity ?? 'album' +const query = args._[0] ?? await question('Search query (Artist - Album): ') + +const data = await ffetch('https://itunes.apple.com/search', { + query: { + term: query, + entity, + limit: 15, + }, +}).parsedJson(z.object({ + results: z.array(z.object({ + kind: z.literal('song').optional(), + artistName: z.string(), + collectionName: z.string(), + artworkUrl100: z.string(), + releaseDate: z.string(), + trackName: z.string().optional(), + }).passthrough()), +})) + +for (const [i, result] of iter.enumerate(data.results)) { + if (result.kind === 'song') { + console.log(`${i + 1}. ${result.artistName} - ${result.trackName} (${result.collectionName}, ${new Date(result.releaseDate).toLocaleDateString('ru-RU')})`) + continue + } + + console.log(`${i + 1}. ${result.artistName} - ${result.collectionName} (${new Date(result.releaseDate).toLocaleDateString('ru-RU')})`) +} + +console.log('Enter number to download album art:') + +const number = Number.parseInt(await question('[1] > ') || '1') + +const artworkUrl = data.results[number - 1].artworkUrl100.replace('100x100', '1500x1500') + +await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`) diff --git a/scripts/media/itunes-artist-art-fetcher.ts b/scripts/media/itunes-artist-art-fetcher.ts new file mode 100644 index 0000000..024298d --- /dev/null +++ b/scripts/media/itunes-artist-art-fetcher.ts @@ -0,0 +1,63 @@ +import { iter } from '@fuman/utils' +import { z } from 'zod' +import { minimist, question } from 'zx' + +import { downloadFile, ffetch } from '../../utils/fetch.ts' + +const args = minimist(process.argv.slice(2), { + string: ['filename'], +}) + +const query = args._[0] ?? await question('Search query: ') + +const data = await ffetch('https://itunes.apple.com/search', { + query: { + term: query, + entity: 'musicArtist', + limit: 15, + }, +}).parsedJson(z.object({ + results: z.array(z.object({ + wrapperType: z.literal('artist'), + artistName: z.string(), + artistLinkUrl: z.string(), + primaryGenreName: z.string().default('Unknown'), + }).passthrough()), +})) + +for (const [i, result] of iter.enumerate(data.results)) { + console.log(`${i + 1}. ${result.artistName} (${result.primaryGenreName})`) + continue +} + +console.log('Enter number to download artist art:') + +const number = Number.parseInt(await question('[1] > ') || '1') + +const pageUrl = data.results[number - 1].artistLinkUrl +const $ = await ffetch(pageUrl).cheerio() +const pageData = JSON.parse($('#serialized-server-data').html()!) + +const pageDataValidated = z.tuple([ + z.object({ + data: z.object({ + seoData: z.object({ + artworkUrl: z.string(), + }), + }), + }), +]).parse(pageData) + +// {w}x{h}{c}.{f} +const artworkUrl = pageDataValidated[0].data.seoData.artworkUrl + .replace('{w}', '2500') + .replace('{h}', '2500') + .replace('{c}', 'cc') + .replace('{f}', 'jpg') + +if (artworkUrl === '/assets/meta/apple-music.png') { + console.log('No artwork available') + process.exit(1) +} + +await downloadFile(artworkUrl, args.filename ?? `assets/${query.replace(/\s/g, '_')}.jpg`) diff --git a/scripts/misc/update-forkgram.ts b/scripts/misc/update-forkgram.ts new file mode 100644 index 0000000..878def9 --- /dev/null +++ b/scripts/misc/update-forkgram.ts @@ -0,0 +1,51 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import plist from 'plist' +import { z } from 'zod' +import { $ } from 'zx' +import { ffetch } from '../../utils/fetch.ts' + +const latestVerInfo = await ffetch('https://api.github.com/repos/forkgram/tdesktop/releases/latest').parsedJson( + z.object({ + tag_name: z.string().transform(v => v.replace(/^v/, '')), + assets: z.array(z.object({ + name: z.string(), + browser_download_url: z.string(), + })), + }), +) + +const INSTALL_PATH = '/Applications/Forkgram.app' + +console.log('latest version:', latestVerInfo.tag_name) + +const installedPlist = await readFile(join(INSTALL_PATH, 'Contents/Info.plist'), 'utf8') +const installedPlistParsed = z.object({ + CFBundleShortVersionString: z.string(), +}).parse(plist.parse(installedPlist)) + +console.log('installed version:', installedPlistParsed.CFBundleShortVersionString) + +if (installedPlistParsed.CFBundleShortVersionString === latestVerInfo.tag_name) { + console.log('āœ… no update needed') + process.exit(0) +} + +const arm64Asset = latestVerInfo.assets.find(asset => asset.name === 'Forkgram.macOS.no.auto-update_arm64.zip') +if (!arm64Asset) { + console.error('āŒ no arm64 asset found') + process.exit(1) +} + +console.log('installing new version...') +await $`curl -L ${arm64Asset.browser_download_url} -o /tmp/forkgram.zip` +await $`unzip -o /tmp/forkgram.zip -d /tmp/forkgram` +await $`kill -9 $(pgrep -f /Applications/Forkgram.app/Contents/MacOS/Telegram)` +await $`rm -rf ${INSTALL_PATH}` +await $`mv /tmp/forkgram/Telegram.app ${INSTALL_PATH}` +await $`rm -rf /tmp/forkgram` +await $`xattr -cr ${INSTALL_PATH}` + +await $`open ${INSTALL_PATH}` + +console.log('āœ… done') diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b75efba --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleDetection": "force", + "module": "ESNext", + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "allowJs": true, + + // Best practices + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": false, + + "noPropertyAccessFromIndexSignature": false, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noEmit": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + } +} diff --git a/utils/captcha.ts b/utils/captcha.ts new file mode 100644 index 0000000..11d4f60 --- /dev/null +++ b/utils/captcha.ts @@ -0,0 +1,87 @@ +import { sleep } from '@fuman/utils' +import { z } from 'zod' +import { ffetch } from './fetch.ts' +import { getEnv } from './misc.ts' + +const CreateTaskResponse = z.object({ + errorId: z.number(), + errorCode: z.string().optional().nullable(), + taskId: z.number(), +}) + +const GetTaskResultResponse = z.object({ + errorId: z.number(), + errorCode: z.string().optional().nullable(), + status: z.enum(['ready', 'processing']), + solution: z.unknown().optional(), +}) + +export async function solveCaptcha(task: unknown) { + const res = await ffetch.post('https://api.capmonster.cloud/createTask', { + json: { + clientKey: getEnv('CAPMONSTER_API_TOKEN'), + task, + }, + }).parsedJson(CreateTaskResponse) + + if (res.errorId) { + throw new Error(`createTask error ${res.errorId}: ${res.errorCode}`) + } + + const taskId = res.taskId + + await sleep(5_000) + + let requestCount = 0 + + while (true) { + requestCount += 1 + if (requestCount > 100) { + // "Limit: 120 requests per task. If the limit is exceeded, the user's account may be temporarily locked." + // just to be safe + throw new Error('captcha request count exceeded') + } + + const res = await ffetch.post('https://api.capmonster.cloud/getTaskResult', { + json: { + clientKey: getEnv('CAPMONSTER_API_TOKEN'), + taskId, + }, + }).parsedJson(GetTaskResultResponse) + + if (res.errorId) { + throw new Error(`getTaskResult error ${res.errorId}: ${res.errorCode}`) + } + + if (res.status === 'ready') { + return res.solution + } + + await sleep(2_000) + } +} + +export async function solveRecaptcha(params?: { + url: string + siteKey: string + s?: string + userAgent?: string + cookies?: string + isInvisible?: boolean +}) { + const res = await solveCaptcha({ + type: 'RecaptchaV2TaskProxyless', + websiteURL: params?.url, + websiteKey: params?.siteKey, + recaptchaDataSValue: params?.s, + userAgent: params?.userAgent, + cookies: params?.cookies, + isInvisible: params?.isInvisible, + }) + + if (typeof res !== 'object' || !res || !('gRecaptchaResponse' in res) || typeof res.gRecaptchaResponse !== 'string') { + throw new Error('invalid recaptcha response') + } + + return res.gRecaptchaResponse +} diff --git a/utils/currency.ts b/utils/currency.ts new file mode 100644 index 0000000..f345557 --- /dev/null +++ b/utils/currency.ts @@ -0,0 +1,113 @@ +import { asyncPool } from '@fuman/utils' + +import { z } from 'zod' +import { ffetch } from './fetch.ts' +import { getEnv } from './misc.ts' + +// token management +const TOKENS = getEnv('OXR_TOKENS').split(',') +// api token => requests remaining +const usageAvailable = new Map() +function getToken() { + // find token with the most requests remaining + const token = TOKENS.find(t => usageAvailable.get(t)! > 0) + if (!token) throw new Error('no tokens available') + + // consume 1 request + usageAvailable.set(token, usageAvailable.get(token)! - 1) + + return token +} + +// base => other => value +// NB: ideally we should have expiration and persistence on this +const data = new Map>() + +async function fetchMissingPairs(list: { from: string, to: string }[]) { + const missing = list.filter(c => !data.has(c.from) && !data.has(c.to) && c.from !== c.to) + if (missing.length === 0) return + + const basesToFetch = new Set() + + for (const { from, to } of missing) { + if (!basesToFetch.has(from) && !basesToFetch.has(to)) { + basesToFetch.add(from) + } + } + + if (!usageAvailable.size) { + // NB: ideally we should lock here for a production-ready implementation + + // fetch usage for all tokens + await asyncPool(TOKENS, async (token) => { + const res = await ffetch('https://openexchangerates.org/api/usage.json', { + query: { + app_id: token, + }, + }).parsedJson(z.object({ + status: z.literal(200), + data: z.object({ + app_id: z.string(), + status: z.literal('active'), + usage: z.object({ + requests_remaining: z.number(), + }), + }), + })) + + usageAvailable.set(token, res.data.usage.requests_remaining) + }, { onError: () => 'ignore' }) + + if (!usageAvailable.size) { + throw new Error('failed to fetch usage, are all tokens dead?') + } + } + + // console.log('will fetch bases:', [...basesToFetch]) + + await asyncPool(basesToFetch, async (base) => { + const res = await ffetch('https://openexchangerates.org/api/latest.json', { + query: { + app_id: getToken(), + }, + }).parsedJson(z.object({ + rates: z.record(z.string(), z.number()), + })) + + data.set(base, res.rates) + }) +} + +export async function convertCurrenciesBatch(list: { from: string, to: string, amount: number }[]) { + await fetchMissingPairs(list) + const ret: { from: string, to: string, amount: number, converted: number }[] = [] + + for (const { from, to, amount } of list) { + let result: number + + if (from === to) { + result = amount + } else if (data.has(from)) { + const rate = data.get(from)![to]! + if (!rate) throw new Error(`rate unavailable: ${from} -> ${to}`) + result = amount * rate + // console.log('converted from', from, 'to', to, 'amount', amount, 'result', result, 'rate', rate) + } else if (data.has(to)) { + const rate = data.get(to)![from]! + if (!rate) throw new Error(`rate unavailable: ${from} -> ${to}`) + result = amount / rate + // console.log('converted rev from', from, 'to', to, 'amount', amount, 'result', result, 'rate', rate) + } else { + throw new Error(`rate unavailable: ${from} -> ${to}`) + } + + ret.push({ + from, + to, + amount, + converted: result, + }) + } + + return ret +} diff --git a/utils/fetch.ts b/utils/fetch.ts new file mode 100644 index 0000000..6a8de33 --- /dev/null +++ b/utils/fetch.ts @@ -0,0 +1,37 @@ +import { createWriteStream } from 'node:fs' + +import { type FfetchAddon, ffetchAddons, ffetchBase, type FfetchResultInternals } from '@fuman/fetch' +import { toughCookieAddon } from '@fuman/fetch/tough' +import { ffetchZodAdapter } from '@fuman/fetch/zod' +import { webReadableToFuman, write } from '@fuman/io' +import { nodeWritableToFuman } from '@fuman/node' +import { type CheerioAPI, load } from 'cheerio' + +const cheerioAddon: FfetchAddon Promise }> = { + response: { + async cheerio(this: FfetchResultInternals) { + this._headers ??= {} + this._headers.Accept ??= 'text/html; charset=utf-8' + return load(await this.text()) + }, + }, +} + +export const ffetch = ffetchBase.extend({ + addons: [ + ffetchAddons.parser(ffetchZodAdapter()), + cheerioAddon, + toughCookieAddon(), + ], +}) + +export async function downloadStream(stream: ReadableStream, path: string) { + const file = nodeWritableToFuman(createWriteStream(path)) + await write.pipe(file, webReadableToFuman(stream)) + file.close() +} + +export async function downloadFile(url: string, path: string, extra?: Parameters[1]) { + const stream = await ffetch(url, extra).stream() + await downloadStream(stream, path) +} diff --git a/utils/fs.ts b/utils/fs.ts new file mode 100644 index 0000000..90834ab --- /dev/null +++ b/utils/fs.ts @@ -0,0 +1,19 @@ +import * as fsp from 'node:fs/promises' + +export async function fileExists(path: string): Promise { + try { + const stat = await fsp.stat(path) + return stat.isFile() + } catch { + return false + } +} + +export async function directoryExists(path: string): Promise { + try { + const stat = await fsp.stat(path) + return stat.isDirectory() + } catch { + return false + } +} diff --git a/utils/misc.ts b/utils/misc.ts new file mode 100644 index 0000000..e38398d --- /dev/null +++ b/utils/misc.ts @@ -0,0 +1,10 @@ +import 'dotenv/config' + +export function getEnv(key: string): string +export function getEnv(key: string, parser: (value: string) => T): T +export function getEnv(key: string, parser?: (value: string) => T): T | string { + const value = process.env[key] + if (!value) throw new Error(`env variable ${key} not found`) + if (!parser) return value + return parser(value) +} diff --git a/utils/navidrome.ts b/utils/navidrome.ts new file mode 100644 index 0000000..be66efb --- /dev/null +++ b/utils/navidrome.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' +import { ffetch as ffetchBase } from './fetch.ts' +import { getEnv } from './misc.ts' + +export const navidromeFfetch = ffetchBase.extend({ + baseUrl: getEnv('NAVIDROME_ENDPOINT'), + headers: { + 'x-nd-authorization': `Bearer ${getEnv('NAVIDROME_TOKEN')}`, + }, +}) + +export const NavidromeSong = z.object({ + id: z.string(), + title: z.string(), + album: z.string(), + albumArtist: z.string(), + artist: z.string(), + path: z.string(), + duration: z.number(), +}) +export type NavidromeSong = z.infer + +export function fetchSongs(offset: number, pageSize: number) { + return navidromeFfetch('/api/song', { + query: { + _start: offset, + _end: offset + pageSize, + _order: 'ASC', + _sort: 'title', + }, + }).parsedJson(z.array(NavidromeSong)) +} diff --git a/utils/oauth.ts b/utils/oauth.ts new file mode 100644 index 0000000..98c31f6 --- /dev/null +++ b/utils/oauth.ts @@ -0,0 +1,78 @@ +import type { MaybePromise } from '@fuman/utils' +import * as fsp from 'node:fs/promises' +import { z } from 'zod' + +export interface OauthStorage { + write: (value: string) => MaybePromise + read: () => MaybePromise +} + +export class LocalOauthStorage implements OauthStorage { + constructor(private filename: string) {} + + async write(value: string) { + await fsp.writeFile(this.filename, value) + } + + async read() { + try { + return await fsp.readFile(this.filename, 'utf8') + } catch (e) { + return null + } + } +} + +const OauthState = z.object({ + accessToken: z.string(), + refreshToken: z.string().optional(), + expiresAt: z.number(), +}) +type OauthState = z.infer + +export class OauthHandler { + constructor(private params: { + storage: OauthStorage + refreshToken: (refreshToken: string) => MaybePromise<{ + accessToken: string + refreshToken: string + expiresIn: number + }> + /** number of milliseconds to subtract from token expiration time */ + jitter?: number + }) { + this.params.jitter = this.params.jitter ?? 5000 + } + + #cache: OauthState | null = null + async readOauthState() { + if (this.#cache) return this.#cache + const value = await this.params.storage.read() + if (!value) return null + + return OauthState.parse(JSON.parse(value)) + } + + async writeOauthState(value: OauthState) { + this.#cache = value + await this.params.storage.write(JSON.stringify(value)) + } + + async getAccessToken() { + const state = await this.readOauthState() + if (!state) return null + + if (state.expiresAt < Date.now() + this.params.jitter!) { + if (!state.refreshToken) return null + const { accessToken, refreshToken, expiresIn } = await this.params.refreshToken(state.refreshToken) + await this.writeOauthState({ + accessToken, + refreshToken, + expiresAt: Date.now() + expiresIn * 1000, + }) + return accessToken + } + + return state.accessToken + } +} diff --git a/utils/telegram.ts b/utils/telegram.ts new file mode 100644 index 0000000..d10733f --- /dev/null +++ b/utils/telegram.ts @@ -0,0 +1,11 @@ +import { TelegramClient, type TelegramClientOptions } from '@mtcute/node' +import { getEnv } from './misc.ts' + +export function createTg(session: string, extra?: Partial) { + return new TelegramClient({ + apiId: getEnv('TELEGRAM_API_ID', Number), + apiHash: getEnv('TELEGRAM_API_HASH'), + storage: `assets/${session}.session`, + ...extra, + }) +} diff --git a/utils/webdav.ts b/utils/webdav.ts new file mode 100644 index 0000000..fe7aad2 --- /dev/null +++ b/utils/webdav.ts @@ -0,0 +1,324 @@ +import { ffetchBase, type FfetchResult } from '@fuman/fetch' +import { asNonNull, assert, base64, utf8 } from '@fuman/utils' +import { Parser } from 'htmlparser2' +import { z } from 'zod' + +const XML_HEADER = '' + +export interface WebdavClientOptions { + baseUrl: string + username?: string + password?: string + headers?: Record +} + +export interface WebdavResourceBase { + href: string + name: string + status: string + lastModified?: Date + raw: Record + // todo: lockdiscovery + // todo: supportedlock +} + +export interface WebdavCollection extends WebdavResourceBase { + type: 'collection' +} + +export interface WebdavFile extends WebdavResourceBase { + type: 'file' + size: number + etag?: string + contentType?: string +} + +export type WebdavResource = WebdavCollection | WebdavFile + +const DResponseSchema = z.object({ + 'd:href': z.string(), + 'd:propstat': z.object({ + 'd:prop': z.object({ + 'd:resourcetype': z.union([ + z.literal(true), + z.object({ + 'd:collection': z.literal(true), + }), + ]), + 'd:displayname': z.union([z.literal(true), z.string()]), + 'd:getcontentlength': z.coerce.number().optional(), + 'd:getlastmodified': z.string().transform(v => new Date(v)).optional(), + 'd:getetag': z.string().optional(), + 'd:getcontenttype': z.string().optional(), + }).passthrough(), + 'd:status': z.string(), + }), +}) + +const DMultistatusSchema = z.object({ + 'd:multistatus': z.tuple([z.object({ + 'd:response': z.array(DResponseSchema), + })]), +}) + +function escapeXml(str: string) { + return str.replace(//g, '>') +} + +function xmlToJson(xml: string) { + const res: Record = {} + + const stack: any[] = [res] + + const parser = new Parser({ + onopentag(name) { + name = name.toLowerCase() + + const node: any = {} + const top = stack[stack.length - 1] + if (!top[name]) { + top[name] = [] + } + top[name].push(node) + stack.push(node) + }, + onclosetag(name) { + const obj = stack.pop() + const top = stack[stack.length - 1] + const ourIdx = top[name].length - 1 + + const keys = Object.keys(obj) + if (keys.length === 1 && keys[0] === '_text') { + top[name][ourIdx] = obj._text + } else if (keys.length === 0) { + top[name][ourIdx] = true + } else { + // replace one-element arrays with the element itself + for (const key of keys) { + if (key === 'd:response') continue + const val = obj[key] + if (Array.isArray(val) && val.length === 1) { + obj[key] = val[0] + } + } + } + }, + ontext(text) { + const top = stack[stack.length - 1] + if (top._text === undefined) { + top._text = '' + } + top._text += text + }, + }) + + parser.write(xml) + parser.end() + + return res +} + +export class WebdavClient { + readonly ffetch: typeof ffetchBase + readonly basePath + + constructor(options: WebdavClientOptions) { + const headers: Record = { + 'Content-Type': 'application/xml; charset="utf-8"', + ...options.headers, + } + if (options.username) { + let authStr = options.username + if (options.password) { + authStr += `:${options.password}` + } + headers.Authorization = `Basic ${base64.encode(utf8.encoder.encode(authStr))}` + } + + this.ffetch = ffetchBase.extend({ + baseUrl: options.baseUrl, + headers, + }) + this.basePath = new URL(options.baseUrl).pathname + if (this.basePath[this.basePath.length - 1] !== '/') { + this.basePath += '/' + } + } + + mapPropfindResponse = (obj: z.infer): WebdavResource => { + const name = obj['d:propstat']['d:prop']['d:displayname'] + const base: WebdavResourceBase = { + href: obj['d:href'], + name: name === true ? '' : name, + status: obj['d:propstat']['d:status'], + lastModified: obj['d:propstat']['d:prop']['d:getlastmodified'], + raw: obj['d:propstat']['d:prop'], + } + if (base.href.startsWith(this.basePath)) { + base.href = base.href.slice(this.basePath.length) + if (base.href !== '/') { + base.href = `/${base.href}` + } + } + + if (typeof obj['d:propstat']['d:prop']['d:resourcetype'] === 'object' && obj['d:propstat']['d:prop']['d:resourcetype']['d:collection']) { + const res = base as WebdavCollection + res.type = 'collection' + return res + } else { + const res = base as WebdavFile + res.type = 'file' + res.size = asNonNull(obj['d:propstat']['d:prop']['d:getcontentlength']) + res.etag = obj['d:propstat']['d:prop']['d:getetag'] + res.contentType = obj['d:propstat']['d:prop']['d:getcontenttype'] + return res + } + } + + async propfind( + path: string, + params?: { + depth?: number | 'infinity' + properties?: string[] + }, + ): Promise { + const body = params?.properties + ? [ + XML_HEADER, + '', + '', + ...params.properties.map(prop => `<${prop}/>`), + '', + '', + ].join('\n') + : undefined + const res = await this.ffetch(path, { + method: 'PROPFIND', + headers: { + Depth: params?.depth ? String(params.depth) : '1', + }, + body, + }).text() + + const json = DMultistatusSchema.parse(xmlToJson(res)) + return json['d:multistatus'][0]['d:response'].map(this.mapPropfindResponse) + } + + async proppatch(path: string, params: { + set?: Record + remove?: string[] + }): Promise { + if (!params.set && !params.remove) return + + const lines: string[] = [ + XML_HEADER, + '', + ] + if (params.set) { + lines.push('') + for (const [key, value] of Object.entries(params.set ?? {})) { + lines.push(`<${key}>${ + typeof value === 'object' ? value._xml : escapeXml(value) + }`) + } + lines.push('') + } + if (params.remove) { + lines.push('') + for (const key of params.remove) { + lines.push(`<${key}/>`) + } + lines.push('') + } + lines.push('') + + const body = lines.join('\n') + await this.ffetch(path, { + method: 'PROPPATCH', + body, + }) + } + + async mkcol(path: string): Promise { + const res = await this.ffetch(path, { + method: 'MKCOL', + }) + if (res.status !== 201) throw new Error(`mkcol failed: ${res.status}`) + } + + async delete(path: string): Promise { + const res = await this.ffetch(path, { + method: 'DELETE', + }) + if (res.status !== 204) throw new Error(`delete failed: ${res.status}`) + } + + get(path: string): FfetchResult { + return this.ffetch(path, { + method: 'GET', + }) + } + + async put(path: string, body: BodyInit): Promise { + await this.ffetch(path, { + method: 'PUT', + body, + }) + } + + async copy( + source: string, + destination: string, + params?: { + /** whether to overwrite the destination if it exists */ + overwrite?: boolean + depth?: number | 'infinity' + }, + ): Promise { + if (destination[0] === '/') destination = destination.slice(1) + if (this.basePath) destination = this.basePath + destination + const headers: Record = { + Destination: destination, + } + if (params?.overwrite !== true) { + headers.Overwrite = 'F' + } + if (params?.depth) { + headers.Depth = String(params.depth) + } + + const res = await this.ffetch(source, { + method: 'COPY', + headers, + }) + if (res.status !== 201) throw new Error(`copy failed: ${res.status}`) + } + + async move( + source: string, + destination: string, + params?: { + /** whether to overwrite the destination if it exists */ + overwrite?: boolean + depth?: number | 'infinity' + }, + ): Promise { + if (destination[0] === '/') destination = destination.slice(1) + if (this.basePath) destination = this.basePath + destination + const headers: Record = { + Destination: destination, + } + if (params?.overwrite !== true) { + headers.Overwrite = 'F' + } + if (params?.depth) { + headers.Depth = String(params.depth) + } + + const res = await this.ffetch(source, { + method: 'MOVE', + headers, + }) + if (res.status !== 201) throw new Error(`move failed: ${res.status}`) + } +} diff --git a/utils/xml.ts b/utils/xml.ts new file mode 100644 index 0000000..b9c928d --- /dev/null +++ b/utils/xml.ts @@ -0,0 +1,20 @@ +import type { ChildNode } from 'domhandler' +import { DomHandler } from 'domhandler' +import { Parser } from 'htmlparser2' + +export function xmlToDom(xml: string) { + let _error: Error | null = null + let _dom: ChildNode[] | null = null + + const handler = new DomHandler((error, dom) => { + _error = error + _dom = dom + }) + const parser = new Parser(handler) + parser.write(xml) + parser.end() + + if (_error) throw _error + + return _dom! +} From ae327488d4607e723742069bf9bb6a6d8e71d896 Mon Sep 17 00:00:00 2001 From: teidesu Date: Tue, 14 Jan 2025 05:42:32 +0300 Subject: [PATCH 05/61] Update README.md --- README | 1 - README.md | 13 +++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 257801a..0000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -meow :3 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..10284c3 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +## teidesu/scripts + +hewwo so this repo is a collection of some scripts i wrote over time that might be interesting to someone except me + +feel free to use the code in this repo in any way you want + +> note: this repo is auto-generated from a private one, because there is some stuff i am legally not able to share +> +> if this results in some files being missing and some script being un-runnable, please do hmu! + +### license + +this repo is licensed under [DON'T BE A DICK PUBLIC LICENSE](https://github.com/philsturgeon/dbad) \ No newline at end of file From 9891d7734d416f928d70d98af5b93124fec2e5be Mon Sep 17 00:00:00 2001 From: teidesu Date: Tue, 14 Jan 2025 05:42:32 +0300 Subject: [PATCH 06/61] Update README.md --- README | 1 - README.md | 13 +++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 257801a..0000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -meow :3 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..10284c3 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +## teidesu/scripts + +hewwo so this repo is a collection of some scripts i wrote over time that might be interesting to someone except me + +feel free to use the code in this repo in any way you want + +> note: this repo is auto-generated from a private one, because there is some stuff i am legally not able to share +> +> if this results in some files being missing and some script being un-runnable, please do hmu! + +### license + +this repo is licensed under [DON'T BE A DICK PUBLIC LICENSE](https://github.com/philsturgeon/dbad) \ No newline at end of file From 89e10fc021e3c64f638bdb1370db9a423b03afae Mon Sep 17 00:00:00 2001 From: desu-bot Date: Thu, 16 Jan 2025 03:25:20 +0000 Subject: [PATCH 07/61] chore: update public repo --- package.json | 3 + pnpm-lock.yaml | 132 +++++++++++++++++ scripts/media/fwmc-radio.ts | 40 +++++ scripts/media/soundcloud-dl.ts | 262 +++++++++++++++++++++++++++++++++ utils/fs.ts | 4 + utils/misc.ts | 6 + utils/opus.ts | 30 ++++ utils/strings.ts | 54 +++++++ 8 files changed, 531 insertions(+) create mode 100644 scripts/media/fwmc-radio.ts create mode 100644 scripts/media/soundcloud-dl.ts create mode 100644 utils/opus.ts create mode 100644 utils/strings.ts diff --git a/package.json b/package.json index 4336100..8e7bf7c 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,11 @@ "dependencies": { "@faker-js/faker": "^9.3.0", "@fuman/io": "^0.0.4", + "@fuman/net": "^0.0.9", "@fuman/node": "^0.0.4", "@mtcute/node": "^0.19.1", "@types/plist": "^3.0.5", + "@types/spinnies": "^0.5.3", "cheerio": "^1.0.0", "es-main": "^1.3.0", "filesize": "^10.1.6", @@ -19,6 +21,7 @@ "nanoid": "^5.0.9", "plist": "^3.1.0", "qrcode-terminal": "^0.12.0", + "spinnies": "^0.5.1", "tough-cookie": "^5.0.0", "tough-cookie-file-store": "^2.0.3", "undici": "^7.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7616233..de66943 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@fuman/io': specifier: ^0.0.4 version: 0.0.4 + '@fuman/net': + specifier: ^0.0.9 + version: 0.0.9 '@fuman/node': specifier: ^0.0.4 version: 0.0.4 @@ -23,6 +26,9 @@ importers: '@types/plist': specifier: ^3.0.5 version: 3.0.5 + '@types/spinnies': + specifier: ^0.5.3 + version: 0.5.3 cheerio: specifier: ^1.0.0 version: 1.0.0 @@ -47,6 +53,9 @@ importers: qrcode-terminal: specifier: ^0.12.0 version: 0.12.0 + spinnies: + specifier: ^0.5.1 + version: 0.5.1 tough-cookie: specifier: ^5.0.0 version: 5.0.0 @@ -264,9 +273,15 @@ packages: '@fuman/io@0.0.4': resolution: {integrity: sha512-IXzBJjHTVKyi04WaGtSXE0dhL3QK45ekrEZNfH/V59XQ1WupSqWevfSWd9T07rdagc2jtaeu8aJY6bwaiJpdYg==} + '@fuman/io@0.0.8': + resolution: {integrity: sha512-+cRZ2JOMYceNQ2Rh6UYro5XVa11j29Sdd3Yhi4GfxAx6ZwCNIw3P80xcTRwCZSfMPLDNN9Etkq7NIc5v9lpItw==} + '@fuman/net@0.0.4': resolution: {integrity: sha512-a8Isnj+qgRNaqqmBCT6lZ9GZj5F3vQdygN5AzB6GGCbLKcOeH+1u5Twh5CUAW/dM7oogrTWOwCqgvS2XHbjzaQ==} + '@fuman/net@0.0.9': + resolution: {integrity: sha512-asn7VJbT8woVXAFCUMZrdyNZCSsXZclraeVZ6RYJ+T3RwQ+JfMMZtXLLTZ7XHrBPxk8x8hoHOJa/Fnyfm+ggbQ==} + '@fuman/node@0.0.4': resolution: {integrity: sha512-tgwbIceUHWuwh4RTwJRQ1sLjzuIGrWx0SeCrqYhGF+IkI/B7DY0FP2SZykWImkVDtW8IzmdZskPZqiDINRGcNg==} @@ -375,6 +390,9 @@ packages: '@types/plist@3.0.5': resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + '@types/spinnies@0.5.3': + resolution: {integrity: sha512-HYrOubG2TVgRQRKcW1HJ/1eJIIBpLqDoJo551McJgWdO8xzxnaxu/bPKdqC/7okoEy4ZZjy3I4/DwK1sz2OCog==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -485,10 +503,18 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -553,6 +579,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -578,14 +608,24 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -1032,6 +1072,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1320,6 +1364,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -1383,6 +1431,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1558,6 +1610,10 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1592,6 +1648,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1623,6 +1682,9 @@ packages: spdx-license-ids@3.0.20: resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} + spinnies@0.5.1: + resolution: {integrity: sha512-WpjSXv9NQz0nU3yCT9TFEOfpFrXADY9C5fG6eAJqixLhvTX1jP3w92Y8IE5oafIe42nlF9otjhllnXN/QCaB3A==} + stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} @@ -1633,6 +1695,10 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1649,6 +1715,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2040,11 +2110,20 @@ snapshots: dependencies: '@fuman/utils': 0.0.4 + '@fuman/io@0.0.8': + dependencies: + '@fuman/utils': 0.0.4 + '@fuman/net@0.0.4': dependencies: '@fuman/io': 0.0.4 '@fuman/utils': 0.0.4 + '@fuman/net@0.0.9': + dependencies: + '@fuman/io': 0.0.8 + '@fuman/utils': 0.0.4 + '@fuman/node@0.0.4': dependencies: '@fuman/io': 0.0.4 @@ -2182,6 +2261,8 @@ snapshots: '@types/node': 22.10.0 xmlbuilder: 15.1.1 + '@types/spinnies@0.5.3': {} + '@types/unist@3.0.3': {} '@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2)': @@ -2320,8 +2401,14 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -2388,6 +2475,12 @@ snapshots: ccount@2.0.1: {} + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2426,16 +2519,26 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} comment-parser@1.4.1: {} @@ -2948,6 +3051,8 @@ snapshots: graphemer@1.4.0: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} hasown@2.0.2: @@ -3393,6 +3498,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-fn@2.1.0: {} + mimic-response@3.1.0: {} min-indent@1.0.1: {} @@ -3449,6 +3556,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3637,6 +3748,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + reusify@1.0.4: {} run-parallel@1.2.0: @@ -3663,6 +3779,8 @@ snapshots: shebang-regex@3.0.0: {} + signal-exit@3.0.7: {} + simple-concat@1.0.1: {} simple-get@4.0.1: @@ -3696,6 +3814,12 @@ snapshots: spdx-license-ids@3.0.20: {} + spinnies@0.5.1: + dependencies: + chalk: 2.4.2 + cli-cursor: 3.1.0 + strip-ansi: 5.2.0 + stable-hash@0.0.4: {} string-width@4.2.3: @@ -3708,6 +3832,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3720,6 +3848,10 @@ snapshots: strip-json-comments@3.1.1: {} + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 diff --git a/scripts/media/fwmc-radio.ts b/scripts/media/fwmc-radio.ts new file mode 100644 index 0000000..a12dc4d --- /dev/null +++ b/scripts/media/fwmc-radio.ts @@ -0,0 +1,40 @@ +import { mkdir } from 'node:fs/promises' +import { asyncPool } from '@fuman/utils' +import json5 from 'json5' +import Spinnies from 'spinnies' +import { z } from 'zod' +import { downloadFile, ffetch } from '../../utils/fetch.ts' +import { fileExists } from '../../utils/fs.ts' +import { parseJsObject } from '../../utils/strings.ts' + +const $ = await ffetch('https://fwmc-ai.github.io/radio/').cheerio() + +const script = $('script:icontains(const playlist =)').html()! + +const playlistJs = parseJsObject(`[${script.split('const playlist = [').at(-1)!}`)! +const playlist = z.array( + z.object({ + id: z.string(), + title: z.string(), + file: z.string(), + cover: z.string(), + category: z.enum(['original', 'cover']), + lyrics: z.string(), + }), +).parse(json5.parse(playlistJs)) + +const spinnies = new Spinnies() + +await mkdir('assets/fwmc-radio', { recursive: true }) + +await asyncPool(playlist, async (item) => { + const dlPath = `assets/fwmc-radio/${item.id}.mp3` + if (await fileExists(dlPath)) return + + spinnies.add(item.id, { text: item.title }) + await downloadFile(new URL(item.file, 'https://fwmc-ai.github.io/radio/').toString(), dlPath) + spinnies.remove(item.id) +}) + +console.log('done') +spinnies.stopAll() diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts new file mode 100644 index 0000000..fc640dd --- /dev/null +++ b/scripts/media/soundcloud-dl.ts @@ -0,0 +1,262 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { ffetchAddons } from '@fuman/fetch' +import { asyncPool, base64 } from '@fuman/utils' +import { load } from 'cheerio' +import Spinnies from 'spinnies' +import { ProxyAgent } from 'undici' +import { z } from 'zod' +import { $ } from 'zx' +import { downloadFile, ffetch as ffetchBase } from '../../utils/fetch.ts' +import { sanitizeFilename } from '../../utils/fs.ts' +import { chunks, getEnv } from '../../utils/misc.ts' +import { generateOpusImageBlob } from '../../utils/opus.ts' + +const ffetchApi = ffetchBase.extend({ + baseUrl: 'https://api-v2.soundcloud.com', + // @ts-expect-error lol fixme + query: { + client_id: '4BowhSywvkJtklODQDzjNMq9sK9wyDJ4', + app_version: '1736857534', + app_locale: 'en', + }, + addons: [ + ffetchAddons.rateLimitHandler(), + ], + rateLimit: { + isRejected(res) { + return res.status === 429 + }, + defaultWaitTime: 10_000, + }, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'Authorization': `OAuth ${getEnv('SOUNDCLOUD_TOKEN')}`, + }, +}) +const ffetchHtml = ffetchBase.extend({ + baseUrl: 'https://soundcloud.com', + headers: { + Cookie: `oauth_token=${getEnv('SOUNDCLOUD_TOKEN')}`, + }, + extra: { + // @ts-expect-error lol fixme + dispatcher: new ProxyAgent('http://127.0.0.1:7891'), + }, +}) + +const ScTrack = z.object({ + id: z.number(), + kind: z.literal('track'), + title: z.string(), + duration: z.number(), + permalink_url: z.string(), + artwork_url: z.string().transform(s => s.replace('-large.jpg', '-t500x500.jpg')).nullable(), + media: z.object({ + transcodings: z.array(z.object({ + url: z.string(), + preset: z.string(), + format: z.object({ + protocol: z.string(), + mime_type: z.string(), + }), + quality: z.string(), + is_legacy_transcoding: z.boolean(), + })), + }), + track_authorization: z.string(), + user: z.object({ + username: z.string(), + permalink: z.string(), + }), +}) +type ScTrack = z.infer + +const ScPlaylist = z.object({ + id: z.number(), + title: z.string(), + duration: z.number(), + permalink_url: z.string(), + genre: z.string(), + description: z.string().nullable(), + track_count: z.number(), + user: z.object({ + username: z.string(), + }), + tracks: z.array(z.union([ + ScTrack, + z.object({ + id: z.number(), + kind: z.literal('track'), + }), + ])), +}) +type ScPlaylist = z.infer + +function extractHydrationData(html: string) { + const $ = load(html) + const script = $('script:contains(window.__sc_hydration = )') + return JSON.parse(script.html()!.replace('window.__sc_hydration = ', '').slice(0, -1)) +} + +async function fetchTrackByUrl(url: string) { + const html = await ffetchHtml(url).text() + const hydrationData = extractHydrationData(html) + const track = hydrationData.find(it => it.hydratable === 'sound') + if (!track) throw new Error('no track found') + + return ScTrack.parse(track.data) +} + +async function fetchPlaylistByUrl(url: string) { + const html = await ffetchHtml(url).text() + const hydrationData = extractHydrationData(html) + const playlist = hydrationData.find(it => it.hydratable === 'playlist') + if (!playlist) throw new Error('no playlist found') + + return ScPlaylist.parse(playlist.data) +} + +async function fetchTracksById(trackIds: number[]) { + return ffetchApi('/tracks', { + query: { + ids: trackIds.join(','), + }, + }).parsedJson(z.array(ScTrack)) +} + +async function downloadTrack(track: ScTrack, opts: { + /* download destination (filename without extension) */ + destination: string +}) { + const artworkPath = join('assets', `sc-tmp-${track.id}.jpg`) + const artworkBytes = track.artwork_url ? new Uint8Array(await ffetchHtml(track.artwork_url).arrayBuffer()) : null + + // find the best transcoding + const transcoding = track.media.transcodings.sort((a, b) => { + // prefer non-legacy transcodings + if (a.is_legacy_transcoding && !b.is_legacy_transcoding) return -1 + if (!a.is_legacy_transcoding && b.is_legacy_transcoding) return 1 + + // prefer hq + if (a.quality === 'sq' && b.quality === 'hq') return -1 + if (a.quality === 'hq' && b.quality === 'sq') return 1 + + // prefer opus + if (a.preset === 'opus_0_0' && b.preset !== 'opus_0_0') return -1 + if (a.preset !== 'opus_0_0' && b.preset === 'opus_0_0') return 1 + + return 0 + })[0] + + const { url: hlsUrl } = await ffetchApi(transcoding.url, { + query: { + track_authorization: track.track_authorization, + }, + }).parsedJson(z.object({ + url: z.string(), + })) + + const ext = transcoding.format.mime_type.match(/^audio\/(\w+)(;|$)/)![1] + const filename = `${opts.destination}.${ext}` + + const params: string[] = [ + '-y', + '-i', + hlsUrl, + '-c', + 'copy', + ] + + if (ext === 'mp3') { + if (artworkBytes) { + await writeFile(artworkPath, artworkBytes) + params.push( + '-i', + artworkPath, + '-map', + '0:a', + '-map', + '1:0', + ) + } + params.push( + '-id3v2_version', + '3', + '-metadata:s:v', + 'title="Album cover"', + '-metadata:s:v', + 'comment="Cover (front)"', + ) + } else if (ext === 'ogg' && artworkBytes) { + const blob = base64.encode(await generateOpusImageBlob(artworkBytes)) + params.push( + '-metadata', + `metadata_block_picture=${blob}`, + ) + } + + params.push( + '-metadata', + `title=${track.title}`, + '-metadata', + `artist=${track.user.username}`, + filename, + ) + + await $`ffmpeg ${params}`.quiet(true) +} + +async function downloadPlaylist(playlist: ScPlaylist) { + const tracks: ScTrack[] = [] + const tracksToFetch = new Set() + const trackIdToPosition = new Map() + + for (let i = 0; i < playlist.tracks.length; i++) { + const track = playlist.tracks[i] + trackIdToPosition.set(track.id, i + 1) + if ('user' in track) { + tracks.push(track) + } else { + tracksToFetch.add(track.id) + } + } + + const spinnies = new Spinnies() + + if (tracksToFetch.size) { + let remaining = tracksToFetch.size + spinnies.add('fetching', { text: `fetching ${remaining} tracks` }) + await asyncPool(chunks(Array.from(tracksToFetch), 20), async (ids) => { + const res = await fetchTracksById(Array.from(ids)) + for (const track of res) { + tracks.push(track) + } + remaining -= ids.length + spinnies.update('fetching', { text: `fetching ${remaining} tracks` }) + }) + spinnies.succeed('fetching') + } + + const destDir = join('assets/soundcloud-dl', sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)) + await mkdir(destDir, { recursive: true }) + + const posPadSize = Math.ceil(Math.log10(tracks.length)) + + await asyncPool(tracks, async (track) => { + const position = trackIdToPosition.get(track.id)! + const filename = `${position.toString().padStart(posPadSize, '0')}. ${track.user.username} - ${track.title}` + + spinnies.add(`${track.id}`, { text: filename }) + await downloadTrack(track, { + destination: join(destDir, filename), + }) + + spinnies.remove(`${track.id}`) + }, { limit: 8 }) + + console.log('done') + spinnies.stopAll() +} + +await downloadPlaylist(await fetchPlaylistByUrl('https://soundcloud.com/user-398958278/sets/l2grace')) diff --git a/utils/fs.ts b/utils/fs.ts index 90834ab..11ab17f 100644 --- a/utils/fs.ts +++ b/utils/fs.ts @@ -17,3 +17,7 @@ export async function directoryExists(path: string): Promise { return false } } + +export function sanitizeFilename(filename: string) { + return filename.replace(/[/\\?%*:|"<>]/g, '_') +} diff --git a/utils/misc.ts b/utils/misc.ts index e38398d..c3ae40c 100644 --- a/utils/misc.ts +++ b/utils/misc.ts @@ -8,3 +8,9 @@ export function getEnv(key: string, parser?: (value: string) => T): T | strin if (!parser) return value return parser(value) } + +export function* chunks(arr: T[], size: number) { + for (let i = 0; i < arr.length; i += size) { + yield arr.slice(i, i + size) + } +} diff --git a/utils/opus.ts b/utils/opus.ts new file mode 100644 index 0000000..06ff13b --- /dev/null +++ b/utils/opus.ts @@ -0,0 +1,30 @@ +import { Bytes, write } from '@fuman/io' +import { $ } from 'zx' + +export async function generateOpusImageBlob(image: Uint8Array) { + // todo we should probably not use ffprobe here but whatever lol + const proc = $`ffprobe -of json -v error -show_entries stream=codec_name,width,height pipe:0` + proc.stdin.write(image) + proc.stdin.end() + const json = await proc.json() + + const img = json.streams[0] + // https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8 + const mime = img.codec_name === 'mjpeg' ? 'image/jpeg' : 'image/png' + const description = 'Cover Artwork' + + const res = Bytes.alloc(image.length + 128) + write.uint32be(res, 3) // picture type = album cover + write.uint32be(res, mime.length) + write.rawString(res, mime) + write.uint32be(res, description.length) + write.rawString(res, description) + write.uint32be(res, img.width) + write.uint32be(res, img.height) + write.uint32be(res, 0) // color depth + write.uint32be(res, 0) // color index (unused, for gifs) + write.uint32be(res, image.length) + write.bytes(res, image) + + return res.result() +} diff --git a/utils/strings.ts b/utils/strings.ts new file mode 100644 index 0000000..fec4799 --- /dev/null +++ b/utils/strings.ts @@ -0,0 +1,54 @@ +export function parseJsObject(str: string, offset = 0) { + let i = offset + const len = str.length + let start = -1 + let end = -1 + + const depth = { + '{': 0, + '[': 0, + } + + const possibleQuotes = { + '"': true, + '\'': true, + '`': true, + } + let inQuote: string | null = null + let escapeNextQuote = false + + while (i < len) { + const char = str[i] + if (char in possibleQuotes && !escapeNextQuote) { + if (inQuote === null) { + inQuote = char + } else if (char === inQuote) { + inQuote = null + } + } else if (inQuote != null) { + escapeNextQuote = char === '\\' && !escapeNextQuote + } else if (inQuote == null && char in depth) { + if (start === -1) { + start = i + } + depth[char] += 1 + } else if (inQuote == null && ( + char === '}' || char === ']' + )) { + if (char === '}') depth['{'] -= 1 + if (char === ']') depth['['] -= 1 + + if (depth['{'] === 0 && depth['['] === 0) { + end = i + 1 + break + } + } + i += 1 + } + + if (start === -1 && end === -1) return null + if (depth['{'] !== 0 || depth['['] !== 0) throw new SyntaxError('Mismatched brackets') + if (inQuote) throw new SyntaxError('Unclosed string') + + return str.substring(start, end) +} From 2423324540d823d31ccd17f787860732a1d36062 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Thu, 16 Jan 2025 03:25:20 +0000 Subject: [PATCH 08/61] chore: update public repo --- package.json | 3 + pnpm-lock.yaml | 132 +++++++++++++++++ scripts/media/fwmc-radio.ts | 40 +++++ scripts/media/soundcloud-dl.ts | 262 +++++++++++++++++++++++++++++++++ utils/fs.ts | 4 + utils/misc.ts | 6 + utils/opus.ts | 30 ++++ utils/strings.ts | 54 +++++++ 8 files changed, 531 insertions(+) create mode 100644 scripts/media/fwmc-radio.ts create mode 100644 scripts/media/soundcloud-dl.ts create mode 100644 utils/opus.ts create mode 100644 utils/strings.ts diff --git a/package.json b/package.json index 4336100..8e7bf7c 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,11 @@ "dependencies": { "@faker-js/faker": "^9.3.0", "@fuman/io": "^0.0.4", + "@fuman/net": "^0.0.9", "@fuman/node": "^0.0.4", "@mtcute/node": "^0.19.1", "@types/plist": "^3.0.5", + "@types/spinnies": "^0.5.3", "cheerio": "^1.0.0", "es-main": "^1.3.0", "filesize": "^10.1.6", @@ -19,6 +21,7 @@ "nanoid": "^5.0.9", "plist": "^3.1.0", "qrcode-terminal": "^0.12.0", + "spinnies": "^0.5.1", "tough-cookie": "^5.0.0", "tough-cookie-file-store": "^2.0.3", "undici": "^7.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7616233..de66943 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@fuman/io': specifier: ^0.0.4 version: 0.0.4 + '@fuman/net': + specifier: ^0.0.9 + version: 0.0.9 '@fuman/node': specifier: ^0.0.4 version: 0.0.4 @@ -23,6 +26,9 @@ importers: '@types/plist': specifier: ^3.0.5 version: 3.0.5 + '@types/spinnies': + specifier: ^0.5.3 + version: 0.5.3 cheerio: specifier: ^1.0.0 version: 1.0.0 @@ -47,6 +53,9 @@ importers: qrcode-terminal: specifier: ^0.12.0 version: 0.12.0 + spinnies: + specifier: ^0.5.1 + version: 0.5.1 tough-cookie: specifier: ^5.0.0 version: 5.0.0 @@ -264,9 +273,15 @@ packages: '@fuman/io@0.0.4': resolution: {integrity: sha512-IXzBJjHTVKyi04WaGtSXE0dhL3QK45ekrEZNfH/V59XQ1WupSqWevfSWd9T07rdagc2jtaeu8aJY6bwaiJpdYg==} + '@fuman/io@0.0.8': + resolution: {integrity: sha512-+cRZ2JOMYceNQ2Rh6UYro5XVa11j29Sdd3Yhi4GfxAx6ZwCNIw3P80xcTRwCZSfMPLDNN9Etkq7NIc5v9lpItw==} + '@fuman/net@0.0.4': resolution: {integrity: sha512-a8Isnj+qgRNaqqmBCT6lZ9GZj5F3vQdygN5AzB6GGCbLKcOeH+1u5Twh5CUAW/dM7oogrTWOwCqgvS2XHbjzaQ==} + '@fuman/net@0.0.9': + resolution: {integrity: sha512-asn7VJbT8woVXAFCUMZrdyNZCSsXZclraeVZ6RYJ+T3RwQ+JfMMZtXLLTZ7XHrBPxk8x8hoHOJa/Fnyfm+ggbQ==} + '@fuman/node@0.0.4': resolution: {integrity: sha512-tgwbIceUHWuwh4RTwJRQ1sLjzuIGrWx0SeCrqYhGF+IkI/B7DY0FP2SZykWImkVDtW8IzmdZskPZqiDINRGcNg==} @@ -375,6 +390,9 @@ packages: '@types/plist@3.0.5': resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + '@types/spinnies@0.5.3': + resolution: {integrity: sha512-HYrOubG2TVgRQRKcW1HJ/1eJIIBpLqDoJo551McJgWdO8xzxnaxu/bPKdqC/7okoEy4ZZjy3I4/DwK1sz2OCog==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -485,10 +503,18 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -553,6 +579,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -578,14 +608,24 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -1032,6 +1072,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1320,6 +1364,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -1383,6 +1431,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1558,6 +1610,10 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1592,6 +1648,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1623,6 +1682,9 @@ packages: spdx-license-ids@3.0.20: resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} + spinnies@0.5.1: + resolution: {integrity: sha512-WpjSXv9NQz0nU3yCT9TFEOfpFrXADY9C5fG6eAJqixLhvTX1jP3w92Y8IE5oafIe42nlF9otjhllnXN/QCaB3A==} + stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} @@ -1633,6 +1695,10 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1649,6 +1715,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2040,11 +2110,20 @@ snapshots: dependencies: '@fuman/utils': 0.0.4 + '@fuman/io@0.0.8': + dependencies: + '@fuman/utils': 0.0.4 + '@fuman/net@0.0.4': dependencies: '@fuman/io': 0.0.4 '@fuman/utils': 0.0.4 + '@fuman/net@0.0.9': + dependencies: + '@fuman/io': 0.0.8 + '@fuman/utils': 0.0.4 + '@fuman/node@0.0.4': dependencies: '@fuman/io': 0.0.4 @@ -2182,6 +2261,8 @@ snapshots: '@types/node': 22.10.0 xmlbuilder: 15.1.1 + '@types/spinnies@0.5.3': {} + '@types/unist@3.0.3': {} '@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.15.0)(typescript@5.7.2))(eslint@9.15.0)(typescript@5.7.2)': @@ -2320,8 +2401,14 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -2388,6 +2475,12 @@ snapshots: ccount@2.0.1: {} + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -2426,16 +2519,26 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} comment-parser@1.4.1: {} @@ -2948,6 +3051,8 @@ snapshots: graphemer@1.4.0: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} hasown@2.0.2: @@ -3393,6 +3498,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-fn@2.1.0: {} + mimic-response@3.1.0: {} min-indent@1.0.1: {} @@ -3449,6 +3556,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3637,6 +3748,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + reusify@1.0.4: {} run-parallel@1.2.0: @@ -3663,6 +3779,8 @@ snapshots: shebang-regex@3.0.0: {} + signal-exit@3.0.7: {} + simple-concat@1.0.1: {} simple-get@4.0.1: @@ -3696,6 +3814,12 @@ snapshots: spdx-license-ids@3.0.20: {} + spinnies@0.5.1: + dependencies: + chalk: 2.4.2 + cli-cursor: 3.1.0 + strip-ansi: 5.2.0 + stable-hash@0.0.4: {} string-width@4.2.3: @@ -3708,6 +3832,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3720,6 +3848,10 @@ snapshots: strip-json-comments@3.1.1: {} + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 diff --git a/scripts/media/fwmc-radio.ts b/scripts/media/fwmc-radio.ts new file mode 100644 index 0000000..a12dc4d --- /dev/null +++ b/scripts/media/fwmc-radio.ts @@ -0,0 +1,40 @@ +import { mkdir } from 'node:fs/promises' +import { asyncPool } from '@fuman/utils' +import json5 from 'json5' +import Spinnies from 'spinnies' +import { z } from 'zod' +import { downloadFile, ffetch } from '../../utils/fetch.ts' +import { fileExists } from '../../utils/fs.ts' +import { parseJsObject } from '../../utils/strings.ts' + +const $ = await ffetch('https://fwmc-ai.github.io/radio/').cheerio() + +const script = $('script:icontains(const playlist =)').html()! + +const playlistJs = parseJsObject(`[${script.split('const playlist = [').at(-1)!}`)! +const playlist = z.array( + z.object({ + id: z.string(), + title: z.string(), + file: z.string(), + cover: z.string(), + category: z.enum(['original', 'cover']), + lyrics: z.string(), + }), +).parse(json5.parse(playlistJs)) + +const spinnies = new Spinnies() + +await mkdir('assets/fwmc-radio', { recursive: true }) + +await asyncPool(playlist, async (item) => { + const dlPath = `assets/fwmc-radio/${item.id}.mp3` + if (await fileExists(dlPath)) return + + spinnies.add(item.id, { text: item.title }) + await downloadFile(new URL(item.file, 'https://fwmc-ai.github.io/radio/').toString(), dlPath) + spinnies.remove(item.id) +}) + +console.log('done') +spinnies.stopAll() diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts new file mode 100644 index 0000000..fc640dd --- /dev/null +++ b/scripts/media/soundcloud-dl.ts @@ -0,0 +1,262 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { ffetchAddons } from '@fuman/fetch' +import { asyncPool, base64 } from '@fuman/utils' +import { load } from 'cheerio' +import Spinnies from 'spinnies' +import { ProxyAgent } from 'undici' +import { z } from 'zod' +import { $ } from 'zx' +import { downloadFile, ffetch as ffetchBase } from '../../utils/fetch.ts' +import { sanitizeFilename } from '../../utils/fs.ts' +import { chunks, getEnv } from '../../utils/misc.ts' +import { generateOpusImageBlob } from '../../utils/opus.ts' + +const ffetchApi = ffetchBase.extend({ + baseUrl: 'https://api-v2.soundcloud.com', + // @ts-expect-error lol fixme + query: { + client_id: '4BowhSywvkJtklODQDzjNMq9sK9wyDJ4', + app_version: '1736857534', + app_locale: 'en', + }, + addons: [ + ffetchAddons.rateLimitHandler(), + ], + rateLimit: { + isRejected(res) { + return res.status === 429 + }, + defaultWaitTime: 10_000, + }, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'Authorization': `OAuth ${getEnv('SOUNDCLOUD_TOKEN')}`, + }, +}) +const ffetchHtml = ffetchBase.extend({ + baseUrl: 'https://soundcloud.com', + headers: { + Cookie: `oauth_token=${getEnv('SOUNDCLOUD_TOKEN')}`, + }, + extra: { + // @ts-expect-error lol fixme + dispatcher: new ProxyAgent('http://127.0.0.1:7891'), + }, +}) + +const ScTrack = z.object({ + id: z.number(), + kind: z.literal('track'), + title: z.string(), + duration: z.number(), + permalink_url: z.string(), + artwork_url: z.string().transform(s => s.replace('-large.jpg', '-t500x500.jpg')).nullable(), + media: z.object({ + transcodings: z.array(z.object({ + url: z.string(), + preset: z.string(), + format: z.object({ + protocol: z.string(), + mime_type: z.string(), + }), + quality: z.string(), + is_legacy_transcoding: z.boolean(), + })), + }), + track_authorization: z.string(), + user: z.object({ + username: z.string(), + permalink: z.string(), + }), +}) +type ScTrack = z.infer + +const ScPlaylist = z.object({ + id: z.number(), + title: z.string(), + duration: z.number(), + permalink_url: z.string(), + genre: z.string(), + description: z.string().nullable(), + track_count: z.number(), + user: z.object({ + username: z.string(), + }), + tracks: z.array(z.union([ + ScTrack, + z.object({ + id: z.number(), + kind: z.literal('track'), + }), + ])), +}) +type ScPlaylist = z.infer + +function extractHydrationData(html: string) { + const $ = load(html) + const script = $('script:contains(window.__sc_hydration = )') + return JSON.parse(script.html()!.replace('window.__sc_hydration = ', '').slice(0, -1)) +} + +async function fetchTrackByUrl(url: string) { + const html = await ffetchHtml(url).text() + const hydrationData = extractHydrationData(html) + const track = hydrationData.find(it => it.hydratable === 'sound') + if (!track) throw new Error('no track found') + + return ScTrack.parse(track.data) +} + +async function fetchPlaylistByUrl(url: string) { + const html = await ffetchHtml(url).text() + const hydrationData = extractHydrationData(html) + const playlist = hydrationData.find(it => it.hydratable === 'playlist') + if (!playlist) throw new Error('no playlist found') + + return ScPlaylist.parse(playlist.data) +} + +async function fetchTracksById(trackIds: number[]) { + return ffetchApi('/tracks', { + query: { + ids: trackIds.join(','), + }, + }).parsedJson(z.array(ScTrack)) +} + +async function downloadTrack(track: ScTrack, opts: { + /* download destination (filename without extension) */ + destination: string +}) { + const artworkPath = join('assets', `sc-tmp-${track.id}.jpg`) + const artworkBytes = track.artwork_url ? new Uint8Array(await ffetchHtml(track.artwork_url).arrayBuffer()) : null + + // find the best transcoding + const transcoding = track.media.transcodings.sort((a, b) => { + // prefer non-legacy transcodings + if (a.is_legacy_transcoding && !b.is_legacy_transcoding) return -1 + if (!a.is_legacy_transcoding && b.is_legacy_transcoding) return 1 + + // prefer hq + if (a.quality === 'sq' && b.quality === 'hq') return -1 + if (a.quality === 'hq' && b.quality === 'sq') return 1 + + // prefer opus + if (a.preset === 'opus_0_0' && b.preset !== 'opus_0_0') return -1 + if (a.preset !== 'opus_0_0' && b.preset === 'opus_0_0') return 1 + + return 0 + })[0] + + const { url: hlsUrl } = await ffetchApi(transcoding.url, { + query: { + track_authorization: track.track_authorization, + }, + }).parsedJson(z.object({ + url: z.string(), + })) + + const ext = transcoding.format.mime_type.match(/^audio\/(\w+)(;|$)/)![1] + const filename = `${opts.destination}.${ext}` + + const params: string[] = [ + '-y', + '-i', + hlsUrl, + '-c', + 'copy', + ] + + if (ext === 'mp3') { + if (artworkBytes) { + await writeFile(artworkPath, artworkBytes) + params.push( + '-i', + artworkPath, + '-map', + '0:a', + '-map', + '1:0', + ) + } + params.push( + '-id3v2_version', + '3', + '-metadata:s:v', + 'title="Album cover"', + '-metadata:s:v', + 'comment="Cover (front)"', + ) + } else if (ext === 'ogg' && artworkBytes) { + const blob = base64.encode(await generateOpusImageBlob(artworkBytes)) + params.push( + '-metadata', + `metadata_block_picture=${blob}`, + ) + } + + params.push( + '-metadata', + `title=${track.title}`, + '-metadata', + `artist=${track.user.username}`, + filename, + ) + + await $`ffmpeg ${params}`.quiet(true) +} + +async function downloadPlaylist(playlist: ScPlaylist) { + const tracks: ScTrack[] = [] + const tracksToFetch = new Set() + const trackIdToPosition = new Map() + + for (let i = 0; i < playlist.tracks.length; i++) { + const track = playlist.tracks[i] + trackIdToPosition.set(track.id, i + 1) + if ('user' in track) { + tracks.push(track) + } else { + tracksToFetch.add(track.id) + } + } + + const spinnies = new Spinnies() + + if (tracksToFetch.size) { + let remaining = tracksToFetch.size + spinnies.add('fetching', { text: `fetching ${remaining} tracks` }) + await asyncPool(chunks(Array.from(tracksToFetch), 20), async (ids) => { + const res = await fetchTracksById(Array.from(ids)) + for (const track of res) { + tracks.push(track) + } + remaining -= ids.length + spinnies.update('fetching', { text: `fetching ${remaining} tracks` }) + }) + spinnies.succeed('fetching') + } + + const destDir = join('assets/soundcloud-dl', sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)) + await mkdir(destDir, { recursive: true }) + + const posPadSize = Math.ceil(Math.log10(tracks.length)) + + await asyncPool(tracks, async (track) => { + const position = trackIdToPosition.get(track.id)! + const filename = `${position.toString().padStart(posPadSize, '0')}. ${track.user.username} - ${track.title}` + + spinnies.add(`${track.id}`, { text: filename }) + await downloadTrack(track, { + destination: join(destDir, filename), + }) + + spinnies.remove(`${track.id}`) + }, { limit: 8 }) + + console.log('done') + spinnies.stopAll() +} + +await downloadPlaylist(await fetchPlaylistByUrl('https://soundcloud.com/user-398958278/sets/l2grace')) diff --git a/utils/fs.ts b/utils/fs.ts index 90834ab..11ab17f 100644 --- a/utils/fs.ts +++ b/utils/fs.ts @@ -17,3 +17,7 @@ export async function directoryExists(path: string): Promise { return false } } + +export function sanitizeFilename(filename: string) { + return filename.replace(/[/\\?%*:|"<>]/g, '_') +} diff --git a/utils/misc.ts b/utils/misc.ts index e38398d..c3ae40c 100644 --- a/utils/misc.ts +++ b/utils/misc.ts @@ -8,3 +8,9 @@ export function getEnv(key: string, parser?: (value: string) => T): T | strin if (!parser) return value return parser(value) } + +export function* chunks(arr: T[], size: number) { + for (let i = 0; i < arr.length; i += size) { + yield arr.slice(i, i + size) + } +} diff --git a/utils/opus.ts b/utils/opus.ts new file mode 100644 index 0000000..06ff13b --- /dev/null +++ b/utils/opus.ts @@ -0,0 +1,30 @@ +import { Bytes, write } from '@fuman/io' +import { $ } from 'zx' + +export async function generateOpusImageBlob(image: Uint8Array) { + // todo we should probably not use ffprobe here but whatever lol + const proc = $`ffprobe -of json -v error -show_entries stream=codec_name,width,height pipe:0` + proc.stdin.write(image) + proc.stdin.end() + const json = await proc.json() + + const img = json.streams[0] + // https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8 + const mime = img.codec_name === 'mjpeg' ? 'image/jpeg' : 'image/png' + const description = 'Cover Artwork' + + const res = Bytes.alloc(image.length + 128) + write.uint32be(res, 3) // picture type = album cover + write.uint32be(res, mime.length) + write.rawString(res, mime) + write.uint32be(res, description.length) + write.rawString(res, description) + write.uint32be(res, img.width) + write.uint32be(res, img.height) + write.uint32be(res, 0) // color depth + write.uint32be(res, 0) // color index (unused, for gifs) + write.uint32be(res, image.length) + write.bytes(res, image) + + return res.result() +} diff --git a/utils/strings.ts b/utils/strings.ts new file mode 100644 index 0000000..fec4799 --- /dev/null +++ b/utils/strings.ts @@ -0,0 +1,54 @@ +export function parseJsObject(str: string, offset = 0) { + let i = offset + const len = str.length + let start = -1 + let end = -1 + + const depth = { + '{': 0, + '[': 0, + } + + const possibleQuotes = { + '"': true, + '\'': true, + '`': true, + } + let inQuote: string | null = null + let escapeNextQuote = false + + while (i < len) { + const char = str[i] + if (char in possibleQuotes && !escapeNextQuote) { + if (inQuote === null) { + inQuote = char + } else if (char === inQuote) { + inQuote = null + } + } else if (inQuote != null) { + escapeNextQuote = char === '\\' && !escapeNextQuote + } else if (inQuote == null && char in depth) { + if (start === -1) { + start = i + } + depth[char] += 1 + } else if (inQuote == null && ( + char === '}' || char === ']' + )) { + if (char === '}') depth['{'] -= 1 + if (char === ']') depth['['] -= 1 + + if (depth['{'] === 0 && depth['['] === 0) { + end = i + 1 + break + } + } + i += 1 + } + + if (start === -1 && end === -1) return null + if (depth['{'] !== 0 || depth['['] !== 0) throw new SyntaxError('Mismatched brackets') + if (inQuote) throw new SyntaxError('Unclosed string') + + return str.substring(start, end) +} From f2d62f5954a14fc8f2f13c6feac2c005ffe6c063 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 18 Jan 2025 04:01:14 +0000 Subject: [PATCH 09/61] chore: update public repo --- package.json | 1 + pnpm-lock.yaml | 271 +++++++++++++++++++++++++++++++++ scripts/media/soundcloud-dl.ts | 103 ++++++++----- 3 files changed, 335 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 8e7bf7c..12de66b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "spinnies": "^0.5.1", "tough-cookie": "^5.0.0", "tough-cookie-file-store": "^2.0.3", + "tsx": "^4.19.2", "undici": "^7.2.0", "wanakana": "^5.3.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de66943..9a21c8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: tough-cookie-file-store: specifier: ^2.0.3 version: 2.0.3 + tsx: + specifier: ^4.19.2 + version: 4.19.2 typescript: specifier: ^5.0.0 version: 5.7.2 @@ -193,6 +196,150 @@ packages: resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==} engines: {node: '>=16'} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-plugin-eslint-comments@4.4.1': resolution: {integrity: sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -773,6 +920,11 @@ packages: es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1033,6 +1185,11 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1785,6 +1942,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -2038,6 +2200,78 @@ snapshots: esquery: 1.6.0 jsdoc-type-pratt-parser: 4.1.0 + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.15.0)': dependencies: escape-string-regexp: 4.0.0 @@ -2677,6 +2911,33 @@ snapshots: es-module-lexer@1.5.4: {} + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -3021,6 +3282,9 @@ snapshots: fs-constants@1.0.0: {} + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} get-caller-file@2.0.5: {} @@ -3921,6 +4185,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index fc640dd..26168e0 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -1,12 +1,12 @@ -import { mkdir, writeFile } from 'node:fs/promises' +import { mkdir, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { ffetchAddons } from '@fuman/fetch' -import { asyncPool, base64 } from '@fuman/utils' +import { assert, asyncPool, base64 } from '@fuman/utils' import { load } from 'cheerio' import Spinnies from 'spinnies' import { ProxyAgent } from 'undici' import { z } from 'zod' -import { $ } from 'zx' +import { $, question } from 'zx' import { downloadFile, ffetch as ffetchBase } from '../../utils/fetch.ts' import { sanitizeFilename } from '../../utils/fs.ts' import { chunks, getEnv } from '../../utils/misc.ts' @@ -133,21 +133,18 @@ async function downloadTrack(track: ScTrack, opts: { const artworkBytes = track.artwork_url ? new Uint8Array(await ffetchHtml(track.artwork_url).arrayBuffer()) : null // find the best transcoding - const transcoding = track.media.transcodings.sort((a, b) => { - // prefer non-legacy transcodings - if (a.is_legacy_transcoding && !b.is_legacy_transcoding) return -1 - if (!a.is_legacy_transcoding && b.is_legacy_transcoding) return 1 - - // prefer hq - if (a.quality === 'sq' && b.quality === 'hq') return -1 - if (a.quality === 'hq' && b.quality === 'sq') return 1 - - // prefer opus - if (a.preset === 'opus_0_0' && b.preset !== 'opus_0_0') return -1 - if (a.preset !== 'opus_0_0' && b.preset === 'opus_0_0') return 1 - - return 0 - })[0] + let transcoding!: typeof track.media.transcodings[0] + for (const t of track.media.transcodings) { + if (t.quality === 'hq') { + transcoding = t + break + } + if (t.preset === 'opus_0_0') { + transcoding = t + break + } + transcoding = t + } const { url: hlsUrl } = await ffetchApi(transcoding.url, { query: { @@ -157,46 +154,55 @@ async function downloadTrack(track: ScTrack, opts: { url: z.string(), })) - const ext = transcoding.format.mime_type.match(/^audio\/(\w+)(;|$)/)![1] + let ext = transcoding.format.mime_type.match(/^audio\/(\w+)(;|$)/)![1] + if (ext === 'mp4') ext = 'm4a' const filename = `${opts.destination}.${ext}` const params: string[] = [ '-y', '-i', hlsUrl, - '-c', - 'copy', ] - if (ext === 'mp3') { - if (artworkBytes) { + if (artworkBytes) { + if (ext === 'mp3') { await writeFile(artworkPath, artworkBytes) params.push( '-i', artworkPath, '-map', - '0:a', + '1:v:0', + '-id3v2_version', + '3', + '-metadata:s:v', + 'title=Album cover', + '-metadata:s:v', + 'comment=Cover (front)', + ) + } else if (ext === 'ogg') { + const blob = base64.encode(await generateOpusImageBlob(artworkBytes)) + params.push( + '-metadata', + `metadata_block_picture=${blob}`, + ) + } else if (ext === 'm4a') { + await writeFile(artworkPath, artworkBytes) + params.push( + '-i', + artworkPath, '-map', - '1:0', + '1', + '-disposition:v', + 'attached_pic', ) } - params.push( - '-id3v2_version', - '3', - '-metadata:s:v', - 'title="Album cover"', - '-metadata:s:v', - 'comment="Cover (front)"', - ) - } else if (ext === 'ogg' && artworkBytes) { - const blob = base64.encode(await generateOpusImageBlob(artworkBytes)) - params.push( - '-metadata', - `metadata_block_picture=${blob}`, - ) } params.push( + '-map', + '0:a', + '-c', + 'copy', '-metadata', `title=${track.title}`, '-metadata', @@ -205,6 +211,8 @@ async function downloadTrack(track: ScTrack, opts: { ) await $`ffmpeg ${params}`.quiet(true) + + await rm(artworkPath, { force: true }) } async function downloadPlaylist(playlist: ScPlaylist) { @@ -259,4 +267,19 @@ async function downloadPlaylist(playlist: ScPlaylist) { spinnies.stopAll() } -await downloadPlaylist(await fetchPlaylistByUrl('https://soundcloud.com/user-398958278/sets/l2grace')) +const url = process.argv[2] ?? await question('url > ') +if (!url.startsWith('https://soundcloud.com/')) { + console.error('url must start with https://soundcloud.com/') + process.exit(1) +} + +if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/sets\//i)) { + await downloadPlaylist(await fetchPlaylistByUrl(url)) +} else { + const track = await fetchTrackByUrl(url) + const filename = `${track.user.username}-${track.title}` + console.log('downloading track:', filename) + await downloadTrack(track, { + destination: join('assets/soundcloud-dl', sanitizeFilename(filename)), + }) +} From 090a502eceb2b448f4fd8921ae0bc2c67d004490 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 18 Jan 2025 04:01:14 +0000 Subject: [PATCH 10/61] chore: update public repo --- package.json | 1 + pnpm-lock.yaml | 271 +++++++++++++++++++++++++++++++++ scripts/media/soundcloud-dl.ts | 103 ++++++++----- 3 files changed, 335 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 8e7bf7c..12de66b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "spinnies": "^0.5.1", "tough-cookie": "^5.0.0", "tough-cookie-file-store": "^2.0.3", + "tsx": "^4.19.2", "undici": "^7.2.0", "wanakana": "^5.3.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de66943..9a21c8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: tough-cookie-file-store: specifier: ^2.0.3 version: 2.0.3 + tsx: + specifier: ^4.19.2 + version: 4.19.2 typescript: specifier: ^5.0.0 version: 5.7.2 @@ -193,6 +196,150 @@ packages: resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==} engines: {node: '>=16'} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-plugin-eslint-comments@4.4.1': resolution: {integrity: sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -773,6 +920,11 @@ packages: es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1033,6 +1185,11 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1785,6 +1942,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -2038,6 +2200,78 @@ snapshots: esquery: 1.6.0 jsdoc-type-pratt-parser: 4.1.0 + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.15.0)': dependencies: escape-string-regexp: 4.0.0 @@ -2677,6 +2911,33 @@ snapshots: es-module-lexer@1.5.4: {} + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -3021,6 +3282,9 @@ snapshots: fs-constants@1.0.0: {} + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} get-caller-file@2.0.5: {} @@ -3921,6 +4185,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index fc640dd..26168e0 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -1,12 +1,12 @@ -import { mkdir, writeFile } from 'node:fs/promises' +import { mkdir, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { ffetchAddons } from '@fuman/fetch' -import { asyncPool, base64 } from '@fuman/utils' +import { assert, asyncPool, base64 } from '@fuman/utils' import { load } from 'cheerio' import Spinnies from 'spinnies' import { ProxyAgent } from 'undici' import { z } from 'zod' -import { $ } from 'zx' +import { $, question } from 'zx' import { downloadFile, ffetch as ffetchBase } from '../../utils/fetch.ts' import { sanitizeFilename } from '../../utils/fs.ts' import { chunks, getEnv } from '../../utils/misc.ts' @@ -133,21 +133,18 @@ async function downloadTrack(track: ScTrack, opts: { const artworkBytes = track.artwork_url ? new Uint8Array(await ffetchHtml(track.artwork_url).arrayBuffer()) : null // find the best transcoding - const transcoding = track.media.transcodings.sort((a, b) => { - // prefer non-legacy transcodings - if (a.is_legacy_transcoding && !b.is_legacy_transcoding) return -1 - if (!a.is_legacy_transcoding && b.is_legacy_transcoding) return 1 - - // prefer hq - if (a.quality === 'sq' && b.quality === 'hq') return -1 - if (a.quality === 'hq' && b.quality === 'sq') return 1 - - // prefer opus - if (a.preset === 'opus_0_0' && b.preset !== 'opus_0_0') return -1 - if (a.preset !== 'opus_0_0' && b.preset === 'opus_0_0') return 1 - - return 0 - })[0] + let transcoding!: typeof track.media.transcodings[0] + for (const t of track.media.transcodings) { + if (t.quality === 'hq') { + transcoding = t + break + } + if (t.preset === 'opus_0_0') { + transcoding = t + break + } + transcoding = t + } const { url: hlsUrl } = await ffetchApi(transcoding.url, { query: { @@ -157,46 +154,55 @@ async function downloadTrack(track: ScTrack, opts: { url: z.string(), })) - const ext = transcoding.format.mime_type.match(/^audio\/(\w+)(;|$)/)![1] + let ext = transcoding.format.mime_type.match(/^audio\/(\w+)(;|$)/)![1] + if (ext === 'mp4') ext = 'm4a' const filename = `${opts.destination}.${ext}` const params: string[] = [ '-y', '-i', hlsUrl, - '-c', - 'copy', ] - if (ext === 'mp3') { - if (artworkBytes) { + if (artworkBytes) { + if (ext === 'mp3') { await writeFile(artworkPath, artworkBytes) params.push( '-i', artworkPath, '-map', - '0:a', + '1:v:0', + '-id3v2_version', + '3', + '-metadata:s:v', + 'title=Album cover', + '-metadata:s:v', + 'comment=Cover (front)', + ) + } else if (ext === 'ogg') { + const blob = base64.encode(await generateOpusImageBlob(artworkBytes)) + params.push( + '-metadata', + `metadata_block_picture=${blob}`, + ) + } else if (ext === 'm4a') { + await writeFile(artworkPath, artworkBytes) + params.push( + '-i', + artworkPath, '-map', - '1:0', + '1', + '-disposition:v', + 'attached_pic', ) } - params.push( - '-id3v2_version', - '3', - '-metadata:s:v', - 'title="Album cover"', - '-metadata:s:v', - 'comment="Cover (front)"', - ) - } else if (ext === 'ogg' && artworkBytes) { - const blob = base64.encode(await generateOpusImageBlob(artworkBytes)) - params.push( - '-metadata', - `metadata_block_picture=${blob}`, - ) } params.push( + '-map', + '0:a', + '-c', + 'copy', '-metadata', `title=${track.title}`, '-metadata', @@ -205,6 +211,8 @@ async function downloadTrack(track: ScTrack, opts: { ) await $`ffmpeg ${params}`.quiet(true) + + await rm(artworkPath, { force: true }) } async function downloadPlaylist(playlist: ScPlaylist) { @@ -259,4 +267,19 @@ async function downloadPlaylist(playlist: ScPlaylist) { spinnies.stopAll() } -await downloadPlaylist(await fetchPlaylistByUrl('https://soundcloud.com/user-398958278/sets/l2grace')) +const url = process.argv[2] ?? await question('url > ') +if (!url.startsWith('https://soundcloud.com/')) { + console.error('url must start with https://soundcloud.com/') + process.exit(1) +} + +if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/sets\//i)) { + await downloadPlaylist(await fetchPlaylistByUrl(url)) +} else { + const track = await fetchTrackByUrl(url) + const filename = `${track.user.username}-${track.title}` + console.log('downloading track:', filename) + await downloadTrack(track, { + destination: join('assets/soundcloud-dl', sanitizeFilename(filename)), + }) +} From c974b06523495639732ae6036befcee7a23abd6d Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 18 Jan 2025 07:55:22 +0000 Subject: [PATCH 11/61] chore: update public repo --- scripts/media/soundcloud-dl.ts | 157 +++++++++++++++++++++++++++++---- utils/fetch.ts | 4 + 2 files changed, 142 insertions(+), 19 deletions(-) diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index 26168e0..0065ce6 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -1,12 +1,12 @@ import { mkdir, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { ffetchAddons } from '@fuman/fetch' -import { assert, asyncPool, base64 } from '@fuman/utils' +import { assert, asyncPool, base64, sleep } from '@fuman/utils' import { load } from 'cheerio' import Spinnies from 'spinnies' import { ProxyAgent } from 'undici' import { z } from 'zod' -import { $, question } from 'zx' +import { $, ProcessOutput, question } from 'zx' import { downloadFile, ffetch as ffetchBase } from '../../utils/fetch.ts' import { sanitizeFilename } from '../../utils/fs.ts' import { chunks, getEnv } from '../../utils/misc.ts' @@ -23,12 +23,6 @@ const ffetchApi = ffetchBase.extend({ addons: [ ffetchAddons.rateLimitHandler(), ], - rateLimit: { - isRejected(res) { - return res.status === 429 - }, - defaultWaitTime: 10_000, - }, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', 'Authorization': `OAuth ${getEnv('SOUNDCLOUD_TOKEN')}`, @@ -39,10 +33,6 @@ const ffetchHtml = ffetchBase.extend({ headers: { Cookie: `oauth_token=${getEnv('SOUNDCLOUD_TOKEN')}`, }, - extra: { - // @ts-expect-error lol fixme - dispatcher: new ProxyAgent('http://127.0.0.1:7891'), - }, }) const ScTrack = z.object({ @@ -50,6 +40,7 @@ const ScTrack = z.object({ kind: z.literal('track'), title: z.string(), duration: z.number(), + description: z.string().nullable(), permalink_url: z.string(), artwork_url: z.string().transform(s => s.replace('-large.jpg', '-t500x500.jpg')).nullable(), media: z.object({ @@ -77,8 +68,8 @@ const ScPlaylist = z.object({ title: z.string(), duration: z.number(), permalink_url: z.string(), - genre: z.string(), - description: z.string().nullable(), + genre: z.string().nullish(), + description: z.string().nullish(), track_count: z.number(), user: z.object({ username: z.string(), @@ -89,10 +80,17 @@ const ScPlaylist = z.object({ id: z.number(), kind: z.literal('track'), }), - ])), + ])).default(() => []), }) type ScPlaylist = z.infer +const ScLike = z.object({ + created_at: z.string(), + kind: z.literal('like'), + track: ScTrack.optional(), + playlist: ScPlaylist.optional(), +}) + function extractHydrationData(html: string) { const $ = load(html) const script = $('script:contains(window.__sc_hydration = )') @@ -128,6 +126,8 @@ async function fetchTracksById(trackIds: number[]) { async function downloadTrack(track: ScTrack, opts: { /* download destination (filename without extension) */ destination: string + onRateLimit?: (waitTime: number) => void + onCdnRateLimit?: () => void }) { const artworkPath = join('assets', `sc-tmp-${track.id}.jpg`) const artworkBytes = track.artwork_url ? new Uint8Array(await ffetchHtml(track.artwork_url).arrayBuffer()) : null @@ -150,6 +150,16 @@ async function downloadTrack(track: ScTrack, opts: { query: { track_authorization: track.track_authorization, }, + rateLimit: { + isRejected(res) { + return res.status === 429 + }, + defaultWaitTime: 60_000, + maxRetries: 10, + onRateLimitExceeded(res, waitTime) { + opts.onRateLimit?.(waitTime) + }, + }, }).parsedJson(z.object({ url: z.string(), })) @@ -207,15 +217,35 @@ async function downloadTrack(track: ScTrack, opts: { `title=${track.title}`, '-metadata', `artist=${track.user.username}`, + '-metadata', + `comment=${track.description ?? ''}`, filename, ) - await $`ffmpeg ${params}`.quiet(true) + while (true) { + try { + await $`ffmpeg ${params}`.quiet(true) + break + } catch (e) { + if (!(e instanceof ProcessOutput)) { + throw e + } + if (e.stderr.includes('429 Too Many Requests')) { + opts.onCdnRateLimit?.() + await sleep(10_000) + continue + } + + throw e + } + } await rm(artworkPath, { force: true }) } -async function downloadPlaylist(playlist: ScPlaylist) { +async function downloadPlaylist(playlist: ScPlaylist, params: { + destination?: string +} = {}) { const tracks: ScTrack[] = [] const tracksToFetch = new Set() const trackIdToPosition = new Map() @@ -246,7 +276,7 @@ async function downloadPlaylist(playlist: ScPlaylist) { spinnies.succeed('fetching') } - const destDir = join('assets/soundcloud-dl', sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)) + const destDir = params.destination ?? join('assets/soundcloud-dl', sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)) await mkdir(destDir, { recursive: true }) const posPadSize = Math.ceil(Math.log10(tracks.length)) @@ -258,6 +288,12 @@ async function downloadPlaylist(playlist: ScPlaylist) { spinnies.add(`${track.id}`, { text: filename }) await downloadTrack(track, { destination: join(destDir, filename), + onRateLimit: (wait) => { + spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + }, + onCdnRateLimit: () => { + spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + }, }) spinnies.remove(`${track.id}`) @@ -267,6 +303,87 @@ async function downloadPlaylist(playlist: ScPlaylist) { spinnies.stopAll() } +async function downloadLikes(username: string) { + const spinnies = new Spinnies() + spinnies.add('collect', { text: 'collecting likes...' }) + + const userPage = await ffetchHtml(`/${username}`).text() + const hydrationData = extractHydrationData(userPage) + const user = hydrationData.find(it => it.hydratable === 'user') + if (!user) throw new Error('no user found') + const userData = z.object({ + likes_count: z.number(), + playlist_likes_count: z.number(), + id: z.number(), + }).parse(user.data) + + const tracks: ScTrack[] = [] + const playlists: ScPlaylist[] = [] + const updateSpinner = () => { + const percent = Math.floor((tracks.length + playlists.length) / (userData.likes_count + userData.playlist_likes_count) * 100) + spinnies.update('collect', { + text: `[${percent}%] collecting liked tracks: ${tracks.length}/${userData.likes_count}, playlists: ${playlists.length}/${userData.playlist_likes_count}`, + }) + } + updateSpinner() + + let offset = '0' + while (true) { + const res = await ffetchApi(`/users/${userData.id}/likes`, { + query: { + limit: 100, + offset, + linked_partitioning: '1', + }, + }).parsedJson(z.object({ + collection: z.array(ScLike), + next_href: z.string().nullable(), + })) + + for (const like of res.collection) { + if (like.track) { + tracks.push(like.track) + } else if (like.playlist) { + playlists.push(like.playlist) + } else { + console.warn('unknown like type:', like.created_at) + } + } + + updateSpinner() + + if (!res.next_href) break + offset = new URL(res.next_href).searchParams.get('offset')! + } + + spinnies.succeed('collect', { text: `collected ${tracks.length} tracks and ${playlists.length} playlists` }) + + const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(username)}-likes`) + await mkdir(baseDir, { recursive: true }) + + await asyncPool(tracks, async (track) => { + const filename = `${track.user.username} - ${track.title}` + spinnies.add(`${track.id}`, { text: filename }) + await downloadTrack(track, { + destination: join(baseDir, sanitizeFilename(filename)), + onRateLimit: (wait) => { + spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + }, + onCdnRateLimit: () => { + spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + }, + }) + spinnies.remove(`${track.id}`) + }) + + for (const playlist of playlists) { + console.log('\uDB83\uDCB8 %s', playlist.title) + await downloadPlaylist(playlist, { + destination: join(baseDir, sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)), + }) + } +} + const url = process.argv[2] ?? await question('url > ') if (!url.startsWith('https://soundcloud.com/')) { console.error('url must start with https://soundcloud.com/') @@ -275,9 +392,11 @@ if (!url.startsWith('https://soundcloud.com/')) { if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/sets\//i)) { await downloadPlaylist(await fetchPlaylistByUrl(url)) +} else if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/likes/i)) { + await downloadLikes(url.match(/^https:\/\/soundcloud.com\/([a-z0-9-]+)\/likes/i)![1]) } else { const track = await fetchTrackByUrl(url) - const filename = `${track.user.username}-${track.title}` + const filename = `${track.user.username} - ${track.title}` console.log('downloading track:', filename) await downloadTrack(track, { destination: join('assets/soundcloud-dl', sanitizeFilename(filename)), diff --git a/utils/fetch.ts b/utils/fetch.ts index 6a8de33..bd33dee 100644 --- a/utils/fetch.ts +++ b/utils/fetch.ts @@ -6,6 +6,7 @@ import { ffetchZodAdapter } from '@fuman/fetch/zod' import { webReadableToFuman, write } from '@fuman/io' import { nodeWritableToFuman } from '@fuman/node' import { type CheerioAPI, load } from 'cheerio' +import { ProxyAgent } from 'undici' const cheerioAddon: FfetchAddon Promise }> = { response: { @@ -23,6 +24,9 @@ export const ffetch = ffetchBase.extend({ cheerioAddon, toughCookieAddon(), ], + extra: { + dispatcher: process.env.http_proxy ? new ProxyAgent(process.env.http_proxy) : undefined, + } as any, }) export async function downloadStream(stream: ReadableStream, path: string) { From 67a623863299d0df09a880cd5fdaed3f54904a8c Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 18 Jan 2025 07:55:22 +0000 Subject: [PATCH 12/61] chore: update public repo --- scripts/media/soundcloud-dl.ts | 157 +++++++++++++++++++++++++++++---- utils/fetch.ts | 4 + 2 files changed, 142 insertions(+), 19 deletions(-) diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index 26168e0..0065ce6 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -1,12 +1,12 @@ import { mkdir, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { ffetchAddons } from '@fuman/fetch' -import { assert, asyncPool, base64 } from '@fuman/utils' +import { assert, asyncPool, base64, sleep } from '@fuman/utils' import { load } from 'cheerio' import Spinnies from 'spinnies' import { ProxyAgent } from 'undici' import { z } from 'zod' -import { $, question } from 'zx' +import { $, ProcessOutput, question } from 'zx' import { downloadFile, ffetch as ffetchBase } from '../../utils/fetch.ts' import { sanitizeFilename } from '../../utils/fs.ts' import { chunks, getEnv } from '../../utils/misc.ts' @@ -23,12 +23,6 @@ const ffetchApi = ffetchBase.extend({ addons: [ ffetchAddons.rateLimitHandler(), ], - rateLimit: { - isRejected(res) { - return res.status === 429 - }, - defaultWaitTime: 10_000, - }, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', 'Authorization': `OAuth ${getEnv('SOUNDCLOUD_TOKEN')}`, @@ -39,10 +33,6 @@ const ffetchHtml = ffetchBase.extend({ headers: { Cookie: `oauth_token=${getEnv('SOUNDCLOUD_TOKEN')}`, }, - extra: { - // @ts-expect-error lol fixme - dispatcher: new ProxyAgent('http://127.0.0.1:7891'), - }, }) const ScTrack = z.object({ @@ -50,6 +40,7 @@ const ScTrack = z.object({ kind: z.literal('track'), title: z.string(), duration: z.number(), + description: z.string().nullable(), permalink_url: z.string(), artwork_url: z.string().transform(s => s.replace('-large.jpg', '-t500x500.jpg')).nullable(), media: z.object({ @@ -77,8 +68,8 @@ const ScPlaylist = z.object({ title: z.string(), duration: z.number(), permalink_url: z.string(), - genre: z.string(), - description: z.string().nullable(), + genre: z.string().nullish(), + description: z.string().nullish(), track_count: z.number(), user: z.object({ username: z.string(), @@ -89,10 +80,17 @@ const ScPlaylist = z.object({ id: z.number(), kind: z.literal('track'), }), - ])), + ])).default(() => []), }) type ScPlaylist = z.infer +const ScLike = z.object({ + created_at: z.string(), + kind: z.literal('like'), + track: ScTrack.optional(), + playlist: ScPlaylist.optional(), +}) + function extractHydrationData(html: string) { const $ = load(html) const script = $('script:contains(window.__sc_hydration = )') @@ -128,6 +126,8 @@ async function fetchTracksById(trackIds: number[]) { async function downloadTrack(track: ScTrack, opts: { /* download destination (filename without extension) */ destination: string + onRateLimit?: (waitTime: number) => void + onCdnRateLimit?: () => void }) { const artworkPath = join('assets', `sc-tmp-${track.id}.jpg`) const artworkBytes = track.artwork_url ? new Uint8Array(await ffetchHtml(track.artwork_url).arrayBuffer()) : null @@ -150,6 +150,16 @@ async function downloadTrack(track: ScTrack, opts: { query: { track_authorization: track.track_authorization, }, + rateLimit: { + isRejected(res) { + return res.status === 429 + }, + defaultWaitTime: 60_000, + maxRetries: 10, + onRateLimitExceeded(res, waitTime) { + opts.onRateLimit?.(waitTime) + }, + }, }).parsedJson(z.object({ url: z.string(), })) @@ -207,15 +217,35 @@ async function downloadTrack(track: ScTrack, opts: { `title=${track.title}`, '-metadata', `artist=${track.user.username}`, + '-metadata', + `comment=${track.description ?? ''}`, filename, ) - await $`ffmpeg ${params}`.quiet(true) + while (true) { + try { + await $`ffmpeg ${params}`.quiet(true) + break + } catch (e) { + if (!(e instanceof ProcessOutput)) { + throw e + } + if (e.stderr.includes('429 Too Many Requests')) { + opts.onCdnRateLimit?.() + await sleep(10_000) + continue + } + + throw e + } + } await rm(artworkPath, { force: true }) } -async function downloadPlaylist(playlist: ScPlaylist) { +async function downloadPlaylist(playlist: ScPlaylist, params: { + destination?: string +} = {}) { const tracks: ScTrack[] = [] const tracksToFetch = new Set() const trackIdToPosition = new Map() @@ -246,7 +276,7 @@ async function downloadPlaylist(playlist: ScPlaylist) { spinnies.succeed('fetching') } - const destDir = join('assets/soundcloud-dl', sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)) + const destDir = params.destination ?? join('assets/soundcloud-dl', sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)) await mkdir(destDir, { recursive: true }) const posPadSize = Math.ceil(Math.log10(tracks.length)) @@ -258,6 +288,12 @@ async function downloadPlaylist(playlist: ScPlaylist) { spinnies.add(`${track.id}`, { text: filename }) await downloadTrack(track, { destination: join(destDir, filename), + onRateLimit: (wait) => { + spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + }, + onCdnRateLimit: () => { + spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + }, }) spinnies.remove(`${track.id}`) @@ -267,6 +303,87 @@ async function downloadPlaylist(playlist: ScPlaylist) { spinnies.stopAll() } +async function downloadLikes(username: string) { + const spinnies = new Spinnies() + spinnies.add('collect', { text: 'collecting likes...' }) + + const userPage = await ffetchHtml(`/${username}`).text() + const hydrationData = extractHydrationData(userPage) + const user = hydrationData.find(it => it.hydratable === 'user') + if (!user) throw new Error('no user found') + const userData = z.object({ + likes_count: z.number(), + playlist_likes_count: z.number(), + id: z.number(), + }).parse(user.data) + + const tracks: ScTrack[] = [] + const playlists: ScPlaylist[] = [] + const updateSpinner = () => { + const percent = Math.floor((tracks.length + playlists.length) / (userData.likes_count + userData.playlist_likes_count) * 100) + spinnies.update('collect', { + text: `[${percent}%] collecting liked tracks: ${tracks.length}/${userData.likes_count}, playlists: ${playlists.length}/${userData.playlist_likes_count}`, + }) + } + updateSpinner() + + let offset = '0' + while (true) { + const res = await ffetchApi(`/users/${userData.id}/likes`, { + query: { + limit: 100, + offset, + linked_partitioning: '1', + }, + }).parsedJson(z.object({ + collection: z.array(ScLike), + next_href: z.string().nullable(), + })) + + for (const like of res.collection) { + if (like.track) { + tracks.push(like.track) + } else if (like.playlist) { + playlists.push(like.playlist) + } else { + console.warn('unknown like type:', like.created_at) + } + } + + updateSpinner() + + if (!res.next_href) break + offset = new URL(res.next_href).searchParams.get('offset')! + } + + spinnies.succeed('collect', { text: `collected ${tracks.length} tracks and ${playlists.length} playlists` }) + + const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(username)}-likes`) + await mkdir(baseDir, { recursive: true }) + + await asyncPool(tracks, async (track) => { + const filename = `${track.user.username} - ${track.title}` + spinnies.add(`${track.id}`, { text: filename }) + await downloadTrack(track, { + destination: join(baseDir, sanitizeFilename(filename)), + onRateLimit: (wait) => { + spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + }, + onCdnRateLimit: () => { + spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + }, + }) + spinnies.remove(`${track.id}`) + }) + + for (const playlist of playlists) { + console.log('\uDB83\uDCB8 %s', playlist.title) + await downloadPlaylist(playlist, { + destination: join(baseDir, sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)), + }) + } +} + const url = process.argv[2] ?? await question('url > ') if (!url.startsWith('https://soundcloud.com/')) { console.error('url must start with https://soundcloud.com/') @@ -275,9 +392,11 @@ if (!url.startsWith('https://soundcloud.com/')) { if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/sets\//i)) { await downloadPlaylist(await fetchPlaylistByUrl(url)) +} else if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/likes/i)) { + await downloadLikes(url.match(/^https:\/\/soundcloud.com\/([a-z0-9-]+)\/likes/i)![1]) } else { const track = await fetchTrackByUrl(url) - const filename = `${track.user.username}-${track.title}` + const filename = `${track.user.username} - ${track.title}` console.log('downloading track:', filename) await downloadTrack(track, { destination: join('assets/soundcloud-dl', sanitizeFilename(filename)), diff --git a/utils/fetch.ts b/utils/fetch.ts index 6a8de33..bd33dee 100644 --- a/utils/fetch.ts +++ b/utils/fetch.ts @@ -6,6 +6,7 @@ import { ffetchZodAdapter } from '@fuman/fetch/zod' import { webReadableToFuman, write } from '@fuman/io' import { nodeWritableToFuman } from '@fuman/node' import { type CheerioAPI, load } from 'cheerio' +import { ProxyAgent } from 'undici' const cheerioAddon: FfetchAddon Promise }> = { response: { @@ -23,6 +24,9 @@ export const ffetch = ffetchBase.extend({ cheerioAddon, toughCookieAddon(), ], + extra: { + dispatcher: process.env.http_proxy ? new ProxyAgent(process.env.http_proxy) : undefined, + } as any, }) export async function downloadStream(stream: ReadableStream, path: string) { From 26bd4280af4d5125724190ca98ebbfb707f6032a Mon Sep 17 00:00:00 2001 From: desu-bot Date: Fri, 24 Jan 2025 09:56:11 +0000 Subject: [PATCH 13/61] chore: update public repo --- package.json | 4 +-- pnpm-lock.yaml | 21 +++++++----- scripts/media/soundcloud-dl.ts | 60 ++++++++++++++++++++++------------ 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 12de66b..bccf840 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ }, "devDependencies": { "@antfu/eslint-config": "3.10.0", - "@fuman/fetch": "0.0.7", - "@fuman/utils": "0.0.4", + "@fuman/fetch": "0.0.10", + "@fuman/utils": "0.0.10", "@types/node": "22.10.0", "domhandler": "^5.0.3", "dotenv": "16.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a21c8b..0185865 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,11 +79,11 @@ importers: specifier: 3.10.0 version: 3.10.0(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0)(typescript@5.7.2) '@fuman/fetch': - specifier: 0.0.7 - version: 0.0.7(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) + specifier: 0.0.10 + version: 0.0.10(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) '@fuman/utils': - specifier: 0.0.4 - version: 0.0.4 + specifier: 0.0.10 + version: 0.0.10 '@types/node': specifier: 22.10.0 version: 22.10.0 @@ -397,8 +397,8 @@ packages: resolution: {integrity: sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} - '@fuman/fetch@0.0.7': - resolution: {integrity: sha512-yzyQI9ssqBrx04zKlfZwRHvJ9sw44RvfForjhaMhy7yA9jtghgVAjceeLTvF5xWEMgvIlIMfZRmSzEtCgGlfcg==} + '@fuman/fetch@0.0.10': + resolution: {integrity: sha512-Zy1GKZ8/NGP2adH0JXVaP91TZPpXngZDTGOFrb38pEp6RYAVSywt0yOPO2kwZGUpordfCj96CMNv+e6zUc7qqw==} peerDependencies: '@badrap/valita': '>=0.4.0' tough-cookie: ^5.0.0 || ^4.0.0 @@ -432,6 +432,9 @@ packages: '@fuman/node@0.0.4': resolution: {integrity: sha512-tgwbIceUHWuwh4RTwJRQ1sLjzuIGrWx0SeCrqYhGF+IkI/B7DY0FP2SZykWImkVDtW8IzmdZskPZqiDINRGcNg==} + '@fuman/utils@0.0.10': + resolution: {integrity: sha512-KVlDx0S1Og7IWcPi93f1T45WPfCSUV6/A4dQb36zZRtb8KECl1BK2u9WkNVI+sjrjKCb3xijjY5gq4lS3PqH5g==} + '@fuman/utils@0.0.4': resolution: {integrity: sha512-YBZIlGDbM8s9G85pWFZJ9wQrDY4511XLHZ06/uxZfXBY0eSStwje8JFNmRMNF0SjRk4D3iRmPl9wirHKTkg89w==} @@ -2332,9 +2335,9 @@ snapshots: '@faker-js/faker@9.3.0': {} - '@fuman/fetch@0.0.7(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': + '@fuman/fetch@0.0.10(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': dependencies: - '@fuman/utils': 0.0.4 + '@fuman/utils': 0.0.10 optionalDependencies: '@badrap/valita': 0.4.2 tough-cookie: 5.0.0 @@ -2364,6 +2367,8 @@ snapshots: '@fuman/net': 0.0.4 '@fuman/utils': 0.0.4 + '@fuman/utils@0.0.10': {} + '@fuman/utils@0.0.4': {} '@humanfs/core@0.19.1': {} diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index 0065ce6..2732d9e 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -14,7 +14,6 @@ import { generateOpusImageBlob } from '../../utils/opus.ts' const ffetchApi = ffetchBase.extend({ baseUrl: 'https://api-v2.soundcloud.com', - // @ts-expect-error lol fixme query: { client_id: '4BowhSywvkJtklODQDzjNMq9sK9wyDJ4', app_version: '1736857534', @@ -115,6 +114,14 @@ async function fetchPlaylistByUrl(url: string) { return ScPlaylist.parse(playlist.data) } +async function fetchPlaylistById(id: number) { + return ffetchApi(`/playlists/${id}`, { + query: { + linked_partitioning: '1', + }, + }).parsedJson(ScPlaylist) +} + async function fetchTracksById(trackIds: number[]) { return ffetchApi('/tracks', { query: { @@ -273,7 +280,7 @@ async function downloadPlaylist(playlist: ScPlaylist, params: { remaining -= ids.length spinnies.update('fetching', { text: `fetching ${remaining} tracks` }) }) - spinnies.succeed('fetching') + spinnies.succeed('fetching', { text: `fetched ${tracks.length} tracks` }) } const destDir = params.destination ?? join('assets/soundcloud-dl', sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)) @@ -287,7 +294,7 @@ async function downloadPlaylist(playlist: ScPlaylist, params: { spinnies.add(`${track.id}`, { text: filename }) await downloadTrack(track, { - destination: join(destDir, filename), + destination: join(destDir, sanitizeFilename(filename)), onRateLimit: (wait) => { spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) }, @@ -297,7 +304,7 @@ async function downloadPlaylist(playlist: ScPlaylist, params: { }) spinnies.remove(`${track.id}`) - }, { limit: 8 }) + }) console.log('done') spinnies.stopAll() @@ -358,27 +365,40 @@ async function downloadLikes(username: string) { spinnies.succeed('collect', { text: `collected ${tracks.length} tracks and ${playlists.length} playlists` }) + spinnies.add('tracks', { text: 'downloading tracks...' }) + const downloaded = 0 + const updateTracksSpinner = () => { + spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` }) + } + updateTracksSpinner() + const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(username)}-likes`) await mkdir(baseDir, { recursive: true }) - await asyncPool(tracks, async (track) => { - const filename = `${track.user.username} - ${track.title}` - spinnies.add(`${track.id}`, { text: filename }) - await downloadTrack(track, { - destination: join(baseDir, sanitizeFilename(filename)), - onRateLimit: (wait) => { - spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) - }, - onCdnRateLimit: () => { - spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) - }, - }) - spinnies.remove(`${track.id}`) - }) + // await asyncPool(tracks, async (track) => { + // const filename = `${track.user.username} - ${track.title}` + // spinnies.add(`${track.id}`, { text: filename }) + // await downloadTrack(track, { + // destination: join(baseDir, sanitizeFilename(filename)), + // onRateLimit: (wait) => { + // spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + // }, + // onCdnRateLimit: () => { + // spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + // }, + // }) + // spinnies.remove(`${track.id}`) + // updateTracksSpinner() + // }) + + spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` }) + spinnies.stopAll() for (const playlist of playlists) { - console.log('\uDB83\uDCB8 %s', playlist.title) - await downloadPlaylist(playlist, { + console.log(`\uDB83\uDCB8 ${playlist.title}`) + + const fullPlaylist = await fetchPlaylistById(playlist.id) + await downloadPlaylist(fullPlaylist, { destination: join(baseDir, sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)), }) } From fd6cfba7265ba78b78e2cc121488787a0f90fa8f Mon Sep 17 00:00:00 2001 From: desu-bot Date: Fri, 24 Jan 2025 09:56:11 +0000 Subject: [PATCH 14/61] chore: update public repo --- package.json | 4 +-- pnpm-lock.yaml | 21 +++++++----- scripts/media/soundcloud-dl.ts | 60 ++++++++++++++++++++++------------ 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 12de66b..bccf840 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ }, "devDependencies": { "@antfu/eslint-config": "3.10.0", - "@fuman/fetch": "0.0.7", - "@fuman/utils": "0.0.4", + "@fuman/fetch": "0.0.10", + "@fuman/utils": "0.0.10", "@types/node": "22.10.0", "domhandler": "^5.0.3", "dotenv": "16.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a21c8b..0185865 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,11 +79,11 @@ importers: specifier: 3.10.0 version: 3.10.0(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0)(typescript@5.7.2) '@fuman/fetch': - specifier: 0.0.7 - version: 0.0.7(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) + specifier: 0.0.10 + version: 0.0.10(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) '@fuman/utils': - specifier: 0.0.4 - version: 0.0.4 + specifier: 0.0.10 + version: 0.0.10 '@types/node': specifier: 22.10.0 version: 22.10.0 @@ -397,8 +397,8 @@ packages: resolution: {integrity: sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} - '@fuman/fetch@0.0.7': - resolution: {integrity: sha512-yzyQI9ssqBrx04zKlfZwRHvJ9sw44RvfForjhaMhy7yA9jtghgVAjceeLTvF5xWEMgvIlIMfZRmSzEtCgGlfcg==} + '@fuman/fetch@0.0.10': + resolution: {integrity: sha512-Zy1GKZ8/NGP2adH0JXVaP91TZPpXngZDTGOFrb38pEp6RYAVSywt0yOPO2kwZGUpordfCj96CMNv+e6zUc7qqw==} peerDependencies: '@badrap/valita': '>=0.4.0' tough-cookie: ^5.0.0 || ^4.0.0 @@ -432,6 +432,9 @@ packages: '@fuman/node@0.0.4': resolution: {integrity: sha512-tgwbIceUHWuwh4RTwJRQ1sLjzuIGrWx0SeCrqYhGF+IkI/B7DY0FP2SZykWImkVDtW8IzmdZskPZqiDINRGcNg==} + '@fuman/utils@0.0.10': + resolution: {integrity: sha512-KVlDx0S1Og7IWcPi93f1T45WPfCSUV6/A4dQb36zZRtb8KECl1BK2u9WkNVI+sjrjKCb3xijjY5gq4lS3PqH5g==} + '@fuman/utils@0.0.4': resolution: {integrity: sha512-YBZIlGDbM8s9G85pWFZJ9wQrDY4511XLHZ06/uxZfXBY0eSStwje8JFNmRMNF0SjRk4D3iRmPl9wirHKTkg89w==} @@ -2332,9 +2335,9 @@ snapshots: '@faker-js/faker@9.3.0': {} - '@fuman/fetch@0.0.7(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': + '@fuman/fetch@0.0.10(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': dependencies: - '@fuman/utils': 0.0.4 + '@fuman/utils': 0.0.10 optionalDependencies: '@badrap/valita': 0.4.2 tough-cookie: 5.0.0 @@ -2364,6 +2367,8 @@ snapshots: '@fuman/net': 0.0.4 '@fuman/utils': 0.0.4 + '@fuman/utils@0.0.10': {} + '@fuman/utils@0.0.4': {} '@humanfs/core@0.19.1': {} diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index 0065ce6..2732d9e 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -14,7 +14,6 @@ import { generateOpusImageBlob } from '../../utils/opus.ts' const ffetchApi = ffetchBase.extend({ baseUrl: 'https://api-v2.soundcloud.com', - // @ts-expect-error lol fixme query: { client_id: '4BowhSywvkJtklODQDzjNMq9sK9wyDJ4', app_version: '1736857534', @@ -115,6 +114,14 @@ async function fetchPlaylistByUrl(url: string) { return ScPlaylist.parse(playlist.data) } +async function fetchPlaylistById(id: number) { + return ffetchApi(`/playlists/${id}`, { + query: { + linked_partitioning: '1', + }, + }).parsedJson(ScPlaylist) +} + async function fetchTracksById(trackIds: number[]) { return ffetchApi('/tracks', { query: { @@ -273,7 +280,7 @@ async function downloadPlaylist(playlist: ScPlaylist, params: { remaining -= ids.length spinnies.update('fetching', { text: `fetching ${remaining} tracks` }) }) - spinnies.succeed('fetching') + spinnies.succeed('fetching', { text: `fetched ${tracks.length} tracks` }) } const destDir = params.destination ?? join('assets/soundcloud-dl', sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)) @@ -287,7 +294,7 @@ async function downloadPlaylist(playlist: ScPlaylist, params: { spinnies.add(`${track.id}`, { text: filename }) await downloadTrack(track, { - destination: join(destDir, filename), + destination: join(destDir, sanitizeFilename(filename)), onRateLimit: (wait) => { spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) }, @@ -297,7 +304,7 @@ async function downloadPlaylist(playlist: ScPlaylist, params: { }) spinnies.remove(`${track.id}`) - }, { limit: 8 }) + }) console.log('done') spinnies.stopAll() @@ -358,27 +365,40 @@ async function downloadLikes(username: string) { spinnies.succeed('collect', { text: `collected ${tracks.length} tracks and ${playlists.length} playlists` }) + spinnies.add('tracks', { text: 'downloading tracks...' }) + const downloaded = 0 + const updateTracksSpinner = () => { + spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` }) + } + updateTracksSpinner() + const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(username)}-likes`) await mkdir(baseDir, { recursive: true }) - await asyncPool(tracks, async (track) => { - const filename = `${track.user.username} - ${track.title}` - spinnies.add(`${track.id}`, { text: filename }) - await downloadTrack(track, { - destination: join(baseDir, sanitizeFilename(filename)), - onRateLimit: (wait) => { - spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) - }, - onCdnRateLimit: () => { - spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) - }, - }) - spinnies.remove(`${track.id}`) - }) + // await asyncPool(tracks, async (track) => { + // const filename = `${track.user.username} - ${track.title}` + // spinnies.add(`${track.id}`, { text: filename }) + // await downloadTrack(track, { + // destination: join(baseDir, sanitizeFilename(filename)), + // onRateLimit: (wait) => { + // spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + // }, + // onCdnRateLimit: () => { + // spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + // }, + // }) + // spinnies.remove(`${track.id}`) + // updateTracksSpinner() + // }) + + spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` }) + spinnies.stopAll() for (const playlist of playlists) { - console.log('\uDB83\uDCB8 %s', playlist.title) - await downloadPlaylist(playlist, { + console.log(`\uDB83\uDCB8 ${playlist.title}`) + + const fullPlaylist = await fetchPlaylistById(playlist.id) + await downloadPlaylist(fullPlaylist, { destination: join(baseDir, sanitizeFilename(`${playlist.user.username} - ${playlist.title}`)), }) } From 176d74e009c160c1225ccf996f0452fbda34d09a Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 29 Jan 2025 14:37:00 +0000 Subject: [PATCH 15/61] chore: update public repo From 75cc53978653228fdb4815353513d5d57dd26f7b Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 29 Jan 2025 14:37:00 +0000 Subject: [PATCH 16/61] chore: update public repo From 16a1237fdf00bda5f7ebd0eaf41bfc8b5cba79fb Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 29 Jan 2025 15:09:10 +0000 Subject: [PATCH 17/61] chore: update public repo From c2410ec78732891fd81f06e88141e93bd52fab51 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 29 Jan 2025 15:09:10 +0000 Subject: [PATCH 18/61] chore: update public repo From a1b29f7fadb1a9e2675e174394ed51bb56cb7ad4 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Mon, 3 Feb 2025 14:42:22 +0000 Subject: [PATCH 19/61] chore: update public repo --- scripts/media/soundcloud-dl.ts | 155 +++++++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 37 deletions(-) diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index 2732d9e..ac459dc 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -16,7 +16,7 @@ const ffetchApi = ffetchBase.extend({ baseUrl: 'https://api-v2.soundcloud.com', query: { client_id: '4BowhSywvkJtklODQDzjNMq9sK9wyDJ4', - app_version: '1736857534', + app_version: '1738322252', app_locale: 'en', }, addons: [ @@ -83,6 +83,18 @@ const ScPlaylist = z.object({ }) type ScPlaylist = z.infer +const ScUser = z.object({ + id: z.number(), + kind: z.literal('user'), + permalink_url: z.string(), + username: z.string(), + + likes_count: z.number(), + track_count: z.number(), + playlist_likes_count: z.number(), +}) +type ScUser = z.infer + const ScLike = z.object({ created_at: z.string(), kind: z.literal('like'), @@ -96,15 +108,6 @@ function extractHydrationData(html: string) { return JSON.parse(script.html()!.replace('window.__sc_hydration = ', '').slice(0, -1)) } -async function fetchTrackByUrl(url: string) { - const html = await ffetchHtml(url).text() - const hydrationData = extractHydrationData(html) - const track = hydrationData.find(it => it.hydratable === 'sound') - if (!track) throw new Error('no track found') - - return ScTrack.parse(track.data) -} - async function fetchPlaylistByUrl(url: string) { const html = await ffetchHtml(url).text() const hydrationData = extractHydrationData(html) @@ -318,11 +321,7 @@ async function downloadLikes(username: string) { const hydrationData = extractHydrationData(userPage) const user = hydrationData.find(it => it.hydratable === 'user') if (!user) throw new Error('no user found') - const userData = z.object({ - likes_count: z.number(), - playlist_likes_count: z.number(), - id: z.number(), - }).parse(user.data) + const userData = ScUser.parse(user.data) const tracks: ScTrack[] = [] const playlists: ScPlaylist[] = [] @@ -366,7 +365,7 @@ async function downloadLikes(username: string) { spinnies.succeed('collect', { text: `collected ${tracks.length} tracks and ${playlists.length} playlists` }) spinnies.add('tracks', { text: 'downloading tracks...' }) - const downloaded = 0 + let downloaded = 0 const updateTracksSpinner = () => { spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` }) } @@ -375,21 +374,22 @@ async function downloadLikes(username: string) { const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(username)}-likes`) await mkdir(baseDir, { recursive: true }) - // await asyncPool(tracks, async (track) => { - // const filename = `${track.user.username} - ${track.title}` - // spinnies.add(`${track.id}`, { text: filename }) - // await downloadTrack(track, { - // destination: join(baseDir, sanitizeFilename(filename)), - // onRateLimit: (wait) => { - // spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) - // }, - // onCdnRateLimit: () => { - // spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) - // }, - // }) - // spinnies.remove(`${track.id}`) - // updateTracksSpinner() - // }) + await asyncPool(tracks, async (track) => { + const filename = `${track.user.username} - ${track.title}` + spinnies.add(`${track.id}`, { text: filename }) + await downloadTrack(track, { + destination: join(baseDir, sanitizeFilename(filename)), + onRateLimit: (wait) => { + spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + }, + onCdnRateLimit: () => { + spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + }, + }) + spinnies.remove(`${track.id}`) + downloaded += 1 + updateTracksSpinner() + }) spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` }) spinnies.stopAll() @@ -404,6 +404,75 @@ async function downloadLikes(username: string) { } } +async function downloadUser(user: ScUser) { + const tracks: ScTrack[] = [] + const spinnies = new Spinnies() + + spinnies.add('collect') + const updateSpinner = () => { + const percent = Math.floor(tracks.length / user.track_count * 100) + spinnies.update('collect', { + text: `[${percent}%] collecting user tracks: ${tracks.length}/${user.track_count}`, + }) + } + updateSpinner() + + let offset = '0' + while (true) { + const res = await ffetchApi(`/users/${user.id}/tracks`, { + query: { + limit: 100, + offset, + linked_partitioning: '1', + }, + }).parsedJson(z.object({ + collection: z.array(ScTrack), + next_href: z.string().nullable(), + })) + + for (const track of res.collection) { + tracks.push(track) + } + + updateSpinner() + + if (!res.next_href) break + offset = new URL(res.next_href).searchParams.get('offset')! + } + + spinnies.succeed('collect', { text: `collected ${tracks.length} tracks` }) + + spinnies.add('tracks', { text: 'downloading tracks...' }) + let downloaded = 0 + const updateTracksSpinner = () => { + spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` }) + } + updateTracksSpinner() + + const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(user.username)}-tracks`) + await mkdir(baseDir, { recursive: true }) + + await asyncPool(tracks, async (track) => { + const filename = track.title + spinnies.add(`${track.id}`, { text: filename }) + await downloadTrack(track, { + destination: join(baseDir, sanitizeFilename(filename)), + onRateLimit: (wait) => { + spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + }, + onCdnRateLimit: () => { + spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + }, + }) + downloaded += 1 + spinnies.remove(`${track.id}`) + updateTracksSpinner() + }) + + spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` }) + spinnies.stopAll() +} + const url = process.argv[2] ?? await question('url > ') if (!url.startsWith('https://soundcloud.com/')) { console.error('url must start with https://soundcloud.com/') @@ -415,10 +484,22 @@ if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/sets\//i)) { } else if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/likes/i)) { await downloadLikes(url.match(/^https:\/\/soundcloud.com\/([a-z0-9-]+)\/likes/i)![1]) } else { - const track = await fetchTrackByUrl(url) - const filename = `${track.user.username} - ${track.title}` - console.log('downloading track:', filename) - await downloadTrack(track, { - destination: join('assets/soundcloud-dl', sanitizeFilename(filename)), - }) + const html = await ffetchHtml(url).text() + + const hydrationData = extractHydrationData(html) + const trackData = hydrationData.find(it => it.hydratable === 'sound') + if (trackData) { + const track = ScTrack.parse(trackData.data) + const filename = `${track.user.username} - ${track.title}` + console.log('downloading track:', filename) + await downloadTrack(track, { + destination: join('assets/soundcloud-dl', sanitizeFilename(filename)), + }) + } else { + const userData = hydrationData.find(it => it.hydratable === 'user') + if (userData) { + const user = ScUser.parse(userData.data) + await downloadUser(user) + } + } } From bb5311f828f43e87522e1fa084b32a3830a30b9a Mon Sep 17 00:00:00 2001 From: desu-bot Date: Mon, 3 Feb 2025 14:42:22 +0000 Subject: [PATCH 20/61] chore: update public repo --- scripts/media/soundcloud-dl.ts | 155 +++++++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 37 deletions(-) diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index 2732d9e..ac459dc 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -16,7 +16,7 @@ const ffetchApi = ffetchBase.extend({ baseUrl: 'https://api-v2.soundcloud.com', query: { client_id: '4BowhSywvkJtklODQDzjNMq9sK9wyDJ4', - app_version: '1736857534', + app_version: '1738322252', app_locale: 'en', }, addons: [ @@ -83,6 +83,18 @@ const ScPlaylist = z.object({ }) type ScPlaylist = z.infer +const ScUser = z.object({ + id: z.number(), + kind: z.literal('user'), + permalink_url: z.string(), + username: z.string(), + + likes_count: z.number(), + track_count: z.number(), + playlist_likes_count: z.number(), +}) +type ScUser = z.infer + const ScLike = z.object({ created_at: z.string(), kind: z.literal('like'), @@ -96,15 +108,6 @@ function extractHydrationData(html: string) { return JSON.parse(script.html()!.replace('window.__sc_hydration = ', '').slice(0, -1)) } -async function fetchTrackByUrl(url: string) { - const html = await ffetchHtml(url).text() - const hydrationData = extractHydrationData(html) - const track = hydrationData.find(it => it.hydratable === 'sound') - if (!track) throw new Error('no track found') - - return ScTrack.parse(track.data) -} - async function fetchPlaylistByUrl(url: string) { const html = await ffetchHtml(url).text() const hydrationData = extractHydrationData(html) @@ -318,11 +321,7 @@ async function downloadLikes(username: string) { const hydrationData = extractHydrationData(userPage) const user = hydrationData.find(it => it.hydratable === 'user') if (!user) throw new Error('no user found') - const userData = z.object({ - likes_count: z.number(), - playlist_likes_count: z.number(), - id: z.number(), - }).parse(user.data) + const userData = ScUser.parse(user.data) const tracks: ScTrack[] = [] const playlists: ScPlaylist[] = [] @@ -366,7 +365,7 @@ async function downloadLikes(username: string) { spinnies.succeed('collect', { text: `collected ${tracks.length} tracks and ${playlists.length} playlists` }) spinnies.add('tracks', { text: 'downloading tracks...' }) - const downloaded = 0 + let downloaded = 0 const updateTracksSpinner = () => { spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` }) } @@ -375,21 +374,22 @@ async function downloadLikes(username: string) { const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(username)}-likes`) await mkdir(baseDir, { recursive: true }) - // await asyncPool(tracks, async (track) => { - // const filename = `${track.user.username} - ${track.title}` - // spinnies.add(`${track.id}`, { text: filename }) - // await downloadTrack(track, { - // destination: join(baseDir, sanitizeFilename(filename)), - // onRateLimit: (wait) => { - // spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) - // }, - // onCdnRateLimit: () => { - // spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) - // }, - // }) - // spinnies.remove(`${track.id}`) - // updateTracksSpinner() - // }) + await asyncPool(tracks, async (track) => { + const filename = `${track.user.username} - ${track.title}` + spinnies.add(`${track.id}`, { text: filename }) + await downloadTrack(track, { + destination: join(baseDir, sanitizeFilename(filename)), + onRateLimit: (wait) => { + spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + }, + onCdnRateLimit: () => { + spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + }, + }) + spinnies.remove(`${track.id}`) + downloaded += 1 + updateTracksSpinner() + }) spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` }) spinnies.stopAll() @@ -404,6 +404,75 @@ async function downloadLikes(username: string) { } } +async function downloadUser(user: ScUser) { + const tracks: ScTrack[] = [] + const spinnies = new Spinnies() + + spinnies.add('collect') + const updateSpinner = () => { + const percent = Math.floor(tracks.length / user.track_count * 100) + spinnies.update('collect', { + text: `[${percent}%] collecting user tracks: ${tracks.length}/${user.track_count}`, + }) + } + updateSpinner() + + let offset = '0' + while (true) { + const res = await ffetchApi(`/users/${user.id}/tracks`, { + query: { + limit: 100, + offset, + linked_partitioning: '1', + }, + }).parsedJson(z.object({ + collection: z.array(ScTrack), + next_href: z.string().nullable(), + })) + + for (const track of res.collection) { + tracks.push(track) + } + + updateSpinner() + + if (!res.next_href) break + offset = new URL(res.next_href).searchParams.get('offset')! + } + + spinnies.succeed('collect', { text: `collected ${tracks.length} tracks` }) + + spinnies.add('tracks', { text: 'downloading tracks...' }) + let downloaded = 0 + const updateTracksSpinner = () => { + spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` }) + } + updateTracksSpinner() + + const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(user.username)}-tracks`) + await mkdir(baseDir, { recursive: true }) + + await asyncPool(tracks, async (track) => { + const filename = track.title + spinnies.add(`${track.id}`, { text: filename }) + await downloadTrack(track, { + destination: join(baseDir, sanitizeFilename(filename)), + onRateLimit: (wait) => { + spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` }) + }, + onCdnRateLimit: () => { + spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` }) + }, + }) + downloaded += 1 + spinnies.remove(`${track.id}`) + updateTracksSpinner() + }) + + spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` }) + spinnies.stopAll() +} + const url = process.argv[2] ?? await question('url > ') if (!url.startsWith('https://soundcloud.com/')) { console.error('url must start with https://soundcloud.com/') @@ -415,10 +484,22 @@ if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/sets\//i)) { } else if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/likes/i)) { await downloadLikes(url.match(/^https:\/\/soundcloud.com\/([a-z0-9-]+)\/likes/i)![1]) } else { - const track = await fetchTrackByUrl(url) - const filename = `${track.user.username} - ${track.title}` - console.log('downloading track:', filename) - await downloadTrack(track, { - destination: join('assets/soundcloud-dl', sanitizeFilename(filename)), - }) + const html = await ffetchHtml(url).text() + + const hydrationData = extractHydrationData(html) + const trackData = hydrationData.find(it => it.hydratable === 'sound') + if (trackData) { + const track = ScTrack.parse(trackData.data) + const filename = `${track.user.username} - ${track.title}` + console.log('downloading track:', filename) + await downloadTrack(track, { + destination: join('assets/soundcloud-dl', sanitizeFilename(filename)), + }) + } else { + const userData = hydrationData.find(it => it.hydratable === 'user') + if (userData) { + const user = ScUser.parse(userData.data) + await downloadUser(user) + } + } } From 2bb69565a3272f4c9075376bfc6c80b804023bf3 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 5 Feb 2025 09:12:00 +0000 Subject: [PATCH 21/61] chore: update public repo From c118bcbfc3cd92f24a0e566dea954c371e4bf6a9 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 5 Feb 2025 09:12:00 +0000 Subject: [PATCH 22/61] chore: update public repo From 1b72ac0bc4915314f1555474efb6dbd214077b46 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 19 Feb 2025 02:30:26 +0000 Subject: [PATCH 23/61] chore: update public repo --- package.json | 9 ++ pnpm-lock.yaml | 38 ++++++++ scripts/misc/shikimori/.gitignore | 1 + scripts/misc/shikimori/animes.ts | 53 +++++++++++ scripts/misc/shikimori/bans.ts | 30 +++++++ scripts/misc/shikimori/characters.ts | 59 ++++++++++++ scripts/misc/shikimori/clubs.ts | 49 ++++++++++ scripts/misc/shikimori/comments.ts | 37 ++++++++ scripts/misc/shikimori/people.ts | 59 ++++++++++++ scripts/misc/shikimori/users.ts | 129 +++++++++++++++++++++++++++ scripts/misc/shikimori/utils.ts | 39 ++++++++ scripts/misc/update-forkgram.ts | 9 +- 12 files changed, 510 insertions(+), 2 deletions(-) create mode 100644 scripts/misc/shikimori/.gitignore create mode 100644 scripts/misc/shikimori/animes.ts create mode 100644 scripts/misc/shikimori/bans.ts create mode 100644 scripts/misc/shikimori/characters.ts create mode 100644 scripts/misc/shikimori/clubs.ts create mode 100644 scripts/misc/shikimori/comments.ts create mode 100644 scripts/misc/shikimori/people.ts create mode 100644 scripts/misc/shikimori/users.ts create mode 100644 scripts/misc/shikimori/utils.ts diff --git a/package.json b/package.json index bccf840..1ecf24e 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,11 @@ "@fuman/net": "^0.0.9", "@fuman/node": "^0.0.4", "@mtcute/node": "^0.19.1", + "@types/better-sqlite3": "^7.6.12", "@types/plist": "^3.0.5", "@types/spinnies": "^0.5.3", + "better-sqlite3": "^11.8.1", + "canvas": "^3.1.0", "cheerio": "^1.0.0", "es-main": "^1.3.0", "filesize": "^10.1.6", @@ -38,5 +41,11 @@ "htmlparser2": "^10.0.0", "zod": "3.23.8", "zx": "8.2.2" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3", + "canvas" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0185865..ef97f05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,12 +23,21 @@ importers: '@mtcute/node': specifier: ^0.19.1 version: 0.19.1 + '@types/better-sqlite3': + specifier: ^7.6.12 + version: 7.6.12 '@types/plist': specifier: ^3.0.5 version: 3.0.5 '@types/spinnies': specifier: ^0.5.3 version: 0.5.3 + better-sqlite3: + specifier: ^11.8.1 + version: 11.8.1 + canvas: + specifier: ^3.1.0 + version: 3.1.0 cheerio: specifier: ^1.0.0 version: 1.0.0 @@ -507,6 +516,9 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@types/better-sqlite3@7.6.12': + resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -688,6 +700,9 @@ packages: better-sqlite3@11.3.0: resolution: {integrity: sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==} + better-sqlite3@11.8.1: + resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -726,6 +741,10 @@ packages: caniuse-lite@1.0.30001684: resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} + canvas@3.1.0: + resolution: {integrity: sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==} + engines: {node: ^18.12.0 || >= 20.9.0} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1579,6 +1598,9 @@ packages: resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} engines: {node: '>=10'} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -2462,6 +2484,10 @@ snapshots: - supports-color - typescript + '@types/better-sqlite3@7.6.12': + dependencies: + '@types/node': 22.10.0 + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -2669,6 +2695,11 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.2 + better-sqlite3@11.8.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.2 + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -2712,6 +2743,11 @@ snapshots: caniuse-lite@1.0.30001684: {} + canvas@3.1.0: + dependencies: + node-addon-api: 7.1.1 + prebuild-install: 7.1.2 + ccount@2.0.1: {} chalk@2.4.2: @@ -3808,6 +3844,8 @@ snapshots: dependencies: semver: 7.6.3 + node-addon-api@7.1.1: {} + node-releases@2.0.18: {} normalize-package-data@2.5.0: diff --git a/scripts/misc/shikimori/.gitignore b/scripts/misc/shikimori/.gitignore new file mode 100644 index 0000000..56d5f49 --- /dev/null +++ b/scripts/misc/shikimori/.gitignore @@ -0,0 +1 @@ +/_very-secret-ratelimit-bypass.ts \ No newline at end of file diff --git a/scripts/misc/shikimori/animes.ts b/scripts/misc/shikimori/animes.ts new file mode 100644 index 0000000..b681ac0 --- /dev/null +++ b/scripts/misc/shikimori/animes.ts @@ -0,0 +1,53 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const isManga = process.argv[2] === 'manga' +const isRanobe = process.argv[2] === 'ranobe' + +const collection = isManga ? 'mangas' : isRanobe ? 'ranobe' : 'animes' + +const db = new Database('assets/shikimori.db') +db.exec(` + create table if not exists ${collection} ( + id integer primary key, + data text not null + ); + create table if not exists ${collection}_related ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare(`insert into ${collection} (id, data) values (?, ?) on conflict (id) do update set data = excluded.data`) +const insertRelatedQuery = db.prepare(`insert into ${collection}_related (id, data) values (?, ?) on conflict (id) do update set data = excluded.data`) + +const maxId = await ffetchShiki(`/api/${collection}?order=id_desc`).json().then(res => res[0].id) +console.log('max id: %d', maxId) + +const counter = counterIter(1, maxId) + +await asyncPool(counter.iter, async (id) => { + if (id % 1000 === 0) { + console.log('currently at %d', id) + } + // const data = await ffetchShiki(`/api/${collection}/${id}`, { + // validateResponse: false, + // }).json() + + // if (data.code === 404) { + // return + // } + + // insertQuery.run(id, JSON.stringify(data)) + + const data = await ffetchShiki(`/api/${collection}/${id}/related`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + return + } + + insertRelatedQuery.run(id, JSON.stringify(data)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/bans.ts b/scripts/misc/shikimori/bans.ts new file mode 100644 index 0000000..085d954 --- /dev/null +++ b/scripts/misc/shikimori/bans.ts @@ -0,0 +1,30 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.exec(` + create table if not exists bans ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into bans (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +const counter = counterIter(1) + +await asyncPool(counter.iter, async (page) => { + if (page % 100 === 0) { + console.log('currently at page %d', page) + } + const data = await ffetchShiki(`/api/bans?page=${page}`).json() + if (!data.length) { + counter.end() + return + } + + for (const ban of data) { + insertQuery.run(ban.id, JSON.stringify(ban)) + } +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/characters.ts b/scripts/misc/shikimori/characters.ts new file mode 100644 index 0000000..29fe2f1 --- /dev/null +++ b/scripts/misc/shikimori/characters.ts @@ -0,0 +1,59 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists characters ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into characters (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +// find maxId with binary search +let maxIdPage = 20000 +let maxIdPageStart = 1 +let maxId = 0 +while (true) { + const midPage = Math.floor((maxIdPageStart + maxIdPage) / 2) + console.log('trying page %d', midPage) + const res = await ffetchShiki.post('/api/graphql', { + json: { + query: `{characters(page: ${midPage}, limit: 50) { id }}`, + }, + }).json() + const items = res.data.characters + if (!items.length) { + maxIdPage = midPage - 1 + continue + } + + if (maxIdPageStart === midPage) { + maxId = Math.max(...items.map(item => item.id)) + break + } else { + maxIdPageStart = midPage + } +} + +console.log('max id: %d', maxId) + +const counter = counterIter(1, maxId) + +await asyncPool(counter.iter, async (id) => { + if (id % 1000 === 0) { + console.log('currently at %d', id) + } + const data = await ffetchShiki(`/api/characters/${id}`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + return + } + + insertQuery.run(id, JSON.stringify(data)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/clubs.ts b/scripts/misc/shikimori/clubs.ts new file mode 100644 index 0000000..714f093 --- /dev/null +++ b/scripts/misc/shikimori/clubs.ts @@ -0,0 +1,49 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists clubs ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into clubs (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +// collect clubs ids + +const ids: Set = new Set() + +const pageCounter = counterIter(1) + +await asyncPool(pageCounter.iter, async (page) => { + const data = await ffetchShiki('/api/clubs', { + query: { page, limit: 50 }, + validateResponse: false, + }).json() + if (!data.length) { + pageCounter.end() + return + } + + for (const club of data) { + ids.add(club.id) + } +}, { limit: 16 }) + +console.log('collected %d clubs', ids.size) + +await asyncPool(ids, async (id, idx) => { + if (idx % 100 === 0) { + console.log('currently at %d', idx) + } + + const clubData = await ffetchShiki(`/api/clubs/${id}`).json() + if (clubData.code === 404) { + return + } + insertQuery.run(id, JSON.stringify(clubData)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/comments.ts b/scripts/misc/shikimori/comments.ts new file mode 100644 index 0000000..80a5bf3 --- /dev/null +++ b/scripts/misc/shikimori/comments.ts @@ -0,0 +1,37 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists comments ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into comments (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +const counter = counterIter(11312000) +let consequent404 = 0 +await asyncPool(counter.iter, async (id) => { + if (id % 1000 === 0) { + console.log('currently at %d', id) + } + const data = await ffetchShiki(`/api/comments/${id}`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + consequent404++ + if (consequent404 > 10_000) { + counter.end() + console.log('10k consequent 404-s, stopping') + } + return + } + + consequent404 = 0 + insertQuery.run(id, JSON.stringify(data)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/people.ts b/scripts/misc/shikimori/people.ts new file mode 100644 index 0000000..50073bf --- /dev/null +++ b/scripts/misc/shikimori/people.ts @@ -0,0 +1,59 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists people ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into people (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +// find maxId with binary search +let maxIdPage = 20000 +let maxIdPageStart = 1 +let maxId = 0 +while (true) { + const midPage = Math.floor((maxIdPageStart + maxIdPage) / 2) + console.log('trying page %d', midPage) + const res = await ffetchShiki.post('/api/graphql', { + json: { + query: `{people(page: ${midPage}, limit: 50) { id }}`, + }, + }).json() + const items = res.data.people + if (!items.length) { + maxIdPage = midPage - 1 + continue + } + + if (maxIdPageStart === midPage) { + maxId = Math.max(...items.map(item => item.id)) + break + } else { + maxIdPageStart = midPage + } +} + +console.log('max id: %d', maxId) + +const counter = counterIter(1, maxId) + +await asyncPool(counter.iter, async (id) => { + if (id % 1000 === 0) { + console.log('currently at %d', id) + } + const data = await ffetchShiki(`/api/people/${id}`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + return + } + + insertQuery.run(id, JSON.stringify(data)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/users.ts b/scripts/misc/shikimori/users.ts new file mode 100644 index 0000000..73411b8 --- /dev/null +++ b/scripts/misc/shikimori/users.ts @@ -0,0 +1,129 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists users ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into users (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +async function fetchUserFriends(userId: number) { + const list: any[] = [] + for (let page = 1; ; page++) { + const data = await ffetchShiki(`/api/users/${userId}/friends`, { + query: { page, limit: 100 }, + validateResponse: false, + }).json() + if (!data.length) { + break + } + + list.push(...data) + } + + return list +} + +async function fetchUserRates(userId: number, kind: 'anime' | 'manga') { + const list: any[] = [] + + for (let page = 1; ; page++) { + const data = await ffetchShiki(`/api/users/${userId}/${kind}_rates`, { + query: { page, limit: 1000 }, + validateResponse: false, + }).json() + if (data === null || !data.length) { + break + } + + for (const item of data) { + // clean up unnecessary data before inserting + delete item.user + if (item[kind]) { + item[`${kind}_id`] = item[kind].id + delete item[kind] + } + + list.push(item) + } + } + + return list +} + +async function fetchUserHistory(userId: number) { + const list: any[] = [] + for (let page = 0; ; page++) { + const data = await ffetchShiki(`/api/users/${userId}/history`, { + query: { page, limit: 100 }, + validateResponse: false, + }).json() + if (!data.length) { + break + } + + for (const item of data) { + if (item.target) { + item.target_type = item.target.url.startsWith('/animes/') ? 'anime' : 'manga' + item.target_id = item.target.id + delete item.target + } + list.push(item) + } + } + + return list +} + +const counter = counterIter(467800) +let consequent404 = 0 +await asyncPool(counter.iter, async (id) => { + if (id % 100 === 0) { + console.log('currently at %d', id) + } + const data = await ffetchShiki(`/api/users/${id}`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + consequent404++ + if (consequent404 > 1_000) { + counter.end() + console.log('1k consequent 404-s, stopping') + } + return + } + + consequent404 = 0 + + // fetch extra data + const [ + favsData, + friends, + animeRates, + mangaRates, + history, + ] = await Promise.all([ + ffetchShiki(`/api/users/${id}/favourites`).json(), + fetchUserFriends(id), + fetchUserRates(id, 'anime'), + fetchUserRates(id, 'manga'), + fetchUserHistory(id), + ]) + + data._extra = { + favs: favsData, + friends, + animeRates, + mangaRates, + history, + } + + insertQuery.run(id, JSON.stringify(data)) +}, { limit: 32 }) diff --git a/scripts/misc/shikimori/utils.ts b/scripts/misc/shikimori/utils.ts new file mode 100644 index 0000000..e25dc28 --- /dev/null +++ b/scripts/misc/shikimori/utils.ts @@ -0,0 +1,39 @@ +import { ffetch as ffetchBase } from '../../../utils/fetch.ts' +import { rateLimitBypass } from './_very-secret-ratelimit-bypass.ts' + +export const ffetchShiki = ffetchBase.extend({ + baseUrl: 'https://shikimori.one', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + }, + retry: {}, + ...(rateLimitBypass as any), +}) + +export function counterIter(start = 0, end = Infinity) { + let i = start + let ended = false + + const iter: IterableIterator = { + [Symbol.iterator]: () => iter, + next() { + if (ended) { + return { value: undefined, done: true } + } + + if (i > end) { + return { value: undefined, done: true } + } + + return { value: i++, done: false } + }, + } + return { + iter, + end: () => { + ended = true + }, + } +} diff --git a/scripts/misc/update-forkgram.ts b/scripts/misc/update-forkgram.ts index 878def9..700beab 100644 --- a/scripts/misc/update-forkgram.ts +++ b/scripts/misc/update-forkgram.ts @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises' import { join } from 'node:path' import plist from 'plist' import { z } from 'zod' -import { $ } from 'zx' +import { $, sleep } from 'zx' import { ffetch } from '../../utils/fetch.ts' const latestVerInfo = await ffetch('https://api.github.com/repos/forkgram/tdesktop/releases/latest').parsedJson( @@ -40,12 +40,17 @@ if (!arm64Asset) { console.log('installing new version...') await $`curl -L ${arm64Asset.browser_download_url} -o /tmp/forkgram.zip` await $`unzip -o /tmp/forkgram.zip -d /tmp/forkgram` -await $`kill -9 $(pgrep -f /Applications/Forkgram.app/Contents/MacOS/Telegram)` +const pid = await $`/usr/bin/pgrep -f /Applications/Forkgram.app/Contents/MacOS/Telegram`.text().catch(() => null) +if (pid) { + await $`kill -9 ${pid.trim()}` +} await $`rm -rf ${INSTALL_PATH}` await $`mv /tmp/forkgram/Telegram.app ${INSTALL_PATH}` await $`rm -rf /tmp/forkgram` await $`xattr -cr ${INSTALL_PATH}` +await sleep(1000) + await $`open ${INSTALL_PATH}` console.log('āœ… done') From f02ccb6029e36b23ce5869b952e8b6ffa742ddbe Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 19 Feb 2025 02:30:26 +0000 Subject: [PATCH 24/61] chore: update public repo --- package.json | 9 ++ pnpm-lock.yaml | 38 ++++++++ scripts/misc/shikimori/.gitignore | 1 + scripts/misc/shikimori/animes.ts | 53 +++++++++++ scripts/misc/shikimori/bans.ts | 30 +++++++ scripts/misc/shikimori/characters.ts | 59 ++++++++++++ scripts/misc/shikimori/clubs.ts | 49 ++++++++++ scripts/misc/shikimori/comments.ts | 37 ++++++++ scripts/misc/shikimori/people.ts | 59 ++++++++++++ scripts/misc/shikimori/users.ts | 129 +++++++++++++++++++++++++++ scripts/misc/shikimori/utils.ts | 39 ++++++++ scripts/misc/update-forkgram.ts | 9 +- 12 files changed, 510 insertions(+), 2 deletions(-) create mode 100644 scripts/misc/shikimori/.gitignore create mode 100644 scripts/misc/shikimori/animes.ts create mode 100644 scripts/misc/shikimori/bans.ts create mode 100644 scripts/misc/shikimori/characters.ts create mode 100644 scripts/misc/shikimori/clubs.ts create mode 100644 scripts/misc/shikimori/comments.ts create mode 100644 scripts/misc/shikimori/people.ts create mode 100644 scripts/misc/shikimori/users.ts create mode 100644 scripts/misc/shikimori/utils.ts diff --git a/package.json b/package.json index bccf840..1ecf24e 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,11 @@ "@fuman/net": "^0.0.9", "@fuman/node": "^0.0.4", "@mtcute/node": "^0.19.1", + "@types/better-sqlite3": "^7.6.12", "@types/plist": "^3.0.5", "@types/spinnies": "^0.5.3", + "better-sqlite3": "^11.8.1", + "canvas": "^3.1.0", "cheerio": "^1.0.0", "es-main": "^1.3.0", "filesize": "^10.1.6", @@ -38,5 +41,11 @@ "htmlparser2": "^10.0.0", "zod": "3.23.8", "zx": "8.2.2" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3", + "canvas" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0185865..ef97f05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,12 +23,21 @@ importers: '@mtcute/node': specifier: ^0.19.1 version: 0.19.1 + '@types/better-sqlite3': + specifier: ^7.6.12 + version: 7.6.12 '@types/plist': specifier: ^3.0.5 version: 3.0.5 '@types/spinnies': specifier: ^0.5.3 version: 0.5.3 + better-sqlite3: + specifier: ^11.8.1 + version: 11.8.1 + canvas: + specifier: ^3.1.0 + version: 3.1.0 cheerio: specifier: ^1.0.0 version: 1.0.0 @@ -507,6 +516,9 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@types/better-sqlite3@7.6.12': + resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -688,6 +700,9 @@ packages: better-sqlite3@11.3.0: resolution: {integrity: sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==} + better-sqlite3@11.8.1: + resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -726,6 +741,10 @@ packages: caniuse-lite@1.0.30001684: resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==} + canvas@3.1.0: + resolution: {integrity: sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==} + engines: {node: ^18.12.0 || >= 20.9.0} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1579,6 +1598,9 @@ packages: resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} engines: {node: '>=10'} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -2462,6 +2484,10 @@ snapshots: - supports-color - typescript + '@types/better-sqlite3@7.6.12': + dependencies: + '@types/node': 22.10.0 + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -2669,6 +2695,11 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.2 + better-sqlite3@11.8.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.2 + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -2712,6 +2743,11 @@ snapshots: caniuse-lite@1.0.30001684: {} + canvas@3.1.0: + dependencies: + node-addon-api: 7.1.1 + prebuild-install: 7.1.2 + ccount@2.0.1: {} chalk@2.4.2: @@ -3808,6 +3844,8 @@ snapshots: dependencies: semver: 7.6.3 + node-addon-api@7.1.1: {} + node-releases@2.0.18: {} normalize-package-data@2.5.0: diff --git a/scripts/misc/shikimori/.gitignore b/scripts/misc/shikimori/.gitignore new file mode 100644 index 0000000..56d5f49 --- /dev/null +++ b/scripts/misc/shikimori/.gitignore @@ -0,0 +1 @@ +/_very-secret-ratelimit-bypass.ts \ No newline at end of file diff --git a/scripts/misc/shikimori/animes.ts b/scripts/misc/shikimori/animes.ts new file mode 100644 index 0000000..b681ac0 --- /dev/null +++ b/scripts/misc/shikimori/animes.ts @@ -0,0 +1,53 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const isManga = process.argv[2] === 'manga' +const isRanobe = process.argv[2] === 'ranobe' + +const collection = isManga ? 'mangas' : isRanobe ? 'ranobe' : 'animes' + +const db = new Database('assets/shikimori.db') +db.exec(` + create table if not exists ${collection} ( + id integer primary key, + data text not null + ); + create table if not exists ${collection}_related ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare(`insert into ${collection} (id, data) values (?, ?) on conflict (id) do update set data = excluded.data`) +const insertRelatedQuery = db.prepare(`insert into ${collection}_related (id, data) values (?, ?) on conflict (id) do update set data = excluded.data`) + +const maxId = await ffetchShiki(`/api/${collection}?order=id_desc`).json().then(res => res[0].id) +console.log('max id: %d', maxId) + +const counter = counterIter(1, maxId) + +await asyncPool(counter.iter, async (id) => { + if (id % 1000 === 0) { + console.log('currently at %d', id) + } + // const data = await ffetchShiki(`/api/${collection}/${id}`, { + // validateResponse: false, + // }).json() + + // if (data.code === 404) { + // return + // } + + // insertQuery.run(id, JSON.stringify(data)) + + const data = await ffetchShiki(`/api/${collection}/${id}/related`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + return + } + + insertRelatedQuery.run(id, JSON.stringify(data)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/bans.ts b/scripts/misc/shikimori/bans.ts new file mode 100644 index 0000000..085d954 --- /dev/null +++ b/scripts/misc/shikimori/bans.ts @@ -0,0 +1,30 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.exec(` + create table if not exists bans ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into bans (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +const counter = counterIter(1) + +await asyncPool(counter.iter, async (page) => { + if (page % 100 === 0) { + console.log('currently at page %d', page) + } + const data = await ffetchShiki(`/api/bans?page=${page}`).json() + if (!data.length) { + counter.end() + return + } + + for (const ban of data) { + insertQuery.run(ban.id, JSON.stringify(ban)) + } +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/characters.ts b/scripts/misc/shikimori/characters.ts new file mode 100644 index 0000000..29fe2f1 --- /dev/null +++ b/scripts/misc/shikimori/characters.ts @@ -0,0 +1,59 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists characters ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into characters (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +// find maxId with binary search +let maxIdPage = 20000 +let maxIdPageStart = 1 +let maxId = 0 +while (true) { + const midPage = Math.floor((maxIdPageStart + maxIdPage) / 2) + console.log('trying page %d', midPage) + const res = await ffetchShiki.post('/api/graphql', { + json: { + query: `{characters(page: ${midPage}, limit: 50) { id }}`, + }, + }).json() + const items = res.data.characters + if (!items.length) { + maxIdPage = midPage - 1 + continue + } + + if (maxIdPageStart === midPage) { + maxId = Math.max(...items.map(item => item.id)) + break + } else { + maxIdPageStart = midPage + } +} + +console.log('max id: %d', maxId) + +const counter = counterIter(1, maxId) + +await asyncPool(counter.iter, async (id) => { + if (id % 1000 === 0) { + console.log('currently at %d', id) + } + const data = await ffetchShiki(`/api/characters/${id}`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + return + } + + insertQuery.run(id, JSON.stringify(data)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/clubs.ts b/scripts/misc/shikimori/clubs.ts new file mode 100644 index 0000000..714f093 --- /dev/null +++ b/scripts/misc/shikimori/clubs.ts @@ -0,0 +1,49 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists clubs ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into clubs (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +// collect clubs ids + +const ids: Set = new Set() + +const pageCounter = counterIter(1) + +await asyncPool(pageCounter.iter, async (page) => { + const data = await ffetchShiki('/api/clubs', { + query: { page, limit: 50 }, + validateResponse: false, + }).json() + if (!data.length) { + pageCounter.end() + return + } + + for (const club of data) { + ids.add(club.id) + } +}, { limit: 16 }) + +console.log('collected %d clubs', ids.size) + +await asyncPool(ids, async (id, idx) => { + if (idx % 100 === 0) { + console.log('currently at %d', idx) + } + + const clubData = await ffetchShiki(`/api/clubs/${id}`).json() + if (clubData.code === 404) { + return + } + insertQuery.run(id, JSON.stringify(clubData)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/comments.ts b/scripts/misc/shikimori/comments.ts new file mode 100644 index 0000000..80a5bf3 --- /dev/null +++ b/scripts/misc/shikimori/comments.ts @@ -0,0 +1,37 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists comments ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into comments (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +const counter = counterIter(11312000) +let consequent404 = 0 +await asyncPool(counter.iter, async (id) => { + if (id % 1000 === 0) { + console.log('currently at %d', id) + } + const data = await ffetchShiki(`/api/comments/${id}`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + consequent404++ + if (consequent404 > 10_000) { + counter.end() + console.log('10k consequent 404-s, stopping') + } + return + } + + consequent404 = 0 + insertQuery.run(id, JSON.stringify(data)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/people.ts b/scripts/misc/shikimori/people.ts new file mode 100644 index 0000000..50073bf --- /dev/null +++ b/scripts/misc/shikimori/people.ts @@ -0,0 +1,59 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists people ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into people (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +// find maxId with binary search +let maxIdPage = 20000 +let maxIdPageStart = 1 +let maxId = 0 +while (true) { + const midPage = Math.floor((maxIdPageStart + maxIdPage) / 2) + console.log('trying page %d', midPage) + const res = await ffetchShiki.post('/api/graphql', { + json: { + query: `{people(page: ${midPage}, limit: 50) { id }}`, + }, + }).json() + const items = res.data.people + if (!items.length) { + maxIdPage = midPage - 1 + continue + } + + if (maxIdPageStart === midPage) { + maxId = Math.max(...items.map(item => item.id)) + break + } else { + maxIdPageStart = midPage + } +} + +console.log('max id: %d', maxId) + +const counter = counterIter(1, maxId) + +await asyncPool(counter.iter, async (id) => { + if (id % 1000 === 0) { + console.log('currently at %d', id) + } + const data = await ffetchShiki(`/api/people/${id}`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + return + } + + insertQuery.run(id, JSON.stringify(data)) +}, { limit: 64 }) diff --git a/scripts/misc/shikimori/users.ts b/scripts/misc/shikimori/users.ts new file mode 100644 index 0000000..73411b8 --- /dev/null +++ b/scripts/misc/shikimori/users.ts @@ -0,0 +1,129 @@ +import { asyncPool } from '@fuman/utils' +import Database from 'better-sqlite3' +import { counterIter, ffetchShiki } from './utils.ts' + +const db = new Database('assets/shikimori.db') +db.pragma('journal_mode = WAL') +db.exec(` + create table if not exists users ( + id integer primary key, + data text not null + ); +`) + +const insertQuery = db.prepare('insert into users (id, data) values (?, ?) on conflict (id) do update set data = excluded.data') + +async function fetchUserFriends(userId: number) { + const list: any[] = [] + for (let page = 1; ; page++) { + const data = await ffetchShiki(`/api/users/${userId}/friends`, { + query: { page, limit: 100 }, + validateResponse: false, + }).json() + if (!data.length) { + break + } + + list.push(...data) + } + + return list +} + +async function fetchUserRates(userId: number, kind: 'anime' | 'manga') { + const list: any[] = [] + + for (let page = 1; ; page++) { + const data = await ffetchShiki(`/api/users/${userId}/${kind}_rates`, { + query: { page, limit: 1000 }, + validateResponse: false, + }).json() + if (data === null || !data.length) { + break + } + + for (const item of data) { + // clean up unnecessary data before inserting + delete item.user + if (item[kind]) { + item[`${kind}_id`] = item[kind].id + delete item[kind] + } + + list.push(item) + } + } + + return list +} + +async function fetchUserHistory(userId: number) { + const list: any[] = [] + for (let page = 0; ; page++) { + const data = await ffetchShiki(`/api/users/${userId}/history`, { + query: { page, limit: 100 }, + validateResponse: false, + }).json() + if (!data.length) { + break + } + + for (const item of data) { + if (item.target) { + item.target_type = item.target.url.startsWith('/animes/') ? 'anime' : 'manga' + item.target_id = item.target.id + delete item.target + } + list.push(item) + } + } + + return list +} + +const counter = counterIter(467800) +let consequent404 = 0 +await asyncPool(counter.iter, async (id) => { + if (id % 100 === 0) { + console.log('currently at %d', id) + } + const data = await ffetchShiki(`/api/users/${id}`, { + validateResponse: false, + }).json() + + if (data.code === 404) { + consequent404++ + if (consequent404 > 1_000) { + counter.end() + console.log('1k consequent 404-s, stopping') + } + return + } + + consequent404 = 0 + + // fetch extra data + const [ + favsData, + friends, + animeRates, + mangaRates, + history, + ] = await Promise.all([ + ffetchShiki(`/api/users/${id}/favourites`).json(), + fetchUserFriends(id), + fetchUserRates(id, 'anime'), + fetchUserRates(id, 'manga'), + fetchUserHistory(id), + ]) + + data._extra = { + favs: favsData, + friends, + animeRates, + mangaRates, + history, + } + + insertQuery.run(id, JSON.stringify(data)) +}, { limit: 32 }) diff --git a/scripts/misc/shikimori/utils.ts b/scripts/misc/shikimori/utils.ts new file mode 100644 index 0000000..e25dc28 --- /dev/null +++ b/scripts/misc/shikimori/utils.ts @@ -0,0 +1,39 @@ +import { ffetch as ffetchBase } from '../../../utils/fetch.ts' +import { rateLimitBypass } from './_very-secret-ratelimit-bypass.ts' + +export const ffetchShiki = ffetchBase.extend({ + baseUrl: 'https://shikimori.one', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + }, + retry: {}, + ...(rateLimitBypass as any), +}) + +export function counterIter(start = 0, end = Infinity) { + let i = start + let ended = false + + const iter: IterableIterator = { + [Symbol.iterator]: () => iter, + next() { + if (ended) { + return { value: undefined, done: true } + } + + if (i > end) { + return { value: undefined, done: true } + } + + return { value: i++, done: false } + }, + } + return { + iter, + end: () => { + ended = true + }, + } +} diff --git a/scripts/misc/update-forkgram.ts b/scripts/misc/update-forkgram.ts index 878def9..700beab 100644 --- a/scripts/misc/update-forkgram.ts +++ b/scripts/misc/update-forkgram.ts @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises' import { join } from 'node:path' import plist from 'plist' import { z } from 'zod' -import { $ } from 'zx' +import { $, sleep } from 'zx' import { ffetch } from '../../utils/fetch.ts' const latestVerInfo = await ffetch('https://api.github.com/repos/forkgram/tdesktop/releases/latest').parsedJson( @@ -40,12 +40,17 @@ if (!arm64Asset) { console.log('installing new version...') await $`curl -L ${arm64Asset.browser_download_url} -o /tmp/forkgram.zip` await $`unzip -o /tmp/forkgram.zip -d /tmp/forkgram` -await $`kill -9 $(pgrep -f /Applications/Forkgram.app/Contents/MacOS/Telegram)` +const pid = await $`/usr/bin/pgrep -f /Applications/Forkgram.app/Contents/MacOS/Telegram`.text().catch(() => null) +if (pid) { + await $`kill -9 ${pid.trim()}` +} await $`rm -rf ${INSTALL_PATH}` await $`mv /tmp/forkgram/Telegram.app ${INSTALL_PATH}` await $`rm -rf /tmp/forkgram` await $`xattr -cr ${INSTALL_PATH}` +await sleep(1000) + await $`open ${INSTALL_PATH}` console.log('āœ… done') From 7e7001e63bf9d952521b72643a588435d4aaca82 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 8 Mar 2025 08:19:53 +0000 Subject: [PATCH 25/61] chore: update public repo --- eslint.config.js | 7 ++++++- scripts/misc/shikimori/utils.ts | 26 +------------------------- utils/counter.ts | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 utils/counter.ts diff --git a/eslint.config.js b/eslint.config.js index a159663..81e0f5d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,8 +1,13 @@ import antfu from '@antfu/eslint-config' export default antfu({ - ignores: ['assets/'], + ignores: [ + 'assets/', + 'node_modules/', + 'dist/', + ], typescript: true, + gitignore: false, rules: { 'curly': ['error', 'multi-line'], 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], diff --git a/scripts/misc/shikimori/utils.ts b/scripts/misc/shikimori/utils.ts index e25dc28..27cc5d5 100644 --- a/scripts/misc/shikimori/utils.ts +++ b/scripts/misc/shikimori/utils.ts @@ -12,28 +12,4 @@ export const ffetchShiki = ffetchBase.extend({ ...(rateLimitBypass as any), }) -export function counterIter(start = 0, end = Infinity) { - let i = start - let ended = false - - const iter: IterableIterator = { - [Symbol.iterator]: () => iter, - next() { - if (ended) { - return { value: undefined, done: true } - } - - if (i > end) { - return { value: undefined, done: true } - } - - return { value: i++, done: false } - }, - } - return { - iter, - end: () => { - ended = true - }, - } -} +export { counterIter } from '../../../utils/counter.ts' diff --git a/utils/counter.ts b/utils/counter.ts new file mode 100644 index 0000000..7c7c6fd --- /dev/null +++ b/utils/counter.ts @@ -0,0 +1,25 @@ +export function counterIter(start = 0, end = Infinity) { + let i = start + let ended = false + + const iter: IterableIterator = { + [Symbol.iterator]: () => iter, + next() { + if (ended) { + return { value: undefined, done: true } + } + + if (i > end) { + return { value: undefined, done: true } + } + + return { value: i++, done: false } + }, + } + return { + iter, + end: () => { + ended = true + }, + } +} From 655e5a8e154b6d9354b911704617241fd41e40d0 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 8 Mar 2025 08:19:53 +0000 Subject: [PATCH 26/61] chore: update public repo --- eslint.config.js | 7 ++++++- scripts/misc/shikimori/utils.ts | 26 +------------------------- utils/counter.ts | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 utils/counter.ts diff --git a/eslint.config.js b/eslint.config.js index a159663..81e0f5d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,8 +1,13 @@ import antfu from '@antfu/eslint-config' export default antfu({ - ignores: ['assets/'], + ignores: [ + 'assets/', + 'node_modules/', + 'dist/', + ], typescript: true, + gitignore: false, rules: { 'curly': ['error', 'multi-line'], 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], diff --git a/scripts/misc/shikimori/utils.ts b/scripts/misc/shikimori/utils.ts index e25dc28..27cc5d5 100644 --- a/scripts/misc/shikimori/utils.ts +++ b/scripts/misc/shikimori/utils.ts @@ -12,28 +12,4 @@ export const ffetchShiki = ffetchBase.extend({ ...(rateLimitBypass as any), }) -export function counterIter(start = 0, end = Infinity) { - let i = start - let ended = false - - const iter: IterableIterator = { - [Symbol.iterator]: () => iter, - next() { - if (ended) { - return { value: undefined, done: true } - } - - if (i > end) { - return { value: undefined, done: true } - } - - return { value: i++, done: false } - }, - } - return { - iter, - end: () => { - ended = true - }, - } -} +export { counterIter } from '../../../utils/counter.ts' diff --git a/utils/counter.ts b/utils/counter.ts new file mode 100644 index 0000000..7c7c6fd --- /dev/null +++ b/utils/counter.ts @@ -0,0 +1,25 @@ +export function counterIter(start = 0, end = Infinity) { + let i = start + let ended = false + + const iter: IterableIterator = { + [Symbol.iterator]: () => iter, + next() { + if (ended) { + return { value: undefined, done: true } + } + + if (i > end) { + return { value: undefined, done: true } + } + + return { value: i++, done: false } + }, + } + return { + iter, + end: () => { + ended = true + }, + } +} From c3d03874e75df962dcd3f530de50614baf02a81e Mon Sep 17 00:00:00 2001 From: desu-bot Date: Fri, 14 Mar 2025 10:14:10 +0000 Subject: [PATCH 27/61] chore: update public repo From a7f1118602d4e9ea3a589b3ad23754b18ec0fef9 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Fri, 14 Mar 2025 10:14:10 +0000 Subject: [PATCH 28/61] chore: update public repo From 6cf10f04a54ac37f32e11b4ae494cce90998a3ea Mon Sep 17 00:00:00 2001 From: desu-bot Date: Tue, 25 Mar 2025 02:39:46 +0000 Subject: [PATCH 29/61] chore: update public repo --- utils/fetch.ts | 1 + utils/whoisxmlapi.ts | 109 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 utils/whoisxmlapi.ts diff --git a/utils/fetch.ts b/utils/fetch.ts index bd33dee..f892306 100644 --- a/utils/fetch.ts +++ b/utils/fetch.ts @@ -21,6 +21,7 @@ const cheerioAddon: FfetchAddon Promise }> export const ffetch = ffetchBase.extend({ addons: [ ffetchAddons.parser(ffetchZodAdapter()), + ffetchAddons.rateLimitHandler(), cheerioAddon, toughCookieAddon(), ], diff --git a/utils/whoisxmlapi.ts b/utils/whoisxmlapi.ts new file mode 100644 index 0000000..412ea2c --- /dev/null +++ b/utils/whoisxmlapi.ts @@ -0,0 +1,109 @@ +import { sleep } from '@fuman/utils' +import { z } from 'zod' +import { ffetch } from './fetch.ts' +import { getEnv } from './misc.ts' + +// https://whois.whoisxmlapi.com/documentation/output +// not all fields are present currently +const WhoisResultSchema = z.object({ + registrant: z.object({ + organization: z.string(), + name: z.string(), + email: z.string(), + }).partial().optional(), + administrativeContact: z.object({ + organization: z.string(), + name: z.string(), + email: z.string(), + }).partial().optional(), + technicalContact: z.object({ + organization: z.string(), + name: z.string(), + email: z.string(), + }).partial().optional(), + registryData: z.object({ + registrant: z.object({ + organization: z.string(), + name: z.string(), + email: z.string(), + }).partial().optional(), + registrarName: z.string(), + createdDate: z.string(), + updatedDate: z.string(), + expiresDate: z.string(), + }).partial().optional(), + registrarName: z.string().optional(), + createdDate: z.string().optional(), + updatedDate: z.string().optional(), + expiresDate: z.string().optional(), +}) +export type WhoisResult = z.infer + +const WhoisWrapSchema = z.object({ + domainName: z.string(), + domainStatus: z.enum(['I', 'N']), + whoisRecord: WhoisResultSchema.optional(), +}) + +export async function bulkWhois(domains: string[]) { + const res = await ffetch.post('https://www.whoisxmlapi.com/BulkWhoisLookup/bulkServices/bulkWhois', { + json: { + apiKey: getEnv('WHOISXMLAPI_TOKEN'), + domains, + outputFormat: 'JSON', + }, + }).parsedJson(z.object({ + requestId: z.string(), + })) + + while (true) { + const res2 = await ffetch.post('https://www.whoisxmlapi.com/BulkWhoisLookup/bulkServices/getRecords', { + json: { + apiKey: getEnv('WHOISXMLAPI_KEY'), + requestId: res.requestId, + outputFormat: 'JSON', + maxRecords: 1, + }, + }).parsedJson(z.object({ + recordsLeft: z.number(), + })) + + if (res2.recordsLeft !== 0) { + await sleep(1000) + continue + } + + break + } + + const result = new Map() + const finalRes = await ffetch.post('https://www.whoisxmlapi.com/BulkWhoisLookup/bulkServices/getRecords', { + json: { + apiKey: getEnv('WHOISXMLAPI_KEY'), + requestId: res.requestId, + outputFormat: 'JSON', + }, + }).parsedJson(z.object({ + whoisRecords: z.array(WhoisWrapSchema), + })) + + for (const record of finalRes.whoisRecords) { + result.set(record.domainName, record.domainStatus === 'I' ? record.whoisRecord ?? null : null) + } + + return result +} + +export async function whois(domain: string) { + const res = await ffetch.post('https://www.whoisxmlapi.com/whoisserver/WhoisService', { + json: { + domainName: domain, + outputFormat: 'JSON', + apiKey: getEnv('WHOISXMLAPI_TOKEN'), + }, + }).parsedJson(z.object({ + WhoisRecord: WhoisResultSchema.optional(), + })) + + return res.WhoisRecord ?? null +} From 874e1952e3fc1a7ee5f76b853be0b6f2f065c3b4 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Tue, 25 Mar 2025 02:39:46 +0000 Subject: [PATCH 30/61] chore: update public repo --- utils/fetch.ts | 1 + utils/whoisxmlapi.ts | 109 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 utils/whoisxmlapi.ts diff --git a/utils/fetch.ts b/utils/fetch.ts index bd33dee..f892306 100644 --- a/utils/fetch.ts +++ b/utils/fetch.ts @@ -21,6 +21,7 @@ const cheerioAddon: FfetchAddon Promise }> export const ffetch = ffetchBase.extend({ addons: [ ffetchAddons.parser(ffetchZodAdapter()), + ffetchAddons.rateLimitHandler(), cheerioAddon, toughCookieAddon(), ], diff --git a/utils/whoisxmlapi.ts b/utils/whoisxmlapi.ts new file mode 100644 index 0000000..412ea2c --- /dev/null +++ b/utils/whoisxmlapi.ts @@ -0,0 +1,109 @@ +import { sleep } from '@fuman/utils' +import { z } from 'zod' +import { ffetch } from './fetch.ts' +import { getEnv } from './misc.ts' + +// https://whois.whoisxmlapi.com/documentation/output +// not all fields are present currently +const WhoisResultSchema = z.object({ + registrant: z.object({ + organization: z.string(), + name: z.string(), + email: z.string(), + }).partial().optional(), + administrativeContact: z.object({ + organization: z.string(), + name: z.string(), + email: z.string(), + }).partial().optional(), + technicalContact: z.object({ + organization: z.string(), + name: z.string(), + email: z.string(), + }).partial().optional(), + registryData: z.object({ + registrant: z.object({ + organization: z.string(), + name: z.string(), + email: z.string(), + }).partial().optional(), + registrarName: z.string(), + createdDate: z.string(), + updatedDate: z.string(), + expiresDate: z.string(), + }).partial().optional(), + registrarName: z.string().optional(), + createdDate: z.string().optional(), + updatedDate: z.string().optional(), + expiresDate: z.string().optional(), +}) +export type WhoisResult = z.infer + +const WhoisWrapSchema = z.object({ + domainName: z.string(), + domainStatus: z.enum(['I', 'N']), + whoisRecord: WhoisResultSchema.optional(), +}) + +export async function bulkWhois(domains: string[]) { + const res = await ffetch.post('https://www.whoisxmlapi.com/BulkWhoisLookup/bulkServices/bulkWhois', { + json: { + apiKey: getEnv('WHOISXMLAPI_TOKEN'), + domains, + outputFormat: 'JSON', + }, + }).parsedJson(z.object({ + requestId: z.string(), + })) + + while (true) { + const res2 = await ffetch.post('https://www.whoisxmlapi.com/BulkWhoisLookup/bulkServices/getRecords', { + json: { + apiKey: getEnv('WHOISXMLAPI_KEY'), + requestId: res.requestId, + outputFormat: 'JSON', + maxRecords: 1, + }, + }).parsedJson(z.object({ + recordsLeft: z.number(), + })) + + if (res2.recordsLeft !== 0) { + await sleep(1000) + continue + } + + break + } + + const result = new Map() + const finalRes = await ffetch.post('https://www.whoisxmlapi.com/BulkWhoisLookup/bulkServices/getRecords', { + json: { + apiKey: getEnv('WHOISXMLAPI_KEY'), + requestId: res.requestId, + outputFormat: 'JSON', + }, + }).parsedJson(z.object({ + whoisRecords: z.array(WhoisWrapSchema), + })) + + for (const record of finalRes.whoisRecords) { + result.set(record.domainName, record.domainStatus === 'I' ? record.whoisRecord ?? null : null) + } + + return result +} + +export async function whois(domain: string) { + const res = await ffetch.post('https://www.whoisxmlapi.com/whoisserver/WhoisService', { + json: { + domainName: domain, + outputFormat: 'JSON', + apiKey: getEnv('WHOISXMLAPI_TOKEN'), + }, + }).parsedJson(z.object({ + WhoisRecord: WhoisResultSchema.optional(), + })) + + return res.WhoisRecord ?? null +} From 33adc5f4edde2796c1061612469cc68c65afc94a Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 9 Apr 2025 04:20:08 +0000 Subject: [PATCH 31/61] chore: update public repo --- scripts/infra/navidrome-find-duplicates.ts | 31 ++++++-------- scripts/infra/navidrome-stats.ts | 18 +++++++++ utils/navidrome.ts | 47 ++++++++++++++++++---- 3 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 scripts/infra/navidrome-stats.ts diff --git a/scripts/infra/navidrome-find-duplicates.ts b/scripts/infra/navidrome-find-duplicates.ts index 3c89971..c5af85b 100644 --- a/scripts/infra/navidrome-find-duplicates.ts +++ b/scripts/infra/navidrome-find-duplicates.ts @@ -5,7 +5,7 @@ import { join } from 'node:path' import kuromoji from 'kuromoji' import { isKana, toRomaji } from 'wanakana' -import { fetchSongs, navidromeFfetch as ffetch } from '../../utils/navidrome.ts' +import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' const WHITELIST_KEYS = new Set([ // actual different tracks with the same title @@ -53,8 +53,6 @@ function clean(s: string) { return str } -const CHUNK_SIZE = 1000 - function getSongKey(song: NavidromeSong) { return JSON.stringify([ clean(song.artist), @@ -64,23 +62,20 @@ function getSongKey(song: NavidromeSong) { const seen = new Map() -for (let offset = 0; ; offset += CHUNK_SIZE) { - const songs = await fetchSongs(offset, CHUNK_SIZE) - if (songs.length === 0) break - - for (const song of songs) { - const key = getSongKey(song) - if (WHITELIST_KEYS.has(key)) continue - let arr = seen.get(key) - if (!arr) { - arr = [] - seen.set(key, arr) - } - - arr.push(song) +for await (const song of fetchSongsIter({ + onChunkProcessed: (page, items) => { + console.log('āŒ› fetched chunk %d (%d items)', page, items) + }, +})) { + const key = getSongKey(song) + if (WHITELIST_KEYS.has(key)) continue + let arr = seen.get(key) + if (!arr) { + arr = [] + seen.set(key, arr) } - console.log('āŒ› fetched chunk %d (%d items)', Math.floor(offset / CHUNK_SIZE), songs.length) + arr.push(song) } const keysSorted = Array.from(seen.keys()).sort() diff --git a/scripts/infra/navidrome-stats.ts b/scripts/infra/navidrome-stats.ts new file mode 100644 index 0000000..e4bb3fa --- /dev/null +++ b/scripts/infra/navidrome-stats.ts @@ -0,0 +1,18 @@ +import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' + +let count = 0 +let totalSize = 0 +let totalDuration = 0 + +console.log('āŒ› fetching songs...') + +for await (const song of fetchSongsIter()) { + count += 1 + totalSize += song.size + totalDuration += song.duration +} + +console.log('---') +console.log('total songs: %d', count) +console.log('total size: %d GiB', (totalSize / 1024 / 1024 / 1024).toFixed(3)) +console.log('total duration: %d min (%d h)', (totalDuration / 60).toFixed(3), (totalDuration / 60 / 60).toFixed(3)) diff --git a/utils/navidrome.ts b/utils/navidrome.ts index be66efb..5314252 100644 --- a/utils/navidrome.ts +++ b/utils/navidrome.ts @@ -2,12 +2,26 @@ import { z } from 'zod' import { ffetch as ffetchBase } from './fetch.ts' import { getEnv } from './misc.ts' -export const navidromeFfetch = ffetchBase.extend({ - baseUrl: getEnv('NAVIDROME_ENDPOINT'), - headers: { - 'x-nd-authorization': `Bearer ${getEnv('NAVIDROME_TOKEN')}`, - }, -}) +let _cachedFfetch: typeof ffetchBase | undefined +export async function getNavidromeFfetch() { + if (_cachedFfetch) return _cachedFfetch + const baseUrl = getEnv('NAVIDROME_ENDPOINT') + const authRes = await ffetchBase.post('/auth/login', { + baseUrl, + json: { + username: getEnv('NAVIDROME_USERNAME'), + password: getEnv('NAVIDROME_PASSWORD'), + }, + }).parsedJson(z.object({ token: z.string() })) + + _cachedFfetch = ffetchBase.extend({ + baseUrl, + headers: { + 'x-nd-authorization': `Bearer ${authRes.token}`, + }, + }) + return _cachedFfetch +} export const NavidromeSong = z.object({ id: z.string(), @@ -17,11 +31,13 @@ export const NavidromeSong = z.object({ artist: z.string(), path: z.string(), duration: z.number(), + size: z.number(), }) export type NavidromeSong = z.infer -export function fetchSongs(offset: number, pageSize: number) { - return navidromeFfetch('/api/song', { +export async function fetchSongs(offset: number, pageSize: number) { + const api = await getNavidromeFfetch() + return api('/api/song', { query: { _start: offset, _end: offset + pageSize, @@ -30,3 +46,18 @@ export function fetchSongs(offset: number, pageSize: number) { }, }).parsedJson(z.array(NavidromeSong)) } + +export async function* fetchSongsIter(params?: { + chunkSize?: number + onChunkProcessed?: (page: number, items: number) => void +}) { + const { chunkSize = 1000, onChunkProcessed } = params ?? {} + for (let offset = 0; ; offset += chunkSize) { + const songs = await fetchSongs(offset, chunkSize) + if (songs.length === 0) return + + yield * songs + + onChunkProcessed?.(Math.floor(offset / chunkSize), songs.length) + } +} From ce9f435ef201f9f8c25932474484f8f7023ea930 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 9 Apr 2025 04:20:08 +0000 Subject: [PATCH 32/61] chore: update public repo --- scripts/infra/navidrome-find-duplicates.ts | 31 ++++++-------- scripts/infra/navidrome-stats.ts | 18 +++++++++ utils/navidrome.ts | 47 ++++++++++++++++++---- 3 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 scripts/infra/navidrome-stats.ts diff --git a/scripts/infra/navidrome-find-duplicates.ts b/scripts/infra/navidrome-find-duplicates.ts index 3c89971..c5af85b 100644 --- a/scripts/infra/navidrome-find-duplicates.ts +++ b/scripts/infra/navidrome-find-duplicates.ts @@ -5,7 +5,7 @@ import { join } from 'node:path' import kuromoji from 'kuromoji' import { isKana, toRomaji } from 'wanakana' -import { fetchSongs, navidromeFfetch as ffetch } from '../../utils/navidrome.ts' +import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' const WHITELIST_KEYS = new Set([ // actual different tracks with the same title @@ -53,8 +53,6 @@ function clean(s: string) { return str } -const CHUNK_SIZE = 1000 - function getSongKey(song: NavidromeSong) { return JSON.stringify([ clean(song.artist), @@ -64,23 +62,20 @@ function getSongKey(song: NavidromeSong) { const seen = new Map() -for (let offset = 0; ; offset += CHUNK_SIZE) { - const songs = await fetchSongs(offset, CHUNK_SIZE) - if (songs.length === 0) break - - for (const song of songs) { - const key = getSongKey(song) - if (WHITELIST_KEYS.has(key)) continue - let arr = seen.get(key) - if (!arr) { - arr = [] - seen.set(key, arr) - } - - arr.push(song) +for await (const song of fetchSongsIter({ + onChunkProcessed: (page, items) => { + console.log('āŒ› fetched chunk %d (%d items)', page, items) + }, +})) { + const key = getSongKey(song) + if (WHITELIST_KEYS.has(key)) continue + let arr = seen.get(key) + if (!arr) { + arr = [] + seen.set(key, arr) } - console.log('āŒ› fetched chunk %d (%d items)', Math.floor(offset / CHUNK_SIZE), songs.length) + arr.push(song) } const keysSorted = Array.from(seen.keys()).sort() diff --git a/scripts/infra/navidrome-stats.ts b/scripts/infra/navidrome-stats.ts new file mode 100644 index 0000000..e4bb3fa --- /dev/null +++ b/scripts/infra/navidrome-stats.ts @@ -0,0 +1,18 @@ +import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' + +let count = 0 +let totalSize = 0 +let totalDuration = 0 + +console.log('āŒ› fetching songs...') + +for await (const song of fetchSongsIter()) { + count += 1 + totalSize += song.size + totalDuration += song.duration +} + +console.log('---') +console.log('total songs: %d', count) +console.log('total size: %d GiB', (totalSize / 1024 / 1024 / 1024).toFixed(3)) +console.log('total duration: %d min (%d h)', (totalDuration / 60).toFixed(3), (totalDuration / 60 / 60).toFixed(3)) diff --git a/utils/navidrome.ts b/utils/navidrome.ts index be66efb..5314252 100644 --- a/utils/navidrome.ts +++ b/utils/navidrome.ts @@ -2,12 +2,26 @@ import { z } from 'zod' import { ffetch as ffetchBase } from './fetch.ts' import { getEnv } from './misc.ts' -export const navidromeFfetch = ffetchBase.extend({ - baseUrl: getEnv('NAVIDROME_ENDPOINT'), - headers: { - 'x-nd-authorization': `Bearer ${getEnv('NAVIDROME_TOKEN')}`, - }, -}) +let _cachedFfetch: typeof ffetchBase | undefined +export async function getNavidromeFfetch() { + if (_cachedFfetch) return _cachedFfetch + const baseUrl = getEnv('NAVIDROME_ENDPOINT') + const authRes = await ffetchBase.post('/auth/login', { + baseUrl, + json: { + username: getEnv('NAVIDROME_USERNAME'), + password: getEnv('NAVIDROME_PASSWORD'), + }, + }).parsedJson(z.object({ token: z.string() })) + + _cachedFfetch = ffetchBase.extend({ + baseUrl, + headers: { + 'x-nd-authorization': `Bearer ${authRes.token}`, + }, + }) + return _cachedFfetch +} export const NavidromeSong = z.object({ id: z.string(), @@ -17,11 +31,13 @@ export const NavidromeSong = z.object({ artist: z.string(), path: z.string(), duration: z.number(), + size: z.number(), }) export type NavidromeSong = z.infer -export function fetchSongs(offset: number, pageSize: number) { - return navidromeFfetch('/api/song', { +export async function fetchSongs(offset: number, pageSize: number) { + const api = await getNavidromeFfetch() + return api('/api/song', { query: { _start: offset, _end: offset + pageSize, @@ -30,3 +46,18 @@ export function fetchSongs(offset: number, pageSize: number) { }, }).parsedJson(z.array(NavidromeSong)) } + +export async function* fetchSongsIter(params?: { + chunkSize?: number + onChunkProcessed?: (page: number, items: number) => void +}) { + const { chunkSize = 1000, onChunkProcessed } = params ?? {} + for (let offset = 0; ; offset += chunkSize) { + const songs = await fetchSongs(offset, chunkSize) + if (songs.length === 0) return + + yield * songs + + onChunkProcessed?.(Math.floor(offset / chunkSize), songs.length) + } +} From cd9b06512f1633a4cc5bb4c521d6ddaae04c8ee3 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sun, 27 Apr 2025 22:31:46 +0000 Subject: [PATCH 33/61] chore: update public repo --- scripts/misc/hits-nakrutka.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 scripts/misc/hits-nakrutka.ts diff --git a/scripts/misc/hits-nakrutka.ts b/scripts/misc/hits-nakrutka.ts new file mode 100644 index 0000000..3a19643 --- /dev/null +++ b/scripts/misc/hits-nakrutka.ts @@ -0,0 +1,17 @@ +import { asyncPool } from '@fuman/utils' +import { argv, question } from 'zx' +import { counterIter } from '../../utils/counter.ts' + +const url = argv[1] || await question('url > ') +const count = argv[2] || await question('count > ') + +const counter = counterIter(0, count) + +await asyncPool(counter.iter, async (i) => { + if (i % 100 === 0) { + console.log('currently at %d', i) + } + await fetch(url) +}) + +console.log('nakrutka done') From 96426d01c1c0d3d3b766afabf9892d93c9002808 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sun, 27 Apr 2025 22:31:46 +0000 Subject: [PATCH 34/61] chore: update public repo --- scripts/misc/hits-nakrutka.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 scripts/misc/hits-nakrutka.ts diff --git a/scripts/misc/hits-nakrutka.ts b/scripts/misc/hits-nakrutka.ts new file mode 100644 index 0000000..3a19643 --- /dev/null +++ b/scripts/misc/hits-nakrutka.ts @@ -0,0 +1,17 @@ +import { asyncPool } from '@fuman/utils' +import { argv, question } from 'zx' +import { counterIter } from '../../utils/counter.ts' + +const url = argv[1] || await question('url > ') +const count = argv[2] || await question('count > ') + +const counter = counterIter(0, count) + +await asyncPool(counter.iter, async (i) => { + if (i % 100 === 0) { + console.log('currently at %d', i) + } + await fetch(url) +}) + +console.log('nakrutka done') From df138936b131309a535583f88c143d83d8f58055 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Fri, 9 May 2025 06:46:50 +0000 Subject: [PATCH 35/61] chore: update public repo --- scripts/infra/navidrome/find-broken.ts | 42 +++++++++++++++++++ .../find-duplicates.ts} | 4 +- .../remux-m4a.ts} | 8 ++-- .../stats.ts} | 2 +- utils/navidrome.ts | 1 + 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 scripts/infra/navidrome/find-broken.ts rename scripts/infra/{navidrome-find-duplicates.ts => navidrome/find-duplicates.ts} (95%) rename scripts/infra/{navidrome-remux-m4a.ts => navidrome/remux-m4a.ts} (90%) rename scripts/infra/{navidrome-stats.ts => navidrome/stats.ts} (86%) diff --git a/scripts/infra/navidrome/find-broken.ts b/scripts/infra/navidrome/find-broken.ts new file mode 100644 index 0000000..190b406 --- /dev/null +++ b/scripts/infra/navidrome/find-broken.ts @@ -0,0 +1,42 @@ +import { $, ProcessOutput } from 'zx' +import { fetchSongsIter } from '../../../utils/navidrome.ts' +import { asyncPool } from '@fuman/utils' +import { join } from 'path/posix' + +// async function checkIfBroken(path: string) { +// const r = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${path}`.json() +// } + +// for await (const song of fetchSongsIter()) { + +// } + +const broken: string[] = [] + +await asyncPool(fetchSongsIter({ + onChunkProcessed: (page, items) => { + console.log(`Processed page ${page} with ${items} items`) + }, +}), async (song) => { + const fullPath = join(song.libraryPath, song.path) + const path = fullPath.replace('/music/s3/', '/mnt/tank/enc/media/music/') + try { + const r = await $`ffmpeg -v error -i ${path} -f null -`.quiet() + if (r.exitCode !== 0 || r.stderr.trim() !== '') throw r + } catch (e) { + if (!(e instanceof ProcessOutput)) throw e + + console.log('%s - %s (%s) seems broken:', song.artist, song.title, path) + console.log(e.stderr) + broken.push(path) + } +}, { limit: 8 }) + + +if (broken.length > 0) { + console.log('Found %d broken files:', broken.length) + for (const path of broken) { + console.log(' %s', path) + } + process.exit(1) +} \ No newline at end of file diff --git a/scripts/infra/navidrome-find-duplicates.ts b/scripts/infra/navidrome/find-duplicates.ts similarity index 95% rename from scripts/infra/navidrome-find-duplicates.ts rename to scripts/infra/navidrome/find-duplicates.ts index c5af85b..7732caa 100644 --- a/scripts/infra/navidrome-find-duplicates.ts +++ b/scripts/infra/navidrome/find-duplicates.ts @@ -1,11 +1,11 @@ -import type { NavidromeSong } from '../../utils/navidrome.ts' +import type { NavidromeSong } from '../../../utils/navidrome.ts' import { createRequire } from 'node:module' import { join } from 'node:path' import kuromoji from 'kuromoji' import { isKana, toRomaji } from 'wanakana' -import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' +import { fetchSongsIter } from '../../../utils/navidrome.ts' const WHITELIST_KEYS = new Set([ // actual different tracks with the same title diff --git a/scripts/infra/navidrome-remux-m4a.ts b/scripts/infra/navidrome/remux-m4a.ts similarity index 90% rename from scripts/infra/navidrome-remux-m4a.ts rename to scripts/infra/navidrome/remux-m4a.ts index 92b1e30..df77031 100644 --- a/scripts/infra/navidrome-remux-m4a.ts +++ b/scripts/infra/navidrome/remux-m4a.ts @@ -1,10 +1,10 @@ import { readFile, rm } from 'node:fs/promises' import { join } from 'node:path' import { $ } from 'zx' -import { downloadStream } from '../../utils/fetch.ts' -import { getEnv } from '../../utils/misc.ts' -import { fetchSongs } from '../../utils/navidrome.ts' -import { WebdavClient } from '../../utils/webdav.ts' +import { downloadStream } from '../../../utils/fetch.ts' +import { getEnv } from '../../../utils/misc.ts' +import { fetchSongs } from '../../../utils/navidrome.ts' +import { WebdavClient } from '../../../utils/webdav.ts' const webdav = new WebdavClient({ baseUrl: getEnv('NAVIDROME_WEBDAV_ENDPOINT'), diff --git a/scripts/infra/navidrome-stats.ts b/scripts/infra/navidrome/stats.ts similarity index 86% rename from scripts/infra/navidrome-stats.ts rename to scripts/infra/navidrome/stats.ts index e4bb3fa..906da06 100644 --- a/scripts/infra/navidrome-stats.ts +++ b/scripts/infra/navidrome/stats.ts @@ -1,4 +1,4 @@ -import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' +import { fetchSongs, fetchSongsIter } from '../../../utils/navidrome.ts' let count = 0 let totalSize = 0 diff --git a/utils/navidrome.ts b/utils/navidrome.ts index 5314252..d99477d 100644 --- a/utils/navidrome.ts +++ b/utils/navidrome.ts @@ -30,6 +30,7 @@ export const NavidromeSong = z.object({ albumArtist: z.string(), artist: z.string(), path: z.string(), + libraryPath: z.string(), duration: z.number(), size: z.number(), }) From 68a2d17239fa8412702e3acbd44def5a43a3d689 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Fri, 9 May 2025 06:46:50 +0000 Subject: [PATCH 36/61] chore: update public repo --- scripts/infra/navidrome/find-broken.ts | 42 +++++++++++++++++++ .../find-duplicates.ts} | 4 +- .../remux-m4a.ts} | 8 ++-- .../stats.ts} | 2 +- utils/navidrome.ts | 1 + 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 scripts/infra/navidrome/find-broken.ts rename scripts/infra/{navidrome-find-duplicates.ts => navidrome/find-duplicates.ts} (95%) rename scripts/infra/{navidrome-remux-m4a.ts => navidrome/remux-m4a.ts} (90%) rename scripts/infra/{navidrome-stats.ts => navidrome/stats.ts} (86%) diff --git a/scripts/infra/navidrome/find-broken.ts b/scripts/infra/navidrome/find-broken.ts new file mode 100644 index 0000000..190b406 --- /dev/null +++ b/scripts/infra/navidrome/find-broken.ts @@ -0,0 +1,42 @@ +import { $, ProcessOutput } from 'zx' +import { fetchSongsIter } from '../../../utils/navidrome.ts' +import { asyncPool } from '@fuman/utils' +import { join } from 'path/posix' + +// async function checkIfBroken(path: string) { +// const r = await $`ffprobe -v error -show_entries stream=codec_type,codec_name,index:stream_tags=title,language -of json ${path}`.json() +// } + +// for await (const song of fetchSongsIter()) { + +// } + +const broken: string[] = [] + +await asyncPool(fetchSongsIter({ + onChunkProcessed: (page, items) => { + console.log(`Processed page ${page} with ${items} items`) + }, +}), async (song) => { + const fullPath = join(song.libraryPath, song.path) + const path = fullPath.replace('/music/s3/', '/mnt/tank/enc/media/music/') + try { + const r = await $`ffmpeg -v error -i ${path} -f null -`.quiet() + if (r.exitCode !== 0 || r.stderr.trim() !== '') throw r + } catch (e) { + if (!(e instanceof ProcessOutput)) throw e + + console.log('%s - %s (%s) seems broken:', song.artist, song.title, path) + console.log(e.stderr) + broken.push(path) + } +}, { limit: 8 }) + + +if (broken.length > 0) { + console.log('Found %d broken files:', broken.length) + for (const path of broken) { + console.log(' %s', path) + } + process.exit(1) +} \ No newline at end of file diff --git a/scripts/infra/navidrome-find-duplicates.ts b/scripts/infra/navidrome/find-duplicates.ts similarity index 95% rename from scripts/infra/navidrome-find-duplicates.ts rename to scripts/infra/navidrome/find-duplicates.ts index c5af85b..7732caa 100644 --- a/scripts/infra/navidrome-find-duplicates.ts +++ b/scripts/infra/navidrome/find-duplicates.ts @@ -1,11 +1,11 @@ -import type { NavidromeSong } from '../../utils/navidrome.ts' +import type { NavidromeSong } from '../../../utils/navidrome.ts' import { createRequire } from 'node:module' import { join } from 'node:path' import kuromoji from 'kuromoji' import { isKana, toRomaji } from 'wanakana' -import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' +import { fetchSongsIter } from '../../../utils/navidrome.ts' const WHITELIST_KEYS = new Set([ // actual different tracks with the same title diff --git a/scripts/infra/navidrome-remux-m4a.ts b/scripts/infra/navidrome/remux-m4a.ts similarity index 90% rename from scripts/infra/navidrome-remux-m4a.ts rename to scripts/infra/navidrome/remux-m4a.ts index 92b1e30..df77031 100644 --- a/scripts/infra/navidrome-remux-m4a.ts +++ b/scripts/infra/navidrome/remux-m4a.ts @@ -1,10 +1,10 @@ import { readFile, rm } from 'node:fs/promises' import { join } from 'node:path' import { $ } from 'zx' -import { downloadStream } from '../../utils/fetch.ts' -import { getEnv } from '../../utils/misc.ts' -import { fetchSongs } from '../../utils/navidrome.ts' -import { WebdavClient } from '../../utils/webdav.ts' +import { downloadStream } from '../../../utils/fetch.ts' +import { getEnv } from '../../../utils/misc.ts' +import { fetchSongs } from '../../../utils/navidrome.ts' +import { WebdavClient } from '../../../utils/webdav.ts' const webdav = new WebdavClient({ baseUrl: getEnv('NAVIDROME_WEBDAV_ENDPOINT'), diff --git a/scripts/infra/navidrome-stats.ts b/scripts/infra/navidrome/stats.ts similarity index 86% rename from scripts/infra/navidrome-stats.ts rename to scripts/infra/navidrome/stats.ts index e4bb3fa..906da06 100644 --- a/scripts/infra/navidrome-stats.ts +++ b/scripts/infra/navidrome/stats.ts @@ -1,4 +1,4 @@ -import { fetchSongs, fetchSongsIter } from '../../utils/navidrome.ts' +import { fetchSongs, fetchSongsIter } from '../../../utils/navidrome.ts' let count = 0 let totalSize = 0 diff --git a/utils/navidrome.ts b/utils/navidrome.ts index 5314252..d99477d 100644 --- a/utils/navidrome.ts +++ b/utils/navidrome.ts @@ -30,6 +30,7 @@ export const NavidromeSong = z.object({ albumArtist: z.string(), artist: z.string(), path: z.string(), + libraryPath: z.string(), duration: z.number(), size: z.number(), }) From 910d31c9abdab6d795722be689cdb2753ee7f86d Mon Sep 17 00:00:00 2001 From: desu-bot Date: Fri, 9 May 2025 06:48:15 +0000 Subject: [PATCH 37/61] chore: update public repo --- scripts/infra/navidrome/find-broken.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/infra/navidrome/find-broken.ts b/scripts/infra/navidrome/find-broken.ts index 190b406..3fed889 100644 --- a/scripts/infra/navidrome/find-broken.ts +++ b/scripts/infra/navidrome/find-broken.ts @@ -35,7 +35,7 @@ await asyncPool(fetchSongsIter({ if (broken.length > 0) { console.log('Found %d broken files:', broken.length) - for (const path of broken) { + for (const path of broken.sort()) { console.log(' %s', path) } process.exit(1) From a2b73a99820e9766f75252abea81bbd39a947baa Mon Sep 17 00:00:00 2001 From: desu-bot Date: Fri, 9 May 2025 06:48:15 +0000 Subject: [PATCH 38/61] chore: update public repo --- scripts/infra/navidrome/find-broken.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/infra/navidrome/find-broken.ts b/scripts/infra/navidrome/find-broken.ts index 190b406..3fed889 100644 --- a/scripts/infra/navidrome/find-broken.ts +++ b/scripts/infra/navidrome/find-broken.ts @@ -35,7 +35,7 @@ await asyncPool(fetchSongsIter({ if (broken.length > 0) { console.log('Found %d broken files:', broken.length) - for (const path of broken) { + for (const path of broken.sort()) { console.log(' %s', path) } process.exit(1) From 4645c0253f37196e9ddfc6d8eb949fdf94e2144d Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 10 May 2025 21:06:34 +0000 Subject: [PATCH 39/61] chore: update public repo --- package.json | 8 +- pnpm-lock.yaml | 33 ++- scripts/auth/mtcute-login.ts | 3 + scripts/media/deezer-dl.ts | 516 +++++++++++++++++++++++++++++++++++ 4 files changed, 541 insertions(+), 19 deletions(-) create mode 100644 scripts/media/deezer-dl.ts diff --git a/package.json b/package.json index 1ecf24e..3269071 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,6 @@ "name": "teidesu-scripts", "type": "module", "packageManager": "pnpm@9.5.0", - "peerDependencies": { - "typescript": "^5.0.0" - }, "dependencies": { "@faker-js/faker": "^9.3.0", "@fuman/io": "^0.0.4", @@ -17,6 +14,7 @@ "better-sqlite3": "^11.8.1", "canvas": "^3.1.0", "cheerio": "^1.0.0", + "egoroof-blowfish": "4.0.1", "es-main": "^1.3.0", "filesize": "^10.1.6", "json5": "^2.2.3", @@ -33,8 +31,8 @@ }, "devDependencies": { "@antfu/eslint-config": "3.10.0", - "@fuman/fetch": "0.0.10", - "@fuman/utils": "0.0.10", + "@fuman/fetch": "0.1.0", + "@fuman/utils": "0.0.14", "@types/node": "22.10.0", "domhandler": "^5.0.3", "dotenv": "16.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef97f05..871673a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: cheerio: specifier: ^1.0.0 version: 1.0.0 + egoroof-blowfish: + specifier: 4.0.1 + version: 4.0.1 es-main: specifier: ^1.3.0 version: 1.3.0 @@ -74,9 +77,6 @@ importers: tsx: specifier: ^4.19.2 version: 4.19.2 - typescript: - specifier: ^5.0.0 - version: 5.7.2 undici: specifier: ^7.2.0 version: 7.2.0 @@ -88,11 +88,11 @@ importers: specifier: 3.10.0 version: 3.10.0(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0)(typescript@5.7.2) '@fuman/fetch': - specifier: 0.0.10 - version: 0.0.10(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) + specifier: 0.1.0 + version: 0.1.0(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) '@fuman/utils': - specifier: 0.0.10 - version: 0.0.10 + specifier: 0.0.14 + version: 0.0.14 '@types/node': specifier: 22.10.0 version: 22.10.0 @@ -406,8 +406,8 @@ packages: resolution: {integrity: sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} - '@fuman/fetch@0.0.10': - resolution: {integrity: sha512-Zy1GKZ8/NGP2adH0JXVaP91TZPpXngZDTGOFrb38pEp6RYAVSywt0yOPO2kwZGUpordfCj96CMNv+e6zUc7qqw==} + '@fuman/fetch@0.1.0': + resolution: {integrity: sha512-q3CWQJ939Kcmrp/9/dUOCRqQi2og4uweSy7QrzeIxKRsz3jYSca1q2mwTQtTnA+9AAq2WNMlcZtgOA9qVJkXJw==} peerDependencies: '@badrap/valita': '>=0.4.0' tough-cookie: ^5.0.0 || ^4.0.0 @@ -441,8 +441,8 @@ packages: '@fuman/node@0.0.4': resolution: {integrity: sha512-tgwbIceUHWuwh4RTwJRQ1sLjzuIGrWx0SeCrqYhGF+IkI/B7DY0FP2SZykWImkVDtW8IzmdZskPZqiDINRGcNg==} - '@fuman/utils@0.0.10': - resolution: {integrity: sha512-KVlDx0S1Og7IWcPi93f1T45WPfCSUV6/A4dQb36zZRtb8KECl1BK2u9WkNVI+sjrjKCb3xijjY5gq4lS3PqH5g==} + '@fuman/utils@0.0.14': + resolution: {integrity: sha512-HmQo6DXoYBtkN12rZsMSZRTUynByioOHpMZjfrePwUwXuKBYm9F1Rm4R7tGLTNJTG1zMXBJw1Xwy/cRqwzrujw==} '@fuman/utils@0.0.4': resolution: {integrity: sha512-YBZIlGDbM8s9G85pWFZJ9wQrDY4511XLHZ06/uxZfXBY0eSStwje8JFNmRMNF0SjRk4D3iRmPl9wirHKTkg89w==} @@ -906,6 +906,9 @@ packages: doublearray@0.0.2: resolution: {integrity: sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==} + egoroof-blowfish@4.0.1: + resolution: {integrity: sha512-e2gdfyLZrDvyuHX2NkRPFxEuYCIddp+W3MucqjBw9h9nineZUWynuuqQh+R5sNT9IVopPFBHwcnBYHqTcB1Vdw==} + electron-to-chromium@1.5.65: resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==} @@ -2357,9 +2360,9 @@ snapshots: '@faker-js/faker@9.3.0': {} - '@fuman/fetch@0.0.10(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': + '@fuman/fetch@0.1.0(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': dependencies: - '@fuman/utils': 0.0.10 + '@fuman/utils': 0.0.14 optionalDependencies: '@badrap/valita': 0.4.2 tough-cookie: 5.0.0 @@ -2389,7 +2392,7 @@ snapshots: '@fuman/net': 0.0.4 '@fuman/utils': 0.0.4 - '@fuman/utils@0.0.10': {} + '@fuman/utils@0.0.14': {} '@fuman/utils@0.0.4': {} @@ -2920,6 +2923,8 @@ snapshots: doublearray@0.0.2: {} + egoroof-blowfish@4.0.1: {} + electron-to-chromium@1.5.65: {} emoji-regex@8.0.0: {} diff --git a/scripts/auth/mtcute-login.ts b/scripts/auth/mtcute-login.ts index 0bf4af9..7946774 100644 --- a/scripts/auth/mtcute-login.ts +++ b/scripts/auth/mtcute-login.ts @@ -9,6 +9,9 @@ if (!sessionName) { const tg = createTg(sessionName) +await tg.prepare() +await tg.storage.clear(true) + const self = await tg.start({ qrCodeHandler(url, expires) { console.log(qrTerminal.generate(url, { small: true })) diff --git a/scripts/media/deezer-dl.ts b/scripts/media/deezer-dl.ts new file mode 100644 index 0000000..c30dee7 --- /dev/null +++ b/scripts/media/deezer-dl.ts @@ -0,0 +1,516 @@ +import { createHash } from 'node:crypto' +import { mkdir, open, rm, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { Readable } from 'node:stream' +import { TransformStream } from 'node:stream/web' +import { Bytes, read, write } from '@fuman/io' +import { base64, hex, iter, utf8 } from '@fuman/utils' +import { Blowfish } from 'egoroof-blowfish' +import { CookieJar } from 'tough-cookie' +import { FileCookieStore } from 'tough-cookie-file-store' +import { z } from 'zod' +import { $, question } from 'zx' +import { ffetch as ffetchBase } from '../../utils/fetch.ts' +import { sanitizeFilename } from '../../utils/fs.ts' +import { getEnv } from '../../utils/misc.ts' + +const jar = new CookieJar(new FileCookieStore('./assets/deezer-cookies.json')) +await jar.setCookie(`arl=${getEnv('DEEZER_ARL')}; path=/; domain=.deezer.com;`, 'https://www.deezer.com') +await jar.setCookie('comeback=1; path=/; domain=.deezer.com;', 'https://www.deezer.com') + +const ffetch = ffetchBase.extend({ + cookies: jar, + headers: { + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0', + 'accept': '*/*', + 'accept-language': 'en-US,en;q=0.5', + 'accept-encoding': 'gzip, deflate, br', + 'referer': 'https://www.deezer.com/', + 'origin': 'https://www.deezer.com', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + }, +}) + +const GwLightEnvelope = z.object({ + error: z.union([ + z.array(z.unknown()), + z.record(z.unknown()), + ]), + results: z.unknown(), +}) + +async function gwLightApi(params: { + method: string + token?: string + options?: unknown + result: T +}): Promise> { + const { method, token, options, result } = params + const res = await ffetch.post('https://www.deezer.com/ajax/gw-light.php', { + query: { + method, + input: '3', + api_version: '1.0', + api_token: token ?? '', + cid: Math.floor(1000000000 * Math.random()), + }, + json: options ?? {}, + }).parsedJson(GwLightEnvelope) + + if (res.error.length || (typeof res.error === 'object' && !Array.isArray(res.error))) { + throw new Error(JSON.stringify(res.error)) + } + + return result.parse(res.results) +} + +let _cachedToken: [string, number] | undefined +async function getGraphqlToken() { + if (_cachedToken && Date.now() < _cachedToken[1]) return _cachedToken[0] + const hasRefreshToken = (await jar.getCookieString('https://auth.deezer.com')).includes('refresh-token=') + const { jwt } = await ffetch.post(hasRefreshToken ? 'https://auth.deezer.com/login/renew' : 'https://auth.deezer.com/login/arl', { + // i have no idea what these mean + query: { + jo: 'p', + rto: 'c', + i: 'c', + }, + }).parsedJson(z.object({ + jwt: z.string(), + })) + + const expires = JSON.parse(utf8.decoder.decode(base64.decode(jwt.split('.')[1]))).exp * 1000 + _cachedToken = [jwt, expires] + return jwt +} + +async function graphqlApi(params: { + query: string + variables?: unknown + result: T +}): Promise> { + const { query, variables, result } = params + const operationName = query.match(/(?:query|mutation)\s+(\w+)/)?.[1] + + const res = await ffetchBase.post('https://pipe.deezer.com/api', { + json: { + operationName, + query, + variables, + }, + headers: { + Authorization: `Bearer ${await getGraphqlToken()}`, + }, + }).json() + + if (!res.data) { + throw new Error(JSON.stringify(res)) + } + + return result.parse(res.data) +} + +const GwTrack = z.object({ + ALB_ID: z.string(), + ALB_PICTURE: z.string(), + ALB_TITLE: z.string(), + ARTISTS: z.array( + z.object({ + ART_ID: z.string(), + ROLE_ID: z.string(), + ARTISTS_SONGS_ORDER: z.string(), + ART_NAME: z.string(), + ARTIST_IS_DUMMY: z.boolean().optional(), + ART_PICTURE: z.string(), + RANK: z.string(), + }), + ), + ART_ID: z.string(), + ART_NAME: z.string(), + ARTIST_IS_DUMMY: z.boolean().optional(), + DIGITAL_RELEASE_DATE: z.string(), + DISK_NUMBER: z.string(), + DURATION: z.string(), + EXPLICIT_LYRICS: z.string(), + EXPLICIT_TRACK_CONTENT: z.object({ + EXPLICIT_LYRICS_STATUS: z.number(), + EXPLICIT_COVER_STATUS: z.number(), + }), + GENRE_ID: z.string(), + HIERARCHICAL_TITLE: z.string().optional(), + ISRC: z.string(), + LYRICS_ID: z.number(), + PHYSICAL_RELEASE_DATE: z.string(), + PROVIDER_ID: z.string(), + RANK: z.string().optional(), + SMARTRADIO: z.number(), + SNG_CONTRIBUTORS: z.object({ + main_artist: z.array(z.string()), + author: z.array(z.string()), + composer: z.array(z.string()), + }).partial().optional(), + SNG_ID: z.string(), + SNG_TITLE: z.string(), + STATUS: z.number(), + TRACK_NUMBER: z.string(), + USER_ID: z.number(), + VERSION: z.string(), + MD5_ORIGIN: z.string(), + FILESIZE_AAC_64: z.coerce.number(), + FILESIZE_MP3_64: z.coerce.number(), + FILESIZE_MP3_128: z.coerce.number(), + FILESIZE_MP3_256: z.coerce.number(), + FILESIZE_MP3_320: z.coerce.number(), + FILESIZE_MP4_RA1: z.coerce.number(), + FILESIZE_MP4_RA2: z.coerce.number(), + FILESIZE_MP4_RA3: z.coerce.number(), + FILESIZE_FLAC: z.coerce.number(), + FILESIZE: z.coerce.number(), + GAIN: z.nullable(z.coerce.number()), + MEDIA_VERSION: z.string(), + TRACK_TOKEN: z.string(), + TRACK_TOKEN_EXPIRE: z.number(), + RIGHTS: z.object({ + STREAM_ADS_AVAILABLE: z.boolean(), + STREAM_ADS: z.string(), + STREAM_SUB_AVAILABLE: z.boolean(), + STREAM_SUB: z.string(), + }), +}) +type GwTrack = z.infer + +const userData = await gwLightApi({ + method: 'deezer.getUserData', + result: z.object({ + USER: z.object({ + OPTIONS: z.object({ + license_token: z.string(), + }), + BLOG_NAME: z.string(), + }), + checkForm: z.string(), + }), +}) + +const GetUrlResult = z.object({ + data: z.array( + z.object({ + media: z.array( + z.object({ + media_type: z.string(), + cipher: z.object({ type: z.string() }), + format: z.string(), + sources: z.array(z.object({ url: z.string(), provider: z.string() })), + nbf: z.number(), + exp: z.number(), + }), + ), + }), + ), +}) + +const GetLyricsResult = z.object({ + track: z.object({ + lyrics: z.object({ + text: z.string(), + synchronizedLines: z.array(z.object({ + lrcTimestamp: z.string(), + line: z.string(), + duration: z.number(), + })).nullable(), + }).nullable(), + }), +}) + +const BLOWFISH_SALT_1 = [97, 57, 118, 48, 119, 53, 101, 103] +const BLOWFISH_SALT_2 = [49, 110, 102, 122, 99, 56, 108, 52] +const BLOWFISH_CHUNK_SIZE = 61440 +const BLOWFISH_BLOCK_SIZE = 2048 + +class BlowfishDecryptTransform implements Transformer { + cipher: Blowfish + constructor(readonly trackId: string) { + const trackIdMd5 = createHash('md5').update(trackId).digest('hex') + const bfKey = new Uint8Array(16) + for (let i = 0; i < 16; i++) { + bfKey[i] + = trackIdMd5[i].charCodeAt(0) + ^ trackIdMd5[i + 16].charCodeAt(0) + ^ (i % 2 ? BLOWFISH_SALT_2 : BLOWFISH_SALT_1)[ + 7 - Math.floor(i / 2) + ] + } + + this.cipher = new Blowfish(bfKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL) + this.cipher.setIv(hex.decode('0001020304050607')) + } + + #processChunk(chunk: Uint8Array) { + const res = new Uint8Array(chunk.length) + + const size = chunk.length + for ( + let pos = 0, blockIdx = 0; + pos < size && pos + BLOWFISH_BLOCK_SIZE <= size; + pos += BLOWFISH_BLOCK_SIZE, blockIdx++ + ) { + const block = chunk.subarray(pos, pos + BLOWFISH_BLOCK_SIZE) + if (blockIdx % 3 === 0) { + res.set(this.cipher.decode(block, Blowfish.TYPE.UINT8_ARRAY), pos) + } else { + res.set(block, pos) + } + } + + return res + } + + #buffer = Bytes.alloc() + transform(chunk: Uint8Array, ctx: TransformStreamDefaultController) { + write.bytes(this.#buffer, chunk) + + if (this.#buffer.available > BLOWFISH_CHUNK_SIZE) { + const chunk = read.exactly(this.#buffer, BLOWFISH_CHUNK_SIZE) + ctx.enqueue(this.#processChunk(chunk)) + this.#buffer.reclaim() + } + + return Promise.resolve(undefined) + } + + flush(ctx: TransformStreamDefaultController) { + const remaining = this.#buffer.result() + if (remaining.length) { + ctx.enqueue(remaining) + } + } +} + +async function downloadTrack(track: GwTrack, opts: { + destination: string +}) { + const albumUrl = `https://cdn-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/1500x1500-000000-80-0-0.jpg` + const [getUrlRes, albumAb, lyricsRes] = await Promise.all([ + ffetch.post('https://media.deezer.com/v1/get_url', { + json: { + license_token: userData.USER.OPTIONS.license_token, + media: [{ + type: 'FULL', + formats: [ + { cipher: 'BF_CBC_STRIPE', format: 'FLAC' }, + { cipher: 'BF_CBC_STRIPE', format: 'MP3_320' }, + { cipher: 'BF_CBC_STRIPE', format: 'MP3_128' }, + { cipher: 'BF_CBC_STRIPE', format: 'MP3_64' }, + { cipher: 'BF_CBC_STRIPE', format: 'MP3_MISC' }, + ], + }], + track_tokens: [track.TRACK_TOKEN], + }, + }).parsedJson(GetUrlResult), + ffetch.get(albumUrl).arrayBuffer(), + graphqlApi({ + query: 'query GetLyrics($trackId: String!) {\n track(trackId: $trackId) {\n id\n lyrics {\n id\n text\n ...SynchronizedWordByWordLines\n ...SynchronizedLines\n copyright\n writers\n __typename\n }\n __typename\n }\n}\n\nfragment SynchronizedWordByWordLines on Lyrics {\n id\n synchronizedWordByWordLines {\n start\n end\n words {\n start\n end\n word\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment SynchronizedLines on Lyrics {\n id\n synchronizedLines {\n lrcTimestamp\n line\n lineTranslated\n milliseconds\n duration\n __typename\n }\n __typename\n}', + variables: { + trackId: track.SNG_ID, + }, + result: GetLyricsResult, + }), + ]) + + const albumCoverPath = join(`assets/deezer-tmp-${track.SNG_ID}.jpg`) + await writeFile(albumCoverPath, new Uint8Array(albumAb)) + + const media = getUrlRes.data[0].media[0] + + const stream = await ffetch.get(media.sources[0].url).stream() + const decStream = stream.pipeThrough( + new TransformStream( + new BlowfishDecryptTransform(track.SNG_ID), + ) as any, + ) + + const ext = media.format === 'FLAC' ? 'flac' : 'mp3' + const filename = `${opts.destination}.${ext}` + + await mkdir(dirname(filename), { recursive: true }) + + let lyricsLrc: string | undefined + if (lyricsRes.track.lyrics) { + if (lyricsRes.track.lyrics.synchronizedLines) { + lyricsLrc = lyricsRes.track.lyrics.synchronizedLines.map(it => `${it.lrcTimestamp}${it.line}`).join('\n') + } else { + lyricsLrc = lyricsRes.track.lyrics.text + } + } + + if (ext === 'mp3') { + const params: string[] = [ + '-y', + '-i', + 'pipe:0', + '-i', + albumCoverPath, + '-map', + '1:v:0', + '-id3v2_version', + '3', + '-metadata:s:v', + 'title=Album cover', + '-metadata:s:v', + 'comment=Cover (front)', + '-map', + '0:a', + '-c', + 'copy', + '-metadata', + `title=${track.SNG_TITLE}`, + '-metadata', + `artist=${track.ART_NAME}`, + '-metadata', + `album=${track.ALB_TITLE}`, + '-metadata', + `year=${track.DIGITAL_RELEASE_DATE}`, + '-metadata', + `comment=ripped from deezer (id: ${track.SNG_ID})`, + filename, + ] + + if (lyricsLrc) { + params.push( + '-metadata', + `lyrics=${lyricsLrc}`, + ) + } + + const proc = $`ffmpeg ${params}` + const pipe = Readable.fromWeb(decStream as any).pipe(proc.stdin) + await new Promise((resolve, reject) => { + pipe.on('error', reject) + pipe.on('finish', resolve) + }) + await proc + } else { + const fd = await open(filename, 'w+') + const writer = fd.createWriteStream() + + for await (const chunk of decStream as any) { + writer.write(chunk) + } + + writer.end() + + await new Promise((resolve, reject) => { + writer.on('error', reject) + writer.on('finish', resolve) + }) + + const params: string[] = [ + '--remove-all-tags', + `--set-tag=TITLE=${track.SNG_TITLE}`, + `--set-tag=ARTIST=${track.ART_NAME}`, + `--set-tag=ALBUM=${track.ALB_TITLE}`, + `--set-tag=DATE=${track.DIGITAL_RELEASE_DATE}`, + `--set-tag=COMMENT=ripped from deezer (id: ${track.SNG_ID})`, + `--import-picture-from=${albumCoverPath}`, + ] + + params.push(filename) + + await $`metaflac ${params}` + } + + await rm(albumCoverPath, { force: true }) +} + +async function downloadByUri(uri: string) { + const [type, id] = uri.split(':') + + if (type === 'track') { + const res = await gwLightApi({ + method: 'song.getListData', + token: userData.checkForm, + options: { + sng_ids: [id], + }, + result: z.object({ + data: z.array(GwTrack), + }), + }) + const track = res.data[0] + + const filename = `${track.ART_NAME} - ${track.SNG_TITLE}` + + console.log('downloading track:', filename) + await downloadTrack(track, { + destination: join('assets/deezer-dl', sanitizeFilename(filename)), + }) + } +} + +console.log('logged in as %s', userData.USER.BLOG_NAME) + +const url = process.argv[2] ?? await question('url or search > ') + +if (url.match(/^(artist|album|track):(\d+)$/)) { + await downloadByUri(url) +} else if (url.startsWith('https://www.deezer.com/')) { + // todo +} else { + // search query + const searchResult = await graphqlApi({ + query: 'query SearchFull($query: String!, $firstGrid: Int!, $firstList: Int!) {\n instantSearch(query: $query) {\n bestResult {\n __typename\n ... on InstantSearchAlbumBestResult {\n album {\n ...SearchAlbum\n __typename\n }\n __typename\n }\n ... on InstantSearchArtistBestResult {\n artist {\n ...BestResultArtist\n __typename\n }\n __typename\n }\n ... on InstantSearchPlaylistBestResult {\n playlist {\n ...SearchPlaylist\n __typename\n }\n __typename\n }\n ... on InstantSearchPodcastBestResult {\n podcast {\n ...SearchPodcast\n __typename\n }\n __typename\n }\n ... on InstantSearchLivestreamBestResult {\n livestream {\n ...SearchLivestream\n __typename\n }\n __typename\n }\n ... on InstantSearchTrackBestResult {\n track {\n ...TableTrack\n __typename\n }\n __typename\n }\n ... on InstantSearchPodcastEpisodeBestResult {\n podcastEpisode {\n ...SearchPodcastEpisode\n __typename\n }\n __typename\n }\n }\n results {\n artists(first: $firstGrid) {\n edges {\n node {\n ...SearchArtist\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n albums(first: $firstGrid) {\n edges {\n node {\n ...SearchAlbum\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n channels(first: $firstGrid) {\n edges {\n node {\n ...SearchChannel\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n flowConfigs(first: $firstGrid) {\n edges {\n node {\n ...SearchFlowConfig\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n livestreams(first: $firstGrid) {\n edges {\n node {\n ...SearchLivestream\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n playlists(first: $firstGrid) {\n edges {\n node {\n ...SearchPlaylist\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n podcasts(first: $firstGrid) {\n edges {\n node {\n ...SearchPodcast\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n tracks(first: $firstList) {\n edges {\n node {\n ...TableTrack\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n users(first: $firstGrid) {\n edges {\n node {\n ...SearchUser\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n podcastEpisodes(first: $firstList) {\n edges {\n node {\n ...SearchPodcastEpisode\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment SearchAlbum on Album {\n id\n displayTitle\n isFavorite\n releaseDateAlbum: releaseDate\n isExplicitAlbum: isExplicit\n cover {\n ...PictureLarge\n __typename\n }\n contributors {\n edges {\n roles\n node {\n ... on Artist {\n id\n name\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n tracksCount\n __typename\n}\n\nfragment PictureLarge on Picture {\n id\n large: urls(pictureRequest: {width: 500, height: 500})\n explicitStatus\n __typename\n}\n\nfragment BestResultArtist on Artist {\n ...SearchArtist\n hasSmartRadio\n hasTopTracks\n __typename\n}\n\nfragment SearchArtist on Artist {\n id\n isFavorite\n name\n fansCount\n picture {\n ...PictureLarge\n __typename\n }\n __typename\n}\n\nfragment SearchPlaylist on Playlist {\n id\n title\n isFavorite\n estimatedTracksCount\n fansCount\n picture {\n ...PictureLarge\n __typename\n }\n owner {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment SearchPodcast on Podcast {\n id\n displayTitle\n isPodcastFavorite: isFavorite\n cover {\n ...PictureLarge\n __typename\n }\n isExplicit\n rawEpisodes\n __typename\n}\n\nfragment SearchLivestream on Livestream {\n id\n name\n cover {\n ...PictureLarge\n __typename\n }\n __typename\n}\n\nfragment TableTrack on Track {\n id\n title\n duration\n popularity\n isExplicit\n lyrics {\n id\n __typename\n }\n media {\n id\n rights {\n ads {\n available\n availableAfter\n __typename\n }\n sub {\n available\n availableAfter\n __typename\n }\n __typename\n }\n __typename\n }\n album {\n id\n displayTitle\n cover {\n ...PictureXSmall\n ...PictureLarge\n __typename\n }\n __typename\n }\n contributors {\n edges {\n node {\n ... on Artist {\n id\n name\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PictureXSmall on Picture {\n id\n xxx_small: urls(pictureRequest: {width: 40, height: 40})\n explicitStatus\n __typename\n}\n\nfragment SearchPodcastEpisode on PodcastEpisode {\n id\n title\n description\n duration\n releaseDate\n media {\n url\n __typename\n }\n podcast {\n id\n displayTitle\n isExplicit\n cover {\n ...PictureSmall\n ...PictureLarge\n __typename\n }\n rights {\n ads {\n available\n __typename\n }\n sub {\n available\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PictureSmall on Picture {\n id\n small: urls(pictureRequest: {height: 100, width: 100})\n explicitStatus\n __typename\n}\n\nfragment SearchChannel on Channel {\n id\n picture {\n ...PictureLarge\n __typename\n }\n logoAsset {\n id\n large: urls(uiAssetRequest: {width: 500, height: 0})\n __typename\n }\n name\n slug\n backgroundColor\n __typename\n}\n\nfragment SearchFlowConfig on FlowConfig {\n id\n title\n visuals {\n dynamicPageIcon {\n id\n large: urls(uiAssetRequest: {width: 500, height: 500})\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment SearchUser on User {\n id\n name\n picture {\n ...PictureLarge\n __typename\n }\n __typename\n}', + variables: { + query: url, + firstGrid: 10, + firstList: 10, + }, + result: z.object({ + instantSearch: z.object({ + results: z.object({ + artists: z.object({ + edges: z.array(z.object({ + node: z.object({ + id: z.string(), + name: z.string(), + }), + })), + }), + albums: z.object({ + edges: z.array(z.object({ + node: z.object({ + id: z.string(), + displayTitle: z.string(), + }), + })), + }), + tracks: z.object({ + edges: z.array(z.object({ + node: z.object({ + id: z.string(), + title: z.string(), + }), + })), + }), + }), + }), + }), + }) + + for (const [i, { node }] of iter.enumerate(searchResult.instantSearch.results.artists.edges)) { + console.log(`artist:${node.id}: ${node.name}`) + } + + for (const [i, { node }] of iter.enumerate(searchResult.instantSearch.results.albums.edges)) { + console.log(`album:${node.id}: ${node.displayTitle}`) + } + + for (const [i, { node }] of iter.enumerate(searchResult.instantSearch.results.tracks.edges)) { + console.log(`track:${node.id}: ${node.title}`) + } + + const uri = await question('option > ') + + await downloadByUri(uri) +} From 2d46db9c7708f69ed429ddc407328e6a266839dc Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 10 May 2025 21:06:34 +0000 Subject: [PATCH 40/61] chore: update public repo --- package.json | 8 +- pnpm-lock.yaml | 33 ++- scripts/auth/mtcute-login.ts | 3 + scripts/media/deezer-dl.ts | 516 +++++++++++++++++++++++++++++++++++ 4 files changed, 541 insertions(+), 19 deletions(-) create mode 100644 scripts/media/deezer-dl.ts diff --git a/package.json b/package.json index 1ecf24e..3269071 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,6 @@ "name": "teidesu-scripts", "type": "module", "packageManager": "pnpm@9.5.0", - "peerDependencies": { - "typescript": "^5.0.0" - }, "dependencies": { "@faker-js/faker": "^9.3.0", "@fuman/io": "^0.0.4", @@ -17,6 +14,7 @@ "better-sqlite3": "^11.8.1", "canvas": "^3.1.0", "cheerio": "^1.0.0", + "egoroof-blowfish": "4.0.1", "es-main": "^1.3.0", "filesize": "^10.1.6", "json5": "^2.2.3", @@ -33,8 +31,8 @@ }, "devDependencies": { "@antfu/eslint-config": "3.10.0", - "@fuman/fetch": "0.0.10", - "@fuman/utils": "0.0.10", + "@fuman/fetch": "0.1.0", + "@fuman/utils": "0.0.14", "@types/node": "22.10.0", "domhandler": "^5.0.3", "dotenv": "16.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef97f05..871673a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: cheerio: specifier: ^1.0.0 version: 1.0.0 + egoroof-blowfish: + specifier: 4.0.1 + version: 4.0.1 es-main: specifier: ^1.3.0 version: 1.3.0 @@ -74,9 +77,6 @@ importers: tsx: specifier: ^4.19.2 version: 4.19.2 - typescript: - specifier: ^5.0.0 - version: 5.7.2 undici: specifier: ^7.2.0 version: 7.2.0 @@ -88,11 +88,11 @@ importers: specifier: 3.10.0 version: 3.10.0(@typescript-eslint/utils@8.16.0(eslint@9.15.0)(typescript@5.7.2))(@vue/compiler-sfc@3.5.13)(eslint@9.15.0)(typescript@5.7.2) '@fuman/fetch': - specifier: 0.0.10 - version: 0.0.10(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) + specifier: 0.1.0 + version: 0.1.0(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8) '@fuman/utils': - specifier: 0.0.10 - version: 0.0.10 + specifier: 0.0.14 + version: 0.0.14 '@types/node': specifier: 22.10.0 version: 22.10.0 @@ -406,8 +406,8 @@ packages: resolution: {integrity: sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} - '@fuman/fetch@0.0.10': - resolution: {integrity: sha512-Zy1GKZ8/NGP2adH0JXVaP91TZPpXngZDTGOFrb38pEp6RYAVSywt0yOPO2kwZGUpordfCj96CMNv+e6zUc7qqw==} + '@fuman/fetch@0.1.0': + resolution: {integrity: sha512-q3CWQJ939Kcmrp/9/dUOCRqQi2og4uweSy7QrzeIxKRsz3jYSca1q2mwTQtTnA+9AAq2WNMlcZtgOA9qVJkXJw==} peerDependencies: '@badrap/valita': '>=0.4.0' tough-cookie: ^5.0.0 || ^4.0.0 @@ -441,8 +441,8 @@ packages: '@fuman/node@0.0.4': resolution: {integrity: sha512-tgwbIceUHWuwh4RTwJRQ1sLjzuIGrWx0SeCrqYhGF+IkI/B7DY0FP2SZykWImkVDtW8IzmdZskPZqiDINRGcNg==} - '@fuman/utils@0.0.10': - resolution: {integrity: sha512-KVlDx0S1Og7IWcPi93f1T45WPfCSUV6/A4dQb36zZRtb8KECl1BK2u9WkNVI+sjrjKCb3xijjY5gq4lS3PqH5g==} + '@fuman/utils@0.0.14': + resolution: {integrity: sha512-HmQo6DXoYBtkN12rZsMSZRTUynByioOHpMZjfrePwUwXuKBYm9F1Rm4R7tGLTNJTG1zMXBJw1Xwy/cRqwzrujw==} '@fuman/utils@0.0.4': resolution: {integrity: sha512-YBZIlGDbM8s9G85pWFZJ9wQrDY4511XLHZ06/uxZfXBY0eSStwje8JFNmRMNF0SjRk4D3iRmPl9wirHKTkg89w==} @@ -906,6 +906,9 @@ packages: doublearray@0.0.2: resolution: {integrity: sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==} + egoroof-blowfish@4.0.1: + resolution: {integrity: sha512-e2gdfyLZrDvyuHX2NkRPFxEuYCIddp+W3MucqjBw9h9nineZUWynuuqQh+R5sNT9IVopPFBHwcnBYHqTcB1Vdw==} + electron-to-chromium@1.5.65: resolution: {integrity: sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw==} @@ -2357,9 +2360,9 @@ snapshots: '@faker-js/faker@9.3.0': {} - '@fuman/fetch@0.0.10(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': + '@fuman/fetch@0.1.0(@badrap/valita@0.4.2)(tough-cookie@5.0.0)(zod@3.23.8)': dependencies: - '@fuman/utils': 0.0.10 + '@fuman/utils': 0.0.14 optionalDependencies: '@badrap/valita': 0.4.2 tough-cookie: 5.0.0 @@ -2389,7 +2392,7 @@ snapshots: '@fuman/net': 0.0.4 '@fuman/utils': 0.0.4 - '@fuman/utils@0.0.10': {} + '@fuman/utils@0.0.14': {} '@fuman/utils@0.0.4': {} @@ -2920,6 +2923,8 @@ snapshots: doublearray@0.0.2: {} + egoroof-blowfish@4.0.1: {} + electron-to-chromium@1.5.65: {} emoji-regex@8.0.0: {} diff --git a/scripts/auth/mtcute-login.ts b/scripts/auth/mtcute-login.ts index 0bf4af9..7946774 100644 --- a/scripts/auth/mtcute-login.ts +++ b/scripts/auth/mtcute-login.ts @@ -9,6 +9,9 @@ if (!sessionName) { const tg = createTg(sessionName) +await tg.prepare() +await tg.storage.clear(true) + const self = await tg.start({ qrCodeHandler(url, expires) { console.log(qrTerminal.generate(url, { small: true })) diff --git a/scripts/media/deezer-dl.ts b/scripts/media/deezer-dl.ts new file mode 100644 index 0000000..c30dee7 --- /dev/null +++ b/scripts/media/deezer-dl.ts @@ -0,0 +1,516 @@ +import { createHash } from 'node:crypto' +import { mkdir, open, rm, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { Readable } from 'node:stream' +import { TransformStream } from 'node:stream/web' +import { Bytes, read, write } from '@fuman/io' +import { base64, hex, iter, utf8 } from '@fuman/utils' +import { Blowfish } from 'egoroof-blowfish' +import { CookieJar } from 'tough-cookie' +import { FileCookieStore } from 'tough-cookie-file-store' +import { z } from 'zod' +import { $, question } from 'zx' +import { ffetch as ffetchBase } from '../../utils/fetch.ts' +import { sanitizeFilename } from '../../utils/fs.ts' +import { getEnv } from '../../utils/misc.ts' + +const jar = new CookieJar(new FileCookieStore('./assets/deezer-cookies.json')) +await jar.setCookie(`arl=${getEnv('DEEZER_ARL')}; path=/; domain=.deezer.com;`, 'https://www.deezer.com') +await jar.setCookie('comeback=1; path=/; domain=.deezer.com;', 'https://www.deezer.com') + +const ffetch = ffetchBase.extend({ + cookies: jar, + headers: { + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0', + 'accept': '*/*', + 'accept-language': 'en-US,en;q=0.5', + 'accept-encoding': 'gzip, deflate, br', + 'referer': 'https://www.deezer.com/', + 'origin': 'https://www.deezer.com', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + }, +}) + +const GwLightEnvelope = z.object({ + error: z.union([ + z.array(z.unknown()), + z.record(z.unknown()), + ]), + results: z.unknown(), +}) + +async function gwLightApi(params: { + method: string + token?: string + options?: unknown + result: T +}): Promise> { + const { method, token, options, result } = params + const res = await ffetch.post('https://www.deezer.com/ajax/gw-light.php', { + query: { + method, + input: '3', + api_version: '1.0', + api_token: token ?? '', + cid: Math.floor(1000000000 * Math.random()), + }, + json: options ?? {}, + }).parsedJson(GwLightEnvelope) + + if (res.error.length || (typeof res.error === 'object' && !Array.isArray(res.error))) { + throw new Error(JSON.stringify(res.error)) + } + + return result.parse(res.results) +} + +let _cachedToken: [string, number] | undefined +async function getGraphqlToken() { + if (_cachedToken && Date.now() < _cachedToken[1]) return _cachedToken[0] + const hasRefreshToken = (await jar.getCookieString('https://auth.deezer.com')).includes('refresh-token=') + const { jwt } = await ffetch.post(hasRefreshToken ? 'https://auth.deezer.com/login/renew' : 'https://auth.deezer.com/login/arl', { + // i have no idea what these mean + query: { + jo: 'p', + rto: 'c', + i: 'c', + }, + }).parsedJson(z.object({ + jwt: z.string(), + })) + + const expires = JSON.parse(utf8.decoder.decode(base64.decode(jwt.split('.')[1]))).exp * 1000 + _cachedToken = [jwt, expires] + return jwt +} + +async function graphqlApi(params: { + query: string + variables?: unknown + result: T +}): Promise> { + const { query, variables, result } = params + const operationName = query.match(/(?:query|mutation)\s+(\w+)/)?.[1] + + const res = await ffetchBase.post('https://pipe.deezer.com/api', { + json: { + operationName, + query, + variables, + }, + headers: { + Authorization: `Bearer ${await getGraphqlToken()}`, + }, + }).json() + + if (!res.data) { + throw new Error(JSON.stringify(res)) + } + + return result.parse(res.data) +} + +const GwTrack = z.object({ + ALB_ID: z.string(), + ALB_PICTURE: z.string(), + ALB_TITLE: z.string(), + ARTISTS: z.array( + z.object({ + ART_ID: z.string(), + ROLE_ID: z.string(), + ARTISTS_SONGS_ORDER: z.string(), + ART_NAME: z.string(), + ARTIST_IS_DUMMY: z.boolean().optional(), + ART_PICTURE: z.string(), + RANK: z.string(), + }), + ), + ART_ID: z.string(), + ART_NAME: z.string(), + ARTIST_IS_DUMMY: z.boolean().optional(), + DIGITAL_RELEASE_DATE: z.string(), + DISK_NUMBER: z.string(), + DURATION: z.string(), + EXPLICIT_LYRICS: z.string(), + EXPLICIT_TRACK_CONTENT: z.object({ + EXPLICIT_LYRICS_STATUS: z.number(), + EXPLICIT_COVER_STATUS: z.number(), + }), + GENRE_ID: z.string(), + HIERARCHICAL_TITLE: z.string().optional(), + ISRC: z.string(), + LYRICS_ID: z.number(), + PHYSICAL_RELEASE_DATE: z.string(), + PROVIDER_ID: z.string(), + RANK: z.string().optional(), + SMARTRADIO: z.number(), + SNG_CONTRIBUTORS: z.object({ + main_artist: z.array(z.string()), + author: z.array(z.string()), + composer: z.array(z.string()), + }).partial().optional(), + SNG_ID: z.string(), + SNG_TITLE: z.string(), + STATUS: z.number(), + TRACK_NUMBER: z.string(), + USER_ID: z.number(), + VERSION: z.string(), + MD5_ORIGIN: z.string(), + FILESIZE_AAC_64: z.coerce.number(), + FILESIZE_MP3_64: z.coerce.number(), + FILESIZE_MP3_128: z.coerce.number(), + FILESIZE_MP3_256: z.coerce.number(), + FILESIZE_MP3_320: z.coerce.number(), + FILESIZE_MP4_RA1: z.coerce.number(), + FILESIZE_MP4_RA2: z.coerce.number(), + FILESIZE_MP4_RA3: z.coerce.number(), + FILESIZE_FLAC: z.coerce.number(), + FILESIZE: z.coerce.number(), + GAIN: z.nullable(z.coerce.number()), + MEDIA_VERSION: z.string(), + TRACK_TOKEN: z.string(), + TRACK_TOKEN_EXPIRE: z.number(), + RIGHTS: z.object({ + STREAM_ADS_AVAILABLE: z.boolean(), + STREAM_ADS: z.string(), + STREAM_SUB_AVAILABLE: z.boolean(), + STREAM_SUB: z.string(), + }), +}) +type GwTrack = z.infer + +const userData = await gwLightApi({ + method: 'deezer.getUserData', + result: z.object({ + USER: z.object({ + OPTIONS: z.object({ + license_token: z.string(), + }), + BLOG_NAME: z.string(), + }), + checkForm: z.string(), + }), +}) + +const GetUrlResult = z.object({ + data: z.array( + z.object({ + media: z.array( + z.object({ + media_type: z.string(), + cipher: z.object({ type: z.string() }), + format: z.string(), + sources: z.array(z.object({ url: z.string(), provider: z.string() })), + nbf: z.number(), + exp: z.number(), + }), + ), + }), + ), +}) + +const GetLyricsResult = z.object({ + track: z.object({ + lyrics: z.object({ + text: z.string(), + synchronizedLines: z.array(z.object({ + lrcTimestamp: z.string(), + line: z.string(), + duration: z.number(), + })).nullable(), + }).nullable(), + }), +}) + +const BLOWFISH_SALT_1 = [97, 57, 118, 48, 119, 53, 101, 103] +const BLOWFISH_SALT_2 = [49, 110, 102, 122, 99, 56, 108, 52] +const BLOWFISH_CHUNK_SIZE = 61440 +const BLOWFISH_BLOCK_SIZE = 2048 + +class BlowfishDecryptTransform implements Transformer { + cipher: Blowfish + constructor(readonly trackId: string) { + const trackIdMd5 = createHash('md5').update(trackId).digest('hex') + const bfKey = new Uint8Array(16) + for (let i = 0; i < 16; i++) { + bfKey[i] + = trackIdMd5[i].charCodeAt(0) + ^ trackIdMd5[i + 16].charCodeAt(0) + ^ (i % 2 ? BLOWFISH_SALT_2 : BLOWFISH_SALT_1)[ + 7 - Math.floor(i / 2) + ] + } + + this.cipher = new Blowfish(bfKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL) + this.cipher.setIv(hex.decode('0001020304050607')) + } + + #processChunk(chunk: Uint8Array) { + const res = new Uint8Array(chunk.length) + + const size = chunk.length + for ( + let pos = 0, blockIdx = 0; + pos < size && pos + BLOWFISH_BLOCK_SIZE <= size; + pos += BLOWFISH_BLOCK_SIZE, blockIdx++ + ) { + const block = chunk.subarray(pos, pos + BLOWFISH_BLOCK_SIZE) + if (blockIdx % 3 === 0) { + res.set(this.cipher.decode(block, Blowfish.TYPE.UINT8_ARRAY), pos) + } else { + res.set(block, pos) + } + } + + return res + } + + #buffer = Bytes.alloc() + transform(chunk: Uint8Array, ctx: TransformStreamDefaultController) { + write.bytes(this.#buffer, chunk) + + if (this.#buffer.available > BLOWFISH_CHUNK_SIZE) { + const chunk = read.exactly(this.#buffer, BLOWFISH_CHUNK_SIZE) + ctx.enqueue(this.#processChunk(chunk)) + this.#buffer.reclaim() + } + + return Promise.resolve(undefined) + } + + flush(ctx: TransformStreamDefaultController) { + const remaining = this.#buffer.result() + if (remaining.length) { + ctx.enqueue(remaining) + } + } +} + +async function downloadTrack(track: GwTrack, opts: { + destination: string +}) { + const albumUrl = `https://cdn-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/1500x1500-000000-80-0-0.jpg` + const [getUrlRes, albumAb, lyricsRes] = await Promise.all([ + ffetch.post('https://media.deezer.com/v1/get_url', { + json: { + license_token: userData.USER.OPTIONS.license_token, + media: [{ + type: 'FULL', + formats: [ + { cipher: 'BF_CBC_STRIPE', format: 'FLAC' }, + { cipher: 'BF_CBC_STRIPE', format: 'MP3_320' }, + { cipher: 'BF_CBC_STRIPE', format: 'MP3_128' }, + { cipher: 'BF_CBC_STRIPE', format: 'MP3_64' }, + { cipher: 'BF_CBC_STRIPE', format: 'MP3_MISC' }, + ], + }], + track_tokens: [track.TRACK_TOKEN], + }, + }).parsedJson(GetUrlResult), + ffetch.get(albumUrl).arrayBuffer(), + graphqlApi({ + query: 'query GetLyrics($trackId: String!) {\n track(trackId: $trackId) {\n id\n lyrics {\n id\n text\n ...SynchronizedWordByWordLines\n ...SynchronizedLines\n copyright\n writers\n __typename\n }\n __typename\n }\n}\n\nfragment SynchronizedWordByWordLines on Lyrics {\n id\n synchronizedWordByWordLines {\n start\n end\n words {\n start\n end\n word\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment SynchronizedLines on Lyrics {\n id\n synchronizedLines {\n lrcTimestamp\n line\n lineTranslated\n milliseconds\n duration\n __typename\n }\n __typename\n}', + variables: { + trackId: track.SNG_ID, + }, + result: GetLyricsResult, + }), + ]) + + const albumCoverPath = join(`assets/deezer-tmp-${track.SNG_ID}.jpg`) + await writeFile(albumCoverPath, new Uint8Array(albumAb)) + + const media = getUrlRes.data[0].media[0] + + const stream = await ffetch.get(media.sources[0].url).stream() + const decStream = stream.pipeThrough( + new TransformStream( + new BlowfishDecryptTransform(track.SNG_ID), + ) as any, + ) + + const ext = media.format === 'FLAC' ? 'flac' : 'mp3' + const filename = `${opts.destination}.${ext}` + + await mkdir(dirname(filename), { recursive: true }) + + let lyricsLrc: string | undefined + if (lyricsRes.track.lyrics) { + if (lyricsRes.track.lyrics.synchronizedLines) { + lyricsLrc = lyricsRes.track.lyrics.synchronizedLines.map(it => `${it.lrcTimestamp}${it.line}`).join('\n') + } else { + lyricsLrc = lyricsRes.track.lyrics.text + } + } + + if (ext === 'mp3') { + const params: string[] = [ + '-y', + '-i', + 'pipe:0', + '-i', + albumCoverPath, + '-map', + '1:v:0', + '-id3v2_version', + '3', + '-metadata:s:v', + 'title=Album cover', + '-metadata:s:v', + 'comment=Cover (front)', + '-map', + '0:a', + '-c', + 'copy', + '-metadata', + `title=${track.SNG_TITLE}`, + '-metadata', + `artist=${track.ART_NAME}`, + '-metadata', + `album=${track.ALB_TITLE}`, + '-metadata', + `year=${track.DIGITAL_RELEASE_DATE}`, + '-metadata', + `comment=ripped from deezer (id: ${track.SNG_ID})`, + filename, + ] + + if (lyricsLrc) { + params.push( + '-metadata', + `lyrics=${lyricsLrc}`, + ) + } + + const proc = $`ffmpeg ${params}` + const pipe = Readable.fromWeb(decStream as any).pipe(proc.stdin) + await new Promise((resolve, reject) => { + pipe.on('error', reject) + pipe.on('finish', resolve) + }) + await proc + } else { + const fd = await open(filename, 'w+') + const writer = fd.createWriteStream() + + for await (const chunk of decStream as any) { + writer.write(chunk) + } + + writer.end() + + await new Promise((resolve, reject) => { + writer.on('error', reject) + writer.on('finish', resolve) + }) + + const params: string[] = [ + '--remove-all-tags', + `--set-tag=TITLE=${track.SNG_TITLE}`, + `--set-tag=ARTIST=${track.ART_NAME}`, + `--set-tag=ALBUM=${track.ALB_TITLE}`, + `--set-tag=DATE=${track.DIGITAL_RELEASE_DATE}`, + `--set-tag=COMMENT=ripped from deezer (id: ${track.SNG_ID})`, + `--import-picture-from=${albumCoverPath}`, + ] + + params.push(filename) + + await $`metaflac ${params}` + } + + await rm(albumCoverPath, { force: true }) +} + +async function downloadByUri(uri: string) { + const [type, id] = uri.split(':') + + if (type === 'track') { + const res = await gwLightApi({ + method: 'song.getListData', + token: userData.checkForm, + options: { + sng_ids: [id], + }, + result: z.object({ + data: z.array(GwTrack), + }), + }) + const track = res.data[0] + + const filename = `${track.ART_NAME} - ${track.SNG_TITLE}` + + console.log('downloading track:', filename) + await downloadTrack(track, { + destination: join('assets/deezer-dl', sanitizeFilename(filename)), + }) + } +} + +console.log('logged in as %s', userData.USER.BLOG_NAME) + +const url = process.argv[2] ?? await question('url or search > ') + +if (url.match(/^(artist|album|track):(\d+)$/)) { + await downloadByUri(url) +} else if (url.startsWith('https://www.deezer.com/')) { + // todo +} else { + // search query + const searchResult = await graphqlApi({ + query: 'query SearchFull($query: String!, $firstGrid: Int!, $firstList: Int!) {\n instantSearch(query: $query) {\n bestResult {\n __typename\n ... on InstantSearchAlbumBestResult {\n album {\n ...SearchAlbum\n __typename\n }\n __typename\n }\n ... on InstantSearchArtistBestResult {\n artist {\n ...BestResultArtist\n __typename\n }\n __typename\n }\n ... on InstantSearchPlaylistBestResult {\n playlist {\n ...SearchPlaylist\n __typename\n }\n __typename\n }\n ... on InstantSearchPodcastBestResult {\n podcast {\n ...SearchPodcast\n __typename\n }\n __typename\n }\n ... on InstantSearchLivestreamBestResult {\n livestream {\n ...SearchLivestream\n __typename\n }\n __typename\n }\n ... on InstantSearchTrackBestResult {\n track {\n ...TableTrack\n __typename\n }\n __typename\n }\n ... on InstantSearchPodcastEpisodeBestResult {\n podcastEpisode {\n ...SearchPodcastEpisode\n __typename\n }\n __typename\n }\n }\n results {\n artists(first: $firstGrid) {\n edges {\n node {\n ...SearchArtist\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n albums(first: $firstGrid) {\n edges {\n node {\n ...SearchAlbum\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n channels(first: $firstGrid) {\n edges {\n node {\n ...SearchChannel\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n flowConfigs(first: $firstGrid) {\n edges {\n node {\n ...SearchFlowConfig\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n livestreams(first: $firstGrid) {\n edges {\n node {\n ...SearchLivestream\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n playlists(first: $firstGrid) {\n edges {\n node {\n ...SearchPlaylist\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n podcasts(first: $firstGrid) {\n edges {\n node {\n ...SearchPodcast\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n tracks(first: $firstList) {\n edges {\n node {\n ...TableTrack\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n users(first: $firstGrid) {\n edges {\n node {\n ...SearchUser\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n podcastEpisodes(first: $firstList) {\n edges {\n node {\n ...SearchPodcastEpisode\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor\n __typename\n }\n priority\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment SearchAlbum on Album {\n id\n displayTitle\n isFavorite\n releaseDateAlbum: releaseDate\n isExplicitAlbum: isExplicit\n cover {\n ...PictureLarge\n __typename\n }\n contributors {\n edges {\n roles\n node {\n ... on Artist {\n id\n name\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n tracksCount\n __typename\n}\n\nfragment PictureLarge on Picture {\n id\n large: urls(pictureRequest: {width: 500, height: 500})\n explicitStatus\n __typename\n}\n\nfragment BestResultArtist on Artist {\n ...SearchArtist\n hasSmartRadio\n hasTopTracks\n __typename\n}\n\nfragment SearchArtist on Artist {\n id\n isFavorite\n name\n fansCount\n picture {\n ...PictureLarge\n __typename\n }\n __typename\n}\n\nfragment SearchPlaylist on Playlist {\n id\n title\n isFavorite\n estimatedTracksCount\n fansCount\n picture {\n ...PictureLarge\n __typename\n }\n owner {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment SearchPodcast on Podcast {\n id\n displayTitle\n isPodcastFavorite: isFavorite\n cover {\n ...PictureLarge\n __typename\n }\n isExplicit\n rawEpisodes\n __typename\n}\n\nfragment SearchLivestream on Livestream {\n id\n name\n cover {\n ...PictureLarge\n __typename\n }\n __typename\n}\n\nfragment TableTrack on Track {\n id\n title\n duration\n popularity\n isExplicit\n lyrics {\n id\n __typename\n }\n media {\n id\n rights {\n ads {\n available\n availableAfter\n __typename\n }\n sub {\n available\n availableAfter\n __typename\n }\n __typename\n }\n __typename\n }\n album {\n id\n displayTitle\n cover {\n ...PictureXSmall\n ...PictureLarge\n __typename\n }\n __typename\n }\n contributors {\n edges {\n node {\n ... on Artist {\n id\n name\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PictureXSmall on Picture {\n id\n xxx_small: urls(pictureRequest: {width: 40, height: 40})\n explicitStatus\n __typename\n}\n\nfragment SearchPodcastEpisode on PodcastEpisode {\n id\n title\n description\n duration\n releaseDate\n media {\n url\n __typename\n }\n podcast {\n id\n displayTitle\n isExplicit\n cover {\n ...PictureSmall\n ...PictureLarge\n __typename\n }\n rights {\n ads {\n available\n __typename\n }\n sub {\n available\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PictureSmall on Picture {\n id\n small: urls(pictureRequest: {height: 100, width: 100})\n explicitStatus\n __typename\n}\n\nfragment SearchChannel on Channel {\n id\n picture {\n ...PictureLarge\n __typename\n }\n logoAsset {\n id\n large: urls(uiAssetRequest: {width: 500, height: 0})\n __typename\n }\n name\n slug\n backgroundColor\n __typename\n}\n\nfragment SearchFlowConfig on FlowConfig {\n id\n title\n visuals {\n dynamicPageIcon {\n id\n large: urls(uiAssetRequest: {width: 500, height: 500})\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment SearchUser on User {\n id\n name\n picture {\n ...PictureLarge\n __typename\n }\n __typename\n}', + variables: { + query: url, + firstGrid: 10, + firstList: 10, + }, + result: z.object({ + instantSearch: z.object({ + results: z.object({ + artists: z.object({ + edges: z.array(z.object({ + node: z.object({ + id: z.string(), + name: z.string(), + }), + })), + }), + albums: z.object({ + edges: z.array(z.object({ + node: z.object({ + id: z.string(), + displayTitle: z.string(), + }), + })), + }), + tracks: z.object({ + edges: z.array(z.object({ + node: z.object({ + id: z.string(), + title: z.string(), + }), + })), + }), + }), + }), + }), + }) + + for (const [i, { node }] of iter.enumerate(searchResult.instantSearch.results.artists.edges)) { + console.log(`artist:${node.id}: ${node.name}`) + } + + for (const [i, { node }] of iter.enumerate(searchResult.instantSearch.results.albums.edges)) { + console.log(`album:${node.id}: ${node.displayTitle}`) + } + + for (const [i, { node }] of iter.enumerate(searchResult.instantSearch.results.tracks.edges)) { + console.log(`track:${node.id}: ${node.title}`) + } + + const uri = await question('option > ') + + await downloadByUri(uri) +} From 53398ff93d477c52218a1d9fd29a0a013e87c0db Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sun, 11 May 2025 22:29:57 +0000 Subject: [PATCH 41/61] chore: update public repo --- scripts/media/deezer-dl.ts | 344 ++++++++++++++++++++++++++++++++++--- 1 file changed, 319 insertions(+), 25 deletions(-) diff --git a/scripts/media/deezer-dl.ts b/scripts/media/deezer-dl.ts index c30dee7..9797c3b 100644 --- a/scripts/media/deezer-dl.ts +++ b/scripts/media/deezer-dl.ts @@ -4,8 +4,9 @@ import { dirname, join } from 'node:path' import { Readable } from 'node:stream' import { TransformStream } from 'node:stream/web' import { Bytes, read, write } from '@fuman/io' -import { base64, hex, iter, utf8 } from '@fuman/utils' +import { asNonNull, assert, asyncPool, base64, hex, iter, unknownToError, utf8 } from '@fuman/utils' import { Blowfish } from 'egoroof-blowfish' +import Spinnies from 'spinnies' import { CookieJar } from 'tough-cookie' import { FileCookieStore } from 'tough-cookie-file-store' import { z } from 'zod' @@ -130,7 +131,7 @@ const GwTrack = z.object({ ART_ID: z.string(), ART_NAME: z.string(), ARTIST_IS_DUMMY: z.boolean().optional(), - DIGITAL_RELEASE_DATE: z.string(), + DIGITAL_RELEASE_DATE: z.string().optional(), DISK_NUMBER: z.string(), DURATION: z.string(), EXPLICIT_LYRICS: z.string(), @@ -138,24 +139,18 @@ const GwTrack = z.object({ EXPLICIT_LYRICS_STATUS: z.number(), EXPLICIT_COVER_STATUS: z.number(), }), - GENRE_ID: z.string(), - HIERARCHICAL_TITLE: z.string().optional(), - ISRC: z.string(), + ISRC: z.string().optional(), LYRICS_ID: z.number(), - PHYSICAL_RELEASE_DATE: z.string(), PROVIDER_ID: z.string(), - RANK: z.string().optional(), - SMARTRADIO: z.number(), SNG_CONTRIBUTORS: z.object({ main_artist: z.array(z.string()), author: z.array(z.string()), composer: z.array(z.string()), + featuring: z.array(z.string()), }).partial().optional(), SNG_ID: z.string(), SNG_TITLE: z.string(), - STATUS: z.number(), TRACK_NUMBER: z.string(), - USER_ID: z.number(), VERSION: z.string(), MD5_ORIGIN: z.string(), FILESIZE_AAC_64: z.coerce.number(), @@ -168,7 +163,6 @@ const GwTrack = z.object({ FILESIZE_MP4_RA3: z.coerce.number(), FILESIZE_FLAC: z.coerce.number(), FILESIZE: z.coerce.number(), - GAIN: z.nullable(z.coerce.number()), MEDIA_VERSION: z.string(), TRACK_TOKEN: z.string(), TRACK_TOKEN_EXPIRE: z.number(), @@ -181,6 +175,22 @@ const GwTrack = z.object({ }) type GwTrack = z.infer +const GwAlbum = z.object({ + ALB_ID: z.string(), + ALB_TITLE: z.string(), + ARTISTS: z.array(z.object({ + ART_ID: z.string(), + ART_NAME: z.string(), + })), + COPYRIGHT: z.string(), + PRODUCER_LINE: z.string(), + DIGITAL_RELEASE_DATE: z.string(), + SONGS: z.object({ + total: z.number(), + }).optional(), +}) +type GwAlbum = z.infer + const userData = await gwLightApi({ method: 'deezer.getUserData', result: z.object({ @@ -237,10 +247,10 @@ class BlowfishDecryptTransform implements Transformer { for (let i = 0; i < 16; i++) { bfKey[i] = trackIdMd5[i].charCodeAt(0) - ^ trackIdMd5[i + 16].charCodeAt(0) - ^ (i % 2 ? BLOWFISH_SALT_2 : BLOWFISH_SALT_1)[ - 7 - Math.floor(i / 2) - ] + ^ trackIdMd5[i + 16].charCodeAt(0) + ^ (i % 2 ? BLOWFISH_SALT_2 : BLOWFISH_SALT_1)[ + 7 - Math.floor(i / 2) + ] } this.cipher = new Blowfish(bfKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL) @@ -288,8 +298,22 @@ class BlowfishDecryptTransform implements Transformer { } } +function getTrackArtistString(track: GwTrack) { + if (track.ARTISTS) return track.ARTISTS.map(it => it.ART_NAME).join(', ').slice(0, 100) + return track.ART_NAME +} + +function getTrackName(track: GwTrack) { + let name = track.SNG_TITLE + if (track.VERSION) { + name += ` ${track.VERSION}` + } + return name +} + async function downloadTrack(track: GwTrack, opts: { destination: string + album?: GwAlbum }) { const albumUrl = `https://cdn-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/1500x1500-000000-80-0-0.jpg` const [getUrlRes, albumAb, lyricsRes] = await Promise.all([ @@ -365,23 +389,40 @@ async function downloadTrack(track: GwTrack, opts: { '-c', 'copy', '-metadata', - `title=${track.SNG_TITLE}`, - '-metadata', - `artist=${track.ART_NAME}`, + `title=${getTrackName(track)}`, '-metadata', `album=${track.ALB_TITLE}`, '-metadata', `year=${track.DIGITAL_RELEASE_DATE}`, '-metadata', `comment=ripped from deezer (id: ${track.SNG_ID})`, + '-metadata', + `track=${track.TRACK_NUMBER}`, + '-metadata', + `disc=${track.DISK_NUMBER}`, filename, ] + if (opts.album) { + params.push('-metadata', `album=${opts.album.ALB_TITLE}`) + } + + if (track.SNG_CONTRIBUTORS?.composer) { + for (const composer of track.SNG_CONTRIBUTORS.composer) { + params.push('-metadata', `composer=${composer}`) + } + } + + if (track.ARTISTS?.length) { + for (const artist of track.ARTISTS) { + params.push('-metadata', `artist=${artist.ART_NAME}`) + } + } else { + params.push('-metadata', `artist=${track.ART_NAME}`) + } + if (lyricsLrc) { - params.push( - '-metadata', - `lyrics=${lyricsLrc}`, - ) + await writeFile(`${opts.destination}.lrc`, lyricsLrc) } const proc = $`ffmpeg ${params}` @@ -408,14 +449,44 @@ async function downloadTrack(track: GwTrack, opts: { const params: string[] = [ '--remove-all-tags', - `--set-tag=TITLE=${track.SNG_TITLE}`, - `--set-tag=ARTIST=${track.ART_NAME}`, + `--set-tag=TITLE=${getTrackName(track)}`, `--set-tag=ALBUM=${track.ALB_TITLE}`, - `--set-tag=DATE=${track.DIGITAL_RELEASE_DATE}`, + `--set-tag=DATE=${track.DIGITAL_RELEASE_DATE ?? asNonNull(opts.album?.DIGITAL_RELEASE_DATE)}`, + `--set-tag=DISCNUMBER=${track.DISK_NUMBER}`, + `--set-tag=TRACKNUMBER=${track.TRACK_NUMBER}`, `--set-tag=COMMENT=ripped from deezer (id: ${track.SNG_ID})`, `--import-picture-from=${albumCoverPath}`, ] + if (track.ARTISTS) { + for (const artist of track.ARTISTS) { + params.push(`--set-tag=ARTIST=${artist.ART_NAME}`) + } + } else { + params.push(`--set-tag=ARTIST=${track.ART_NAME}`) + } + + if (track.SNG_CONTRIBUTORS?.composer) { + for (const composer of track.SNG_CONTRIBUTORS.composer) { + params.push(`--set-tag=COMPOSER=${composer}`) + } + } + + if (track.SNG_CONTRIBUTORS?.main_artist) { + for (const mainArtist of track.SNG_CONTRIBUTORS.main_artist) { + params.push(`--set-tag=MAIN_ARTIST=${mainArtist}`) + } + } + + if (track.ISRC) { + params.push(`--set-tag=ISRC=${track.ISRC}`) + } + + if (opts.album) { + params.push(`--set-tag=PRODUCER=${opts.album.PRODUCER_LINE}`) + params.push(`--set-tag=COPYRIGHT=${opts.album.COPYRIGHT}`) + } + params.push(filename) await $`metaflac ${params}` @@ -424,6 +495,159 @@ async function downloadTrack(track: GwTrack, opts: { await rm(albumCoverPath, { force: true }) } +async function downloadTrackList(tracks: GwTrack[], opts: { + album?: GwAlbum + poolLimit?: number + destination: string + includeTrackNumber?: boolean + onDownloadStart?: (track: GwTrack) => void + onDownloadEnd?: (track: GwTrack, error: Error | null) => void +}) { + await mkdir(opts.destination, { recursive: true }) + + const isMultiDisc = tracks.some(it => it.DISK_NUMBER !== '1') + + const firstTrackArtistString = getTrackArtistString(tracks[0]) + const isVariousArtists = tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) + + await asyncPool(tracks, async (track) => { + let filename = '' + if (opts.includeTrackNumber) { + if (isMultiDisc) { + filename = `${track.DISK_NUMBER}-` + } + filename = `${track.TRACK_NUMBER.padStart(2, '0')}. ` + } + if (isVariousArtists) { + filename += `${getTrackArtistString(track)} - ` + } + filename += `${getTrackName(track)}` + + const filenamePath = join(opts.destination, sanitizeFilename(filename)) + + opts.onDownloadStart?.(track) + + try { + await downloadTrack(track, { + destination: filenamePath, + album: opts.album, + }) + opts.onDownloadEnd?.(track, null) + } catch (e) { + opts.onDownloadEnd?.(track, unknownToError(e)) + } + }, { limit: opts.poolLimit }) +} + +const GwPageArtist = z.object({ + DATA: z.object({ + ART_NAME: z.string(), + }), + ALBUMS: z.object({ + data: z.array(GwAlbum), + total: z.number(), + }), +}) + +async function downloadArtist(artistId: string) { + const artistInfo = await gwLightApi({ + method: 'deezer.pageArtist', + token: userData.checkForm, + options: { + art_id: artistId, + lang: 'us', + }, + result: z.object({ + DATA: z.object({ + ART_NAME: z.string(), + }), + ALBUMS: z.object({ + data: z.array(GwAlbum), + total: z.number(), + }), + }), + }) + + const albums: GwAlbum[] = artistInfo.ALBUMS.data + + let trackCount = 0 + const spinnies = new Spinnies() + + if (artistInfo.ALBUMS.total > albums.length) { + // fetch the rest + spinnies.add('collect', { text: 'collecting albums...' }) + let offset = albums.length + while (true) { + const res = await gwLightApi({ + method: 'album.getDiscography', + token: userData.checkForm, + options: { + art_id: artistId, + nb: 25, + nb_songs: 0, + start: offset, + }, + result: z.object({ + data: z.array(GwAlbum), + total: z.number(), + }), + }) + + for (const alb of res.data) { + albums.push(alb) + trackCount += asNonNull(alb.SONGS).total + } + + if (res.total <= offset) break + offset += 25 + } + + spinnies.succeed('collect', { text: `collected ${albums.length} albums with a total of ${trackCount} tracks` }) + } + + // fixme: singles should always contain artist name and be saved in artist root dir + // fixme: "featured" albums (i.e. when main artist of the album is not the one we're dling) should have album artist name in its dirname + // todo: automatic musicbrainz matching + + await asyncPool(albums, async (alb) => { + const tracks = await gwLightApi({ + method: 'song.getListByAlbum', + token: userData.checkForm, + options: { + alb_id: alb.ALB_ID, + nb: -1, + }, + result: z.object({ + data: z.array(GwTrack), + total: z.number(), + }), + }) + assert(tracks.total === asNonNull(alb.SONGS).total) + assert(tracks.data.length === asNonNull(alb.SONGS).total) + + await downloadTrackList(tracks.data, { + destination: join( + 'assets/deezer-dl', + sanitizeFilename(artistInfo.DATA.ART_NAME), + sanitizeFilename(alb.ALB_TITLE), + ), + album: alb, + poolLimit: 4, + includeTrackNumber: true, + onDownloadStart(track) { + spinnies.add(`${track.SNG_ID}`, { text: track.SNG_TITLE }) + }, + onDownloadEnd(track, error) { + if (error) { + spinnies.fail(`${track.SNG_ID}`, { text: error.message }) + } else { + spinnies.remove(`${track.SNG_ID}`) + } + }, + }) + }, { limit: 4 }) +} + async function downloadByUri(uri: string) { const [type, id] = uri.split(':') @@ -447,6 +671,76 @@ async function downloadByUri(uri: string) { destination: join('assets/deezer-dl', sanitizeFilename(filename)), }) } + + if (type === 'album') { + const album = await gwLightApi({ + method: 'deezer.pageAlbum', + token: userData.checkForm, + options: { + alb_id: id, + lang: 'us', + }, + result: z.object({ + DATA: GwAlbum, + SONGS: z.object({ + data: z.array(GwTrack), + total: z.number(), + }), + }), + }) + + const tracks = album.SONGS.data + if (tracks.length < album.SONGS.total) { + // fetch the rest + const res = await gwLightApi({ + method: 'song.getListByAlbum', + token: userData.checkForm, + options: { + alb_id: id, + nb: -1, + start: tracks.length, + }, + result: z.object({ + data: z.array(GwTrack), + total: z.number(), + }), + }) + tracks.push(...res.data) + + assert(tracks.length === album.SONGS.total) + } + + const spinnies = new Spinnies() + spinnies.add('download', { text: 'downloading album...' }) + + await downloadTrackList(tracks, { + destination: join( + 'assets/deezer-dl', + sanitizeFilename( + `${album.DATA.ARTISTS.map(it => it.ART_NAME).join(', ').slice(0, 100)} - ${album.DATA.ALB_TITLE}`, + ), + ), + includeTrackNumber: true, + poolLimit: 8, + album: album.DATA, + onDownloadStart(track) { + spinnies.add(`${track.SNG_ID}`, { text: track.SNG_TITLE }) + }, + onDownloadEnd(track, error) { + if (error) { + spinnies.fail(`${track.SNG_ID}`, { text: error.stack }) + } else { + spinnies.remove(`${track.SNG_ID}`) + } + }, + }) + + spinnies.succeed('download', { text: 'downloaded album' }) + } + + if (type === 'artist') { + await downloadArtist(id) + } } console.log('logged in as %s', userData.USER.BLOG_NAME) From 25d88cb28bb4f0e55549638c138fb435664d97b7 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sun, 11 May 2025 22:29:57 +0000 Subject: [PATCH 42/61] chore: update public repo --- scripts/media/deezer-dl.ts | 344 ++++++++++++++++++++++++++++++++++--- 1 file changed, 319 insertions(+), 25 deletions(-) diff --git a/scripts/media/deezer-dl.ts b/scripts/media/deezer-dl.ts index c30dee7..9797c3b 100644 --- a/scripts/media/deezer-dl.ts +++ b/scripts/media/deezer-dl.ts @@ -4,8 +4,9 @@ import { dirname, join } from 'node:path' import { Readable } from 'node:stream' import { TransformStream } from 'node:stream/web' import { Bytes, read, write } from '@fuman/io' -import { base64, hex, iter, utf8 } from '@fuman/utils' +import { asNonNull, assert, asyncPool, base64, hex, iter, unknownToError, utf8 } from '@fuman/utils' import { Blowfish } from 'egoroof-blowfish' +import Spinnies from 'spinnies' import { CookieJar } from 'tough-cookie' import { FileCookieStore } from 'tough-cookie-file-store' import { z } from 'zod' @@ -130,7 +131,7 @@ const GwTrack = z.object({ ART_ID: z.string(), ART_NAME: z.string(), ARTIST_IS_DUMMY: z.boolean().optional(), - DIGITAL_RELEASE_DATE: z.string(), + DIGITAL_RELEASE_DATE: z.string().optional(), DISK_NUMBER: z.string(), DURATION: z.string(), EXPLICIT_LYRICS: z.string(), @@ -138,24 +139,18 @@ const GwTrack = z.object({ EXPLICIT_LYRICS_STATUS: z.number(), EXPLICIT_COVER_STATUS: z.number(), }), - GENRE_ID: z.string(), - HIERARCHICAL_TITLE: z.string().optional(), - ISRC: z.string(), + ISRC: z.string().optional(), LYRICS_ID: z.number(), - PHYSICAL_RELEASE_DATE: z.string(), PROVIDER_ID: z.string(), - RANK: z.string().optional(), - SMARTRADIO: z.number(), SNG_CONTRIBUTORS: z.object({ main_artist: z.array(z.string()), author: z.array(z.string()), composer: z.array(z.string()), + featuring: z.array(z.string()), }).partial().optional(), SNG_ID: z.string(), SNG_TITLE: z.string(), - STATUS: z.number(), TRACK_NUMBER: z.string(), - USER_ID: z.number(), VERSION: z.string(), MD5_ORIGIN: z.string(), FILESIZE_AAC_64: z.coerce.number(), @@ -168,7 +163,6 @@ const GwTrack = z.object({ FILESIZE_MP4_RA3: z.coerce.number(), FILESIZE_FLAC: z.coerce.number(), FILESIZE: z.coerce.number(), - GAIN: z.nullable(z.coerce.number()), MEDIA_VERSION: z.string(), TRACK_TOKEN: z.string(), TRACK_TOKEN_EXPIRE: z.number(), @@ -181,6 +175,22 @@ const GwTrack = z.object({ }) type GwTrack = z.infer +const GwAlbum = z.object({ + ALB_ID: z.string(), + ALB_TITLE: z.string(), + ARTISTS: z.array(z.object({ + ART_ID: z.string(), + ART_NAME: z.string(), + })), + COPYRIGHT: z.string(), + PRODUCER_LINE: z.string(), + DIGITAL_RELEASE_DATE: z.string(), + SONGS: z.object({ + total: z.number(), + }).optional(), +}) +type GwAlbum = z.infer + const userData = await gwLightApi({ method: 'deezer.getUserData', result: z.object({ @@ -237,10 +247,10 @@ class BlowfishDecryptTransform implements Transformer { for (let i = 0; i < 16; i++) { bfKey[i] = trackIdMd5[i].charCodeAt(0) - ^ trackIdMd5[i + 16].charCodeAt(0) - ^ (i % 2 ? BLOWFISH_SALT_2 : BLOWFISH_SALT_1)[ - 7 - Math.floor(i / 2) - ] + ^ trackIdMd5[i + 16].charCodeAt(0) + ^ (i % 2 ? BLOWFISH_SALT_2 : BLOWFISH_SALT_1)[ + 7 - Math.floor(i / 2) + ] } this.cipher = new Blowfish(bfKey, Blowfish.MODE.CBC, Blowfish.PADDING.NULL) @@ -288,8 +298,22 @@ class BlowfishDecryptTransform implements Transformer { } } +function getTrackArtistString(track: GwTrack) { + if (track.ARTISTS) return track.ARTISTS.map(it => it.ART_NAME).join(', ').slice(0, 100) + return track.ART_NAME +} + +function getTrackName(track: GwTrack) { + let name = track.SNG_TITLE + if (track.VERSION) { + name += ` ${track.VERSION}` + } + return name +} + async function downloadTrack(track: GwTrack, opts: { destination: string + album?: GwAlbum }) { const albumUrl = `https://cdn-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/1500x1500-000000-80-0-0.jpg` const [getUrlRes, albumAb, lyricsRes] = await Promise.all([ @@ -365,23 +389,40 @@ async function downloadTrack(track: GwTrack, opts: { '-c', 'copy', '-metadata', - `title=${track.SNG_TITLE}`, - '-metadata', - `artist=${track.ART_NAME}`, + `title=${getTrackName(track)}`, '-metadata', `album=${track.ALB_TITLE}`, '-metadata', `year=${track.DIGITAL_RELEASE_DATE}`, '-metadata', `comment=ripped from deezer (id: ${track.SNG_ID})`, + '-metadata', + `track=${track.TRACK_NUMBER}`, + '-metadata', + `disc=${track.DISK_NUMBER}`, filename, ] + if (opts.album) { + params.push('-metadata', `album=${opts.album.ALB_TITLE}`) + } + + if (track.SNG_CONTRIBUTORS?.composer) { + for (const composer of track.SNG_CONTRIBUTORS.composer) { + params.push('-metadata', `composer=${composer}`) + } + } + + if (track.ARTISTS?.length) { + for (const artist of track.ARTISTS) { + params.push('-metadata', `artist=${artist.ART_NAME}`) + } + } else { + params.push('-metadata', `artist=${track.ART_NAME}`) + } + if (lyricsLrc) { - params.push( - '-metadata', - `lyrics=${lyricsLrc}`, - ) + await writeFile(`${opts.destination}.lrc`, lyricsLrc) } const proc = $`ffmpeg ${params}` @@ -408,14 +449,44 @@ async function downloadTrack(track: GwTrack, opts: { const params: string[] = [ '--remove-all-tags', - `--set-tag=TITLE=${track.SNG_TITLE}`, - `--set-tag=ARTIST=${track.ART_NAME}`, + `--set-tag=TITLE=${getTrackName(track)}`, `--set-tag=ALBUM=${track.ALB_TITLE}`, - `--set-tag=DATE=${track.DIGITAL_RELEASE_DATE}`, + `--set-tag=DATE=${track.DIGITAL_RELEASE_DATE ?? asNonNull(opts.album?.DIGITAL_RELEASE_DATE)}`, + `--set-tag=DISCNUMBER=${track.DISK_NUMBER}`, + `--set-tag=TRACKNUMBER=${track.TRACK_NUMBER}`, `--set-tag=COMMENT=ripped from deezer (id: ${track.SNG_ID})`, `--import-picture-from=${albumCoverPath}`, ] + if (track.ARTISTS) { + for (const artist of track.ARTISTS) { + params.push(`--set-tag=ARTIST=${artist.ART_NAME}`) + } + } else { + params.push(`--set-tag=ARTIST=${track.ART_NAME}`) + } + + if (track.SNG_CONTRIBUTORS?.composer) { + for (const composer of track.SNG_CONTRIBUTORS.composer) { + params.push(`--set-tag=COMPOSER=${composer}`) + } + } + + if (track.SNG_CONTRIBUTORS?.main_artist) { + for (const mainArtist of track.SNG_CONTRIBUTORS.main_artist) { + params.push(`--set-tag=MAIN_ARTIST=${mainArtist}`) + } + } + + if (track.ISRC) { + params.push(`--set-tag=ISRC=${track.ISRC}`) + } + + if (opts.album) { + params.push(`--set-tag=PRODUCER=${opts.album.PRODUCER_LINE}`) + params.push(`--set-tag=COPYRIGHT=${opts.album.COPYRIGHT}`) + } + params.push(filename) await $`metaflac ${params}` @@ -424,6 +495,159 @@ async function downloadTrack(track: GwTrack, opts: { await rm(albumCoverPath, { force: true }) } +async function downloadTrackList(tracks: GwTrack[], opts: { + album?: GwAlbum + poolLimit?: number + destination: string + includeTrackNumber?: boolean + onDownloadStart?: (track: GwTrack) => void + onDownloadEnd?: (track: GwTrack, error: Error | null) => void +}) { + await mkdir(opts.destination, { recursive: true }) + + const isMultiDisc = tracks.some(it => it.DISK_NUMBER !== '1') + + const firstTrackArtistString = getTrackArtistString(tracks[0]) + const isVariousArtists = tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) + + await asyncPool(tracks, async (track) => { + let filename = '' + if (opts.includeTrackNumber) { + if (isMultiDisc) { + filename = `${track.DISK_NUMBER}-` + } + filename = `${track.TRACK_NUMBER.padStart(2, '0')}. ` + } + if (isVariousArtists) { + filename += `${getTrackArtistString(track)} - ` + } + filename += `${getTrackName(track)}` + + const filenamePath = join(opts.destination, sanitizeFilename(filename)) + + opts.onDownloadStart?.(track) + + try { + await downloadTrack(track, { + destination: filenamePath, + album: opts.album, + }) + opts.onDownloadEnd?.(track, null) + } catch (e) { + opts.onDownloadEnd?.(track, unknownToError(e)) + } + }, { limit: opts.poolLimit }) +} + +const GwPageArtist = z.object({ + DATA: z.object({ + ART_NAME: z.string(), + }), + ALBUMS: z.object({ + data: z.array(GwAlbum), + total: z.number(), + }), +}) + +async function downloadArtist(artistId: string) { + const artistInfo = await gwLightApi({ + method: 'deezer.pageArtist', + token: userData.checkForm, + options: { + art_id: artistId, + lang: 'us', + }, + result: z.object({ + DATA: z.object({ + ART_NAME: z.string(), + }), + ALBUMS: z.object({ + data: z.array(GwAlbum), + total: z.number(), + }), + }), + }) + + const albums: GwAlbum[] = artistInfo.ALBUMS.data + + let trackCount = 0 + const spinnies = new Spinnies() + + if (artistInfo.ALBUMS.total > albums.length) { + // fetch the rest + spinnies.add('collect', { text: 'collecting albums...' }) + let offset = albums.length + while (true) { + const res = await gwLightApi({ + method: 'album.getDiscography', + token: userData.checkForm, + options: { + art_id: artistId, + nb: 25, + nb_songs: 0, + start: offset, + }, + result: z.object({ + data: z.array(GwAlbum), + total: z.number(), + }), + }) + + for (const alb of res.data) { + albums.push(alb) + trackCount += asNonNull(alb.SONGS).total + } + + if (res.total <= offset) break + offset += 25 + } + + spinnies.succeed('collect', { text: `collected ${albums.length} albums with a total of ${trackCount} tracks` }) + } + + // fixme: singles should always contain artist name and be saved in artist root dir + // fixme: "featured" albums (i.e. when main artist of the album is not the one we're dling) should have album artist name in its dirname + // todo: automatic musicbrainz matching + + await asyncPool(albums, async (alb) => { + const tracks = await gwLightApi({ + method: 'song.getListByAlbum', + token: userData.checkForm, + options: { + alb_id: alb.ALB_ID, + nb: -1, + }, + result: z.object({ + data: z.array(GwTrack), + total: z.number(), + }), + }) + assert(tracks.total === asNonNull(alb.SONGS).total) + assert(tracks.data.length === asNonNull(alb.SONGS).total) + + await downloadTrackList(tracks.data, { + destination: join( + 'assets/deezer-dl', + sanitizeFilename(artistInfo.DATA.ART_NAME), + sanitizeFilename(alb.ALB_TITLE), + ), + album: alb, + poolLimit: 4, + includeTrackNumber: true, + onDownloadStart(track) { + spinnies.add(`${track.SNG_ID}`, { text: track.SNG_TITLE }) + }, + onDownloadEnd(track, error) { + if (error) { + spinnies.fail(`${track.SNG_ID}`, { text: error.message }) + } else { + spinnies.remove(`${track.SNG_ID}`) + } + }, + }) + }, { limit: 4 }) +} + async function downloadByUri(uri: string) { const [type, id] = uri.split(':') @@ -447,6 +671,76 @@ async function downloadByUri(uri: string) { destination: join('assets/deezer-dl', sanitizeFilename(filename)), }) } + + if (type === 'album') { + const album = await gwLightApi({ + method: 'deezer.pageAlbum', + token: userData.checkForm, + options: { + alb_id: id, + lang: 'us', + }, + result: z.object({ + DATA: GwAlbum, + SONGS: z.object({ + data: z.array(GwTrack), + total: z.number(), + }), + }), + }) + + const tracks = album.SONGS.data + if (tracks.length < album.SONGS.total) { + // fetch the rest + const res = await gwLightApi({ + method: 'song.getListByAlbum', + token: userData.checkForm, + options: { + alb_id: id, + nb: -1, + start: tracks.length, + }, + result: z.object({ + data: z.array(GwTrack), + total: z.number(), + }), + }) + tracks.push(...res.data) + + assert(tracks.length === album.SONGS.total) + } + + const spinnies = new Spinnies() + spinnies.add('download', { text: 'downloading album...' }) + + await downloadTrackList(tracks, { + destination: join( + 'assets/deezer-dl', + sanitizeFilename( + `${album.DATA.ARTISTS.map(it => it.ART_NAME).join(', ').slice(0, 100)} - ${album.DATA.ALB_TITLE}`, + ), + ), + includeTrackNumber: true, + poolLimit: 8, + album: album.DATA, + onDownloadStart(track) { + spinnies.add(`${track.SNG_ID}`, { text: track.SNG_TITLE }) + }, + onDownloadEnd(track, error) { + if (error) { + spinnies.fail(`${track.SNG_ID}`, { text: error.stack }) + } else { + spinnies.remove(`${track.SNG_ID}`) + } + }, + }) + + spinnies.succeed('download', { text: 'downloaded album' }) + } + + if (type === 'artist') { + await downloadArtist(id) + } } console.log('logged in as %s', userData.USER.BLOG_NAME) From c0956d52eab3984ec02fe0a2dff707a8b525525d Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 14 May 2025 09:39:22 +0000 Subject: [PATCH 43/61] chore: update public repo --- utils/csv.ts | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 utils/csv.ts diff --git a/utils/csv.ts b/utils/csv.ts new file mode 100644 index 0000000..609848b --- /dev/null +++ b/utils/csv.ts @@ -0,0 +1,147 @@ +import { FramedReader, type IReadable, TextDelimiterCodec } from '@fuman/io' + +interface CsvReaderOptions { + /** @default '\n' */ + lineDelimiter: string + /** @default ',' */ + delimiter: string + /** @default '"' */ + quote: string + /** @default '"' */ + quoteEscape: string + + /** + * if true, missing values in a line will be treated as empty strings + * @default false + */ + assumeEmptyValues: boolean + + /** whether to treat header line as a data line */ + includeHeader: boolean +} + +export class CsvReader { + #codec: FramedReader + readonly options: CsvReaderOptions + #schema?: Fields + constructor( + stream: IReadable, + options: Partial & { + /** fields that are expected in the csv */ + schema?: Fields + }, + ) { + this.options = { + lineDelimiter: '\n', + delimiter: ',', + quote: '"', + quoteEscape: '"', + assumeEmptyValues: false, + includeHeader: false, + ...options, + } + + this.#codec = new FramedReader(stream, new TextDelimiterCodec(this.options.lineDelimiter)) + this.#schema = options.schema + + if (options.includeHeader) { + if (!options.schema) throw new Error('schema is required if includeHeader is true') + this.#header = options.schema + } + } + + #header?: string[] + + async read(): Promise | null> { + let line = await this.#codec.read() + if (!line) return null + + line = line.trim() + if (line === '') return this.read() + + if (!this.#header) { + this.#header = line.split(this.options.delimiter).map(s => s.trim()) + if (JSON.stringify(this.#schema!) !== JSON.stringify(this.#header)) { + throw new Error(`schema and header are the same (expected ${this.#schema!.join(', ')}; got ${this.#header.join(', ')})`) + } + return this.read() + } + + const obj: Record = {} + + let insideQuote = false + let currentFieldIdx = 0 + let currentValue = '' + for (let i = 0; i < line.length; i++) { + if (line[i] === this.options.quoteEscape) { + if (insideQuote && line[i + 1] === this.options.quote) { + i++ + currentValue += this.options.quote + continue + } + } + + if (line[i] === this.options.quote) { + if (!insideQuote) { + if (currentValue !== '') { + throw new Error('unexpected open quote mid-value') + } + insideQuote = true + continue + } + + if (i !== line.length - 1 && line[i + 1] !== this.options.delimiter) { + console.log(i, line.length, line[i + 1]) + throw new Error(`unexpected close quote mid-value at ${i}`) + } + + insideQuote = false + continue + } + + if (insideQuote) { + currentValue += line[i] + continue + } + + if (line[i] === this.options.delimiter) { + obj[this.#header[currentFieldIdx]] = currentValue + currentFieldIdx += 1 + currentValue = '' + if (currentFieldIdx > this.#header.length) { + throw new Error('too many fields') + } + continue + } + + currentValue += line[i] + } + + obj[this.#header[currentFieldIdx++]] = currentValue + + if (currentFieldIdx < this.#header.length) { + if (this.options.assumeEmptyValues) { + for (let i = currentFieldIdx; i < this.#header.length; i++) { + obj[this.#header[i]] = '' + } + } else { + throw new Error(`missing values for fields: ${this.#header.slice(currentFieldIdx).join(', ')}`) + } + } + + return obj as Record + } + + [Symbol.asyncIterator]() { + const iter: AsyncIterableIterator> = { + next: async () => { + const obj = await this.read() + if (!obj) return { done: true, value: undefined } + return { done: false, value: obj } + }, + [Symbol.asyncIterator]: () => iter, + } + + return iter + } +} From 8c04afc6d2c2d5b8fdc9eac17648efdfc30989f4 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 14 May 2025 09:39:22 +0000 Subject: [PATCH 44/61] chore: update public repo --- utils/csv.ts | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 utils/csv.ts diff --git a/utils/csv.ts b/utils/csv.ts new file mode 100644 index 0000000..609848b --- /dev/null +++ b/utils/csv.ts @@ -0,0 +1,147 @@ +import { FramedReader, type IReadable, TextDelimiterCodec } from '@fuman/io' + +interface CsvReaderOptions { + /** @default '\n' */ + lineDelimiter: string + /** @default ',' */ + delimiter: string + /** @default '"' */ + quote: string + /** @default '"' */ + quoteEscape: string + + /** + * if true, missing values in a line will be treated as empty strings + * @default false + */ + assumeEmptyValues: boolean + + /** whether to treat header line as a data line */ + includeHeader: boolean +} + +export class CsvReader { + #codec: FramedReader + readonly options: CsvReaderOptions + #schema?: Fields + constructor( + stream: IReadable, + options: Partial & { + /** fields that are expected in the csv */ + schema?: Fields + }, + ) { + this.options = { + lineDelimiter: '\n', + delimiter: ',', + quote: '"', + quoteEscape: '"', + assumeEmptyValues: false, + includeHeader: false, + ...options, + } + + this.#codec = new FramedReader(stream, new TextDelimiterCodec(this.options.lineDelimiter)) + this.#schema = options.schema + + if (options.includeHeader) { + if (!options.schema) throw new Error('schema is required if includeHeader is true') + this.#header = options.schema + } + } + + #header?: string[] + + async read(): Promise | null> { + let line = await this.#codec.read() + if (!line) return null + + line = line.trim() + if (line === '') return this.read() + + if (!this.#header) { + this.#header = line.split(this.options.delimiter).map(s => s.trim()) + if (JSON.stringify(this.#schema!) !== JSON.stringify(this.#header)) { + throw new Error(`schema and header are the same (expected ${this.#schema!.join(', ')}; got ${this.#header.join(', ')})`) + } + return this.read() + } + + const obj: Record = {} + + let insideQuote = false + let currentFieldIdx = 0 + let currentValue = '' + for (let i = 0; i < line.length; i++) { + if (line[i] === this.options.quoteEscape) { + if (insideQuote && line[i + 1] === this.options.quote) { + i++ + currentValue += this.options.quote + continue + } + } + + if (line[i] === this.options.quote) { + if (!insideQuote) { + if (currentValue !== '') { + throw new Error('unexpected open quote mid-value') + } + insideQuote = true + continue + } + + if (i !== line.length - 1 && line[i + 1] !== this.options.delimiter) { + console.log(i, line.length, line[i + 1]) + throw new Error(`unexpected close quote mid-value at ${i}`) + } + + insideQuote = false + continue + } + + if (insideQuote) { + currentValue += line[i] + continue + } + + if (line[i] === this.options.delimiter) { + obj[this.#header[currentFieldIdx]] = currentValue + currentFieldIdx += 1 + currentValue = '' + if (currentFieldIdx > this.#header.length) { + throw new Error('too many fields') + } + continue + } + + currentValue += line[i] + } + + obj[this.#header[currentFieldIdx++]] = currentValue + + if (currentFieldIdx < this.#header.length) { + if (this.options.assumeEmptyValues) { + for (let i = currentFieldIdx; i < this.#header.length; i++) { + obj[this.#header[i]] = '' + } + } else { + throw new Error(`missing values for fields: ${this.#header.slice(currentFieldIdx).join(', ')}`) + } + } + + return obj as Record + } + + [Symbol.asyncIterator]() { + const iter: AsyncIterableIterator> = { + next: async () => { + const obj = await this.read() + if (!obj) return { done: true, value: undefined } + return { done: false, value: obj } + }, + [Symbol.asyncIterator]: () => iter, + } + + return iter + } +} From db2be56aa366d3304f32989a508ddaf51c21f319 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 14 May 2025 09:48:24 +0000 Subject: [PATCH 45/61] chore: update public repo --- scripts/media/lastfm-csv-to-sqlite.ts | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 scripts/media/lastfm-csv-to-sqlite.ts diff --git a/scripts/media/lastfm-csv-to-sqlite.ts b/scripts/media/lastfm-csv-to-sqlite.ts new file mode 100644 index 0000000..b4b9005 --- /dev/null +++ b/scripts/media/lastfm-csv-to-sqlite.ts @@ -0,0 +1,60 @@ +import { createReadStream } from 'node:fs' +import { nodeReadableToFuman } from '@fuman/node' +import Database from 'better-sqlite3' +import { question } from 'zx' +import { CsvReader } from '../../utils/csv.ts' + +const csvPath = process.argv[2] ?? await question('path to csv > ') + +// convert csv generated by https://mainstream.ghan.nl/export.html to an sqlite database + +const db = new Database('assets/lastfm-import.db') +db.exec(` + CREATE TABLE IF NOT EXISTS scrobbles ( + date_uts TEXT, + artist_mbid TEXT, + artist_name TEXT, + album_mbid TEXT, + album_name TEXT, + track_mbid TEXT, + track_name TEXT + ); +`) + +const insertQuery = db.prepare(` + INSERT INTO scrobbles ( + date_uts, + artist_mbid, + artist_name, + album_mbid, + album_name, + track_mbid, + track_name + ) VALUES (?, ?, ?, ?, ?, ?, ?) +`) + +const file = nodeReadableToFuman(createReadStream(csvPath)) +const csv = new CsvReader(file, { + schema: ['uts', 'utc_time', 'artist', 'artist_mbid', 'album', 'album_mbid', 'track', 'track_mbid'], +}) + +let i = 0 +while (true) { + const obj = await csv.read() + if (!obj) break + + i += 1 + if (i % 1000 === 0) { + console.log('inserted', i) + } + + insertQuery.run( + obj.uts, + obj.artist_mbid, + obj.artist, + obj.album_mbid, + obj.album, + obj.track_mbid, + obj.track, + ) +} From 3057b2a78c35153588758af5931d676b75d2bab6 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 14 May 2025 09:48:24 +0000 Subject: [PATCH 46/61] chore: update public repo --- scripts/media/lastfm-csv-to-sqlite.ts | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 scripts/media/lastfm-csv-to-sqlite.ts diff --git a/scripts/media/lastfm-csv-to-sqlite.ts b/scripts/media/lastfm-csv-to-sqlite.ts new file mode 100644 index 0000000..b4b9005 --- /dev/null +++ b/scripts/media/lastfm-csv-to-sqlite.ts @@ -0,0 +1,60 @@ +import { createReadStream } from 'node:fs' +import { nodeReadableToFuman } from '@fuman/node' +import Database from 'better-sqlite3' +import { question } from 'zx' +import { CsvReader } from '../../utils/csv.ts' + +const csvPath = process.argv[2] ?? await question('path to csv > ') + +// convert csv generated by https://mainstream.ghan.nl/export.html to an sqlite database + +const db = new Database('assets/lastfm-import.db') +db.exec(` + CREATE TABLE IF NOT EXISTS scrobbles ( + date_uts TEXT, + artist_mbid TEXT, + artist_name TEXT, + album_mbid TEXT, + album_name TEXT, + track_mbid TEXT, + track_name TEXT + ); +`) + +const insertQuery = db.prepare(` + INSERT INTO scrobbles ( + date_uts, + artist_mbid, + artist_name, + album_mbid, + album_name, + track_mbid, + track_name + ) VALUES (?, ?, ?, ?, ?, ?, ?) +`) + +const file = nodeReadableToFuman(createReadStream(csvPath)) +const csv = new CsvReader(file, { + schema: ['uts', 'utc_time', 'artist', 'artist_mbid', 'album', 'album_mbid', 'track', 'track_mbid'], +}) + +let i = 0 +while (true) { + const obj = await csv.read() + if (!obj) break + + i += 1 + if (i % 1000 === 0) { + console.log('inserted', i) + } + + insertQuery.run( + obj.uts, + obj.artist_mbid, + obj.artist, + obj.album_mbid, + obj.album, + obj.track_mbid, + obj.track, + ) +} From 455ec08c01947e6dcf563ac2e5a07da3020c1d37 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 18 Jun 2025 20:26:53 +0000 Subject: [PATCH 47/61] chore: update public repo --- utils/csv.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/csv.ts b/utils/csv.ts index 609848b..6075533 100644 --- a/utils/csv.ts +++ b/utils/csv.ts @@ -16,8 +16,8 @@ interface CsvReaderOptions { */ assumeEmptyValues: boolean - /** whether to treat header line as a data line */ - includeHeader: boolean + /** whether to treat all data from the readable as data (requires `schema` to be set) */ + skipHeader: boolean } export class CsvReader { @@ -29,7 +29,7 @@ export class CsvReader { options: Partial & { /** fields that are expected in the csv */ schema?: Fields - }, + } = {}, ) { this.options = { lineDelimiter: '\n', @@ -37,14 +37,14 @@ export class CsvReader { quote: '"', quoteEscape: '"', assumeEmptyValues: false, - includeHeader: false, + skipHeader: false, ...options, } this.#codec = new FramedReader(stream, new TextDelimiterCodec(this.options.lineDelimiter)) this.#schema = options.schema - if (options.includeHeader) { + if (options.skipHeader) { if (!options.schema) throw new Error('schema is required if includeHeader is true') this.#header = options.schema } From 2b73e3b41141b61eb5fbb650ce58807fc2336779 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 18 Jun 2025 20:26:53 +0000 Subject: [PATCH 48/61] chore: update public repo --- utils/csv.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/csv.ts b/utils/csv.ts index 609848b..6075533 100644 --- a/utils/csv.ts +++ b/utils/csv.ts @@ -16,8 +16,8 @@ interface CsvReaderOptions { */ assumeEmptyValues: boolean - /** whether to treat header line as a data line */ - includeHeader: boolean + /** whether to treat all data from the readable as data (requires `schema` to be set) */ + skipHeader: boolean } export class CsvReader { @@ -29,7 +29,7 @@ export class CsvReader { options: Partial & { /** fields that are expected in the csv */ schema?: Fields - }, + } = {}, ) { this.options = { lineDelimiter: '\n', @@ -37,14 +37,14 @@ export class CsvReader { quote: '"', quoteEscape: '"', assumeEmptyValues: false, - includeHeader: false, + skipHeader: false, ...options, } this.#codec = new FramedReader(stream, new TextDelimiterCodec(this.options.lineDelimiter)) this.#schema = options.schema - if (options.includeHeader) { + if (options.skipHeader) { if (!options.schema) throw new Error('schema is required if includeHeader is true') this.#header = options.schema } From 56472e5520e2ebed9ac716bc2fd75b6ecde53fcb Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 6 Aug 2025 17:30:02 +0000 Subject: [PATCH 49/61] chore: update public repo From ccc5f98f342f99eae1fc84af1bcb6392a6544584 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 6 Aug 2025 17:30:02 +0000 Subject: [PATCH 50/61] chore: update public repo From 6c1fe8a13cac5346485831ce02c9666c03de88e1 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Thu, 14 Aug 2025 09:21:11 +0000 Subject: [PATCH 51/61] chore: update public repo --- package.json | 10 + pnpm-lock.yaml | 1464 ++++++++++++++++- scripts/infra/navidrome/find-untagged-mbz.ts | 22 + .../navidrome/find-untagged-multiartists.ts | 21 + scripts/media/deezer-dl.ts | 104 +- utils/captcha.ts | 87 - utils/navidrome.ts | 12 +- 7 files changed, 1625 insertions(+), 95 deletions(-) create mode 100644 scripts/infra/navidrome/find-untagged-mbz.ts create mode 100644 scripts/infra/navidrome/find-untagged-multiartists.ts delete mode 100644 utils/captcha.ts diff --git a/package.json b/package.json index 3269071..43e33d7 100644 --- a/package.json +++ b/package.json @@ -11,20 +11,28 @@ "@types/better-sqlite3": "^7.6.12", "@types/plist": "^3.0.5", "@types/spinnies": "^0.5.3", + "babel-generator": "^6.26.1", + "babel-traverse": "^6.26.0", + "babylon": "^6.18.0", "better-sqlite3": "^11.8.1", "canvas": "^3.1.0", "cheerio": "^1.0.0", "egoroof-blowfish": "4.0.1", "es-main": "^1.3.0", "filesize": "^10.1.6", + "imapflow": "^1.0.193", "json5": "^2.2.3", "kuromoji": "^0.1.2", + "mailparser": "^3.7.4", "nanoid": "^5.0.9", + "node-libcurl-ja3": "^5.0.3", + "patchright": "^1.52.5", "plist": "^3.1.0", "qrcode-terminal": "^0.12.0", "spinnies": "^0.5.1", "tough-cookie": "^5.0.0", "tough-cookie-file-store": "^2.0.3", + "ts-morph": "^26.0.0", "tsx": "^4.19.2", "undici": "^7.2.0", "wanakana": "^5.3.1" @@ -33,6 +41,7 @@ "@antfu/eslint-config": "3.10.0", "@fuman/fetch": "0.1.0", "@fuman/utils": "0.0.14", + "@types/mailparser": "^3.4.6", "@types/node": "22.10.0", "domhandler": "^5.0.3", "dotenv": "16.4.5", @@ -43,6 +52,7 @@ "pnpm": { "onlyBuiltDependencies": [ "better-sqlite3", + "node-libcurl-ja3", "canvas" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 871673a..e95a7c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,15 @@ importers: '@types/spinnies': specifier: ^0.5.3 version: 0.5.3 + babel-generator: + specifier: ^6.26.1 + version: 6.26.1 + babel-traverse: + specifier: ^6.26.0 + version: 6.26.0 + babylon: + specifier: ^6.18.0 + version: 6.18.0 better-sqlite3: specifier: ^11.8.1 version: 11.8.1 @@ -50,15 +59,27 @@ importers: filesize: specifier: ^10.1.6 version: 10.1.6 + imapflow: + specifier: ^1.0.193 + version: 1.0.193 json5: specifier: ^2.2.3 version: 2.2.3 kuromoji: specifier: ^0.1.2 version: 0.1.2 + mailparser: + specifier: ^3.7.4 + version: 3.7.4 nanoid: specifier: ^5.0.9 version: 5.0.9 + node-libcurl-ja3: + specifier: ^5.0.3 + version: 5.0.3(encoding@0.1.13) + patchright: + specifier: ^1.52.5 + version: 1.52.5 plist: specifier: ^3.1.0 version: 3.1.0 @@ -74,6 +95,9 @@ importers: tough-cookie-file-store: specifier: ^2.0.3 version: 2.0.3 + ts-morph: + specifier: ^26.0.0 + version: 26.0.0 tsx: specifier: ^4.19.2 version: 4.19.2 @@ -93,6 +117,9 @@ importers: '@fuman/utils': specifier: 0.0.14 version: 0.0.14 + '@types/mailparser': + specifier: ^3.4.6 + version: 3.4.6 '@types/node': specifier: 22.10.0 version: 22.10.0 @@ -467,9 +494,25 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + '@mtcute/core@0.19.1': resolution: {integrity: sha512-Xfq1T03kNRcyomkQl1d426piCnbgKZiYV2z/rM4BBrTIMDwGdxUpp66FPTj7N+MHm1/bVoJNFTUO67nBrLmqdA==} @@ -506,16 +549,34 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@npmcli/agent@2.2.2': + resolution: {integrity: sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@npmcli/fs@3.1.1': + resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@stylistic/eslint-plugin@2.11.0': resolution: {integrity: sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=8.40.0' + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + '@types/better-sqlite3@7.6.12': resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} @@ -537,6 +598,9 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/mailparser@3.4.6': + resolution: {integrity: sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -652,6 +716,13 @@ packages: resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -662,9 +733,25 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -673,6 +760,14 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -681,16 +776,59 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + are-we-there-yet@4.0.2: + resolution: {integrity: sha512-ncSWAawFhKMJDTdoAeOV+jyW1VCMj5QIAwULIBV0SSR7B/RLPPEQiknKcg/RIIZlUQrxELpsxMiTUoAQ4sIUyg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + babel-code-frame@6.26.0: + resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==} + + babel-generator@6.26.1: + resolution: {integrity: sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==} + + babel-messages@6.23.0: + resolution: {integrity: sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==} + + babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + + babel-traverse@6.26.0: + resolution: {integrity: sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==} + + babel-types@6.26.0: + resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==} + + babylon@6.18.0: + resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} + hasBin: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -734,6 +872,10 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + cacache@18.0.4: + resolution: {integrity: sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==} + engines: {node: ^16.14.0 || >=18.0.0} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -748,6 +890,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -769,6 +915,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + ci-info@4.1.0: resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} engines: {node: '>=8'} @@ -777,6 +927,10 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -785,6 +939,9 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -798,6 +955,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + comment-parser@1.4.1: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} @@ -808,9 +969,16 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + core-js-compat@3.39.0: resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -827,6 +995,14 @@ packages: engines: {node: '>=4'} hasBin: true + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -844,6 +1020,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -858,10 +1043,21 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-indent@4.0.0: + resolution: {integrity: sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==} + engines: {node: '>=0.10.0'} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -906,6 +1102,9 @@ packages: doublearray@0.0.2: resolution: {integrity: sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + egoroof-blowfish@4.0.1: resolution: {integrity: sha512-e2gdfyLZrDvyuHX2NkRPFxEuYCIddp+W3MucqjBw9h9nineZUWynuuqQh+R5sNT9IVopPFBHwcnBYHqTcB1Vdw==} @@ -915,9 +1114,19 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding-japanese@2.2.0: + resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==} + engines: {node: '>=8.10.0'} + encoding-sniffer@0.2.0: resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -936,6 +1145,13 @@ packages: resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} engines: {node: '>=0.12'} + env-paths@2.2.0: + resolution: {integrity: sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -1157,6 +1373,9 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + exponential-backoff@3.1.2: + resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1164,12 +1383,20 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -1207,9 +1434,29 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1218,6 +1465,16 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + gauge@5.0.2: + resolution: {integrity: sha512-pMaFftXPtiGIHCJHdcUUx9Rby/rFT/Kkt3fIIGCs+9PMDIljSyRiqraTlxNtBReJRDfUefpa263RQ3vnp5G/LQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1236,6 +1493,14 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -1248,12 +1513,20 @@ packages: resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} engines: {node: '>=18'} + globals@9.18.0: + resolution: {integrity: sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==} + engines: {node: '>=0.10.0'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1262,22 +1535,51 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1289,6 +1591,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + imapflow@1.0.193: + resolution: {integrity: sha512-q5saDQTYM1RrUMm/LOWQ42XnBtKqpJc4gwXOz8ki6k7xA6DUQ6pP+Cbj7t7p+L4vRvcBbNF6ukpuxiIuEGYtKQ==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -1301,12 +1606,23 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1322,6 +1638,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finite@1.1.0: + resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -1330,6 +1650,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1337,6 +1660,16 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-tokens@3.0.2: + resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1344,6 +1677,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsdoc-type-pratt-parser@4.1.0: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} @@ -1352,6 +1688,10 @@ packages: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true + jsesc@1.3.0: + resolution: {integrity: sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==} + hasBin: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -1384,13 +1724,28 @@ packages: kuromoji@0.1.2: resolution: {integrity: sha512-V0dUf+C2LpcPEXhoHLMAop/bOht16Dyr+mDiIE39yX3vqau7p80De/koFqpiTcL1zzdZlc3xuHZ8u5gjYRfFaQ==} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libbase64@1.3.0: + resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} + + libmime@5.3.7: + resolution: {integrity: sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==} + + libqp@2.1.1: + resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + local-pkg@0.5.1: resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} engines: {node: '>=14'} @@ -1415,9 +1770,33 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string@0.30.14: resolution: {integrity: sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==} + mailparser@3.7.4: + resolution: {integrity: sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==} + + mailsplit@5.4.5: + resolution: {integrity: sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==} + + mailsplit@5.4.6: + resolution: {integrity: sha512-M+cqmzaPG/mEiCDmqQUz8L177JZLZmXAUpq38owtpq2xlXlTSw+kntnxRt2xsxVFFV6+T8Mj/U0l5s7s6e0rNw==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-fetch-happen@13.0.1: + resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==} + engines: {node: ^16.14.0 || >=18.0.0} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -1558,6 +1937,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1568,15 +1951,63 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@3.0.5: + resolution: {integrity: sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.7.3: resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nan@https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e: + resolution: {tarball: https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e} + version: 2.22.0 + nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1597,6 +2028,10 @@ packages: resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} engines: {node: '>=18'} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + node-abi@3.71.0: resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} engines: {node: '>=10'} @@ -1604,15 +2039,69 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp@10.2.0: + resolution: {integrity: sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==} + engines: {node: ^16.14.0 || >=18.0.0} + hasBin: true + + node-libcurl-ja3@5.0.3: + resolution: {integrity: sha512-GTSkrcktAPbcSm/3GiAd8J2zbERSxsDD/8Lf4/tobMoIAQw1aLmtOqadDBE/GpFNRCyhMNSVg0xTDDvuB8Blhg==} + engines: {node: '>=20'} + os: [darwin, linux] + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nodemailer@7.0.4: + resolution: {integrity: sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==} + engines: {node: '>=6.0.0'} + + nodemailer@7.0.5: + resolution: {integrity: sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==} + engines: {node: '>=6.0.0'} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + npmlog@7.0.1: + resolution: {integrity: sha512-uJ0YFk/mCQpLBt+bxN88AKd+gyqZvZDbtiNxk6Waqcj2aPRyfVx8ITawkyQynxUagInjdYT1+qj4NfA5KJJUxg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1640,10 +2129,17 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.5: resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==} @@ -1672,10 +2168,30 @@ packages: parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + patchright-core@1.52.5: + resolution: {integrity: sha512-8rnLVEK9jDZWzFPy2hCQrp4xhU7zgE8IqseZyjGkgxf+jpAWTuGNgIAlcsKZMfQrDL8j1mZgRIGNAQT00nk6QA==} + engines: {node: '>=18'} + hasBin: true + + patchright@1.52.5: + resolution: {integrity: sha512-wmRpsF9n02j0S+YDk0U3ouuWipHbUowwxbf/4K4G9ng311vvugoo8WndbU/fCsGtme8gYNfcEGcpfF6/L1NXHg==} + engines: {node: '>=18'} + hasBin: true + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1683,9 +2199,16 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1697,6 +2220,16 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + pkg-types@1.2.1: resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} @@ -1725,12 +2258,27 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1745,6 +2293,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -1761,10 +2312,17 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -1777,6 +2335,10 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true + repeating@2.0.1: + resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==} + engines: {node: '>=0.10.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1799,16 +2361,34 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1816,15 +2396,25 @@ packages: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1836,6 +2426,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1848,10 +2442,29 @@ packages: slashes@3.0.12: resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.6: + resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -1870,6 +2483,17 @@ packages: spinnies@0.5.1: resolution: {integrity: sha512-WpjSXv9NQz0nU3yCT9TFEOfpFrXADY9C5fG6eAJqixLhvTX1jP3w92Y8IE5oafIe42nlF9otjhllnXN/QCaB3A==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + ssri@10.0.6: + resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} @@ -1877,9 +2501,17 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + strip-ansi@5.2.0: resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} engines: {node: '>=6'} @@ -1888,6 +2520,10 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -1900,6 +2536,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -1931,9 +2571,20 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tlds@1.259.0: + resolution: {integrity: sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==} + hasBin: true + tldts-core@6.1.66: resolution: {integrity: sha512-s07jJruSwndD2X8bVjwioPfqpIc1pDTzszPe9pL1Skbh4bjytL85KNQ3tolqLbCvpQHawIsGfFi9dgerWjqW4g==} @@ -1941,6 +2592,10 @@ packages: resolution: {integrity: sha512-l3ciXsYFel/jSRfESbyKYud1nOw7WfhrBEF9I3UiarYk/qEaOOwu3qXNECHw4fHGHGTEOuhf/VdKgoDX5M/dhQ==} hasBin: true + to-fast-properties@1.0.3: + resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==} + engines: {node: '>=0.10.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1961,12 +2616,25 @@ packages: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + trim-right@1.0.1: + resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} + engines: {node: '>=0.10.0'} + ts-api-utils@1.4.2: resolution: {integrity: sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1999,6 +2667,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -2013,6 +2684,14 @@ packages: resolution: {integrity: sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==} engines: {node: '>=20.18.1'} + unique-filename@3.0.0: + resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + unique-slug@4.0.0: + resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -2057,6 +2736,9 @@ packages: resolution: {integrity: sha512-OSDqupzTlzl2LGyqTdhcXcl6ezMiFhcUwLBP8YKaBIbMYW1wAwDvupw2T9G9oVaKT9RmaSpyTXjxddFPUcFFIw==} engines: {node: '>=12'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2065,11 +2747,22 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2078,6 +2771,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2093,6 +2790,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml-eslint-parser@1.2.3: resolution: {integrity: sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==} engines: {node: ^14.17.0 || >=16.0.0} @@ -2320,7 +3020,7 @@ snapshots: '@eslint/config-array@0.19.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.7 + debug: 4.4.1 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -2330,7 +3030,7 @@ snapshots: '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.4.1 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -2409,8 +3109,38 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/sourcemap-codec@1.5.0': {} + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.3 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + '@mtcute/core@0.19.1': dependencies: '@fuman/io': 0.0.4 @@ -2473,8 +3203,30 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@npmcli/agent@2.2.2': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 10.4.3 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@3.1.1': + dependencies: + semver: 7.6.3 + + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.1.1': {} + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@stylistic/eslint-plugin@2.11.0(eslint@9.15.0)(typescript@5.7.2)': dependencies: '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) @@ -2487,6 +3239,12 @@ snapshots: - supports-color - typescript + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.0.3 + path-browserify: 1.0.1 + '@types/better-sqlite3@7.6.12': dependencies: '@types/node': 22.10.0 @@ -2512,6 +3270,11 @@ snapshots: '@types/node': 22.10.0 optional: true + '@types/mailparser@3.4.6': + dependencies: + '@types/node': 22.10.0 + iconv-lite: 0.6.3 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -2656,12 +3419,29 @@ snapshots: '@xmldom/xmldom@0.8.10': {} + abbrev@1.1.1: {} + + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 acorn@8.14.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -2669,10 +3449,16 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@2.1.1: {} + ansi-regex@4.1.1: {} ansi-regex@5.0.1: {} + ansi-regex@6.1.0: {} + + ansi-styles@2.2.1: {} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 @@ -2681,14 +3467,76 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.1: {} + + aproba@2.1.0: {} + are-docs-informative@0.0.2: {} + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + are-we-there-yet@4.0.2: {} + argparse@2.0.1: {} async@2.6.4: dependencies: lodash: 4.17.21 + atomic-sleep@1.0.0: {} + + babel-code-frame@6.26.0: + dependencies: + chalk: 1.1.3 + esutils: 2.0.3 + js-tokens: 3.0.2 + + babel-generator@6.26.1: + dependencies: + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + detect-indent: 4.0.0 + jsesc: 1.3.0 + lodash: 4.17.21 + source-map: 0.5.7 + trim-right: 1.0.1 + + babel-messages@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-runtime@6.26.0: + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + + babel-traverse@6.26.0: + dependencies: + babel-code-frame: 6.26.0 + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + debug: 2.6.9 + globals: 9.18.0 + invariant: 2.2.4 + lodash: 4.17.21 + transitivePeerDependencies: + - supports-color + + babel-types@6.26.0: + dependencies: + babel-runtime: 6.26.0 + esutils: 2.0.3 + lodash: 4.17.21 + to-fast-properties: 1.0.3 + + babylon@6.18.0: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -2742,6 +3590,21 @@ snapshots: builtin-modules@3.3.0: {} + cacache@18.0.4: + dependencies: + '@npmcli/fs': 3.1.1 + fs-minipass: 3.0.3 + glob: 10.4.5 + lru-cache: 10.4.3 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 4.0.0 + ssri: 10.0.6 + tar: 6.2.1 + unique-filename: 3.0.0 + callsites@3.1.0: {} caniuse-lite@1.0.30001684: {} @@ -2753,6 +3616,14 @@ snapshots: ccount@2.0.1: {} + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -2791,12 +3662,16 @@ snapshots: chownr@1.1.4: {} + chownr@2.0.0: {} + ci-info@4.1.0: {} clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 + clean-stack@2.2.0: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -2807,6 +3682,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + code-block-writer@13.0.3: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -2819,16 +3696,22 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + comment-parser@1.4.1: {} concat-map@0.0.1: {} confbox@0.1.8: {} + console-control-strings@1.1.0: {} + core-js-compat@3.39.0: dependencies: browserslist: 4.24.2 + core-js@2.6.12: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2847,6 +3730,10 @@ snapshots: cssesc@3.0.0: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -2855,6 +3742,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.1: + dependencies: + ms: 2.1.3 + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -2867,8 +3758,16 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + + delegates@1.0.0: {} + dequal@2.0.3: {} + detect-indent@4.0.0: + dependencies: + repeating: 2.0.1 + detect-libc@2.0.3: {} devlop@1.1.0: @@ -2923,17 +3822,28 @@ snapshots: doublearray@0.0.2: {} + eastasianwidth@0.2.0: {} + egoroof-blowfish@4.0.1: {} electron-to-chromium@1.5.65: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + + encoding-japanese@2.2.0: {} + encoding-sniffer@0.2.0: dependencies: iconv-lite: 0.6.3 whatwg-encoding: 3.1.1 + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -2949,6 +3859,10 @@ snapshots: entities@6.0.0: {} + env-paths@2.2.0: {} + + err-code@2.0.3: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -3227,7 +4141,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.3.7 + debug: 4.4.1 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -3277,6 +4191,8 @@ snapshots: expand-template@2.0.3: {} + exponential-backoff@3.1.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -3287,10 +4203,20 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fast-redact@3.5.0: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -3326,13 +4252,54 @@ snapshots: flatted@3.3.2: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fs-constants@1.0.0: {} + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + gauge@3.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + gauge@5.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 4.1.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + get-caller-file@2.0.5: {} get-tsconfig@4.8.1: @@ -3349,6 +4316,24 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -3357,20 +4342,38 @@ snapshots: globals@15.12.0: {} + globals@9.18.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + has-flag@3.0.0: {} has-flag@4.0.0: {} + has-unicode@2.0.1: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + hosted-git-info@2.8.9: {} + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -3385,6 +4388,13 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + htmlparser2@9.1.0: dependencies: domelementtype: 2.3.0 @@ -3392,6 +4402,29 @@ snapshots: domutils: 3.1.0 entities: 4.5.0 + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -3400,6 +4433,18 @@ snapshots: ignore@5.3.2: {} + imapflow@1.0.193: + dependencies: + encoding-japanese: 2.2.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + mailsplit: 5.4.6 + nodemailer: 7.0.5 + pino: 9.7.0 + socks: 2.8.6 + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -3409,10 +4454,24 @@ snapshots: indent-string@4.0.0: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} ini@1.3.8: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + is-arrayish@0.2.1: {} is-builtin-module@3.2.1: @@ -3425,26 +4484,44 @@ snapshots: is-extglob@2.1.1: {} + is-finite@1.1.0: {} + is-fullwidth-code-point@3.0.0: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-lambda@1.0.1: {} + is-number@7.0.0: {} isexe@2.0.0: {} + isexe@3.1.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-tokens@3.0.2: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: dependencies: argparse: 2.0.1 + jsbn@1.1.0: {} + jsdoc-type-pratt-parser@4.1.0: {} jsesc@0.5.0: {} + jsesc@1.3.0: {} + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -3474,13 +4551,30 @@ snapshots: doublearray: 0.0.2 zlibjs: 0.3.1 + leac@0.6.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + libbase64@1.3.0: {} + + libmime@5.3.7: + dependencies: + encoding-japanese: 2.2.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libqp: 2.1.1 + + libqp@2.1.1: {} + lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + local-pkg@0.5.1: dependencies: mlly: 1.7.3 @@ -3502,10 +4596,62 @@ snapshots: longest-streak@3.1.0: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + magic-string@0.30.14: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + mailparser@3.7.4: + dependencies: + encoding-japanese: 2.2.0 + he: 1.2.0 + html-to-text: 9.0.5 + iconv-lite: 0.6.3 + libmime: 5.3.7 + linkify-it: 5.0.0 + mailsplit: 5.4.5 + nodemailer: 7.0.4 + punycode.js: 2.3.1 + tlds: 1.259.0 + + mailsplit@5.4.5: + dependencies: + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + + mailsplit@5.4.6: + dependencies: + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-fetch-happen@13.0.1: + dependencies: + '@npmcli/agent': 2.2.2 + cacache: 18.0.4 + http-cache-semantics: 4.2.0 + is-lambda: 1.0.1 + minipass: 7.1.2 + minipass-fetch: 3.0.5 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + proc-log: 4.2.0 + promise-retry: 2.0.1 + ssri: 10.0.6 + transitivePeerDependencies: + - supports-color + markdown-table@3.0.4: {} mdast-util-find-and-replace@3.0.1: @@ -3814,6 +4960,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -3824,8 +4974,47 @@ snapshots: minimist@1.2.8: {} + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.2 + + minipass-fetch@3.0.5: + dependencies: + minipass: 7.1.2 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + mkdirp-classic@0.5.3: {} + mkdirp@1.0.4: {} + mlly@1.7.3: dependencies: acorn: 8.14.0 @@ -3833,8 +5022,12 @@ snapshots: pkg-types: 1.2.1 ufo: 1.5.4 + ms@2.0.0: {} + ms@2.1.3: {} + nan@https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e: {} + nanoid@3.3.8: {} nanoid@5.0.9: {} @@ -3845,14 +5038,62 @@ snapshots: natural-orderby@5.0.0: {} + negotiator@0.6.4: {} + node-abi@3.71.0: dependencies: semver: 7.6.3 node-addon-api@7.1.1: {} + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-gyp@10.2.0: + dependencies: + env-paths: 2.2.0 + exponential-backoff: 3.1.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + make-fetch-happen: 13.0.1 + nopt: 7.2.1 + proc-log: 4.2.0 + semver: 7.6.3 + tar: 6.2.1 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + + node-libcurl-ja3@5.0.3(encoding@0.1.13): + dependencies: + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + env-paths: 2.2.0 + nan: https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e + node-gyp: 10.2.0 + npmlog: 7.0.1 + rimraf: 5.0.5 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + - supports-color + node-releases@2.0.18: {} + nodemailer@7.0.4: {} + + nodemailer@7.0.5: {} + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -3860,10 +5101,28 @@ snapshots: semver: 5.7.2 validate-npm-package-license: 3.0.4 + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + npmlog@7.0.1: + dependencies: + are-we-there-yet: 4.0.2 + console-control-strings: 1.1.0 + gauge: 5.0.2 + set-blocking: 2.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 + object-assign@4.1.1: {} + + on-exit-leak-free@2.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3897,8 +5156,14 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.5: {} parent-module@1.0.1: @@ -3932,20 +5197,64 @@ snapshots: dependencies: entities: 4.5.0 + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + patchright-core@1.52.5: {} + + patchright@1.52.5: + dependencies: + patchright-core: 1.52.5 + optionalDependencies: + fsevents: 2.3.2 + + path-browserify@1.0.1: {} + path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + pathe@1.1.2: {} + peberminta@0.9.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.2: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.7.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pkg-types@1.2.1: dependencies: confbox: 0.1.8 @@ -3988,6 +5297,15 @@ snapshots: prelude-ls@1.2.1: {} + proc-log@4.2.0: {} + + process-warning@5.0.0: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + psl@1.15.0: dependencies: punycode: 2.3.1 @@ -3997,6 +5315,8 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + punycode.js@2.3.1: {} + punycode@2.3.1: {} qrcode-terminal@0.12.0: {} @@ -4005,6 +5325,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -4031,10 +5353,14 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + real-require@0.2.0: {} + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.1 + regenerator-runtime@0.11.1: {} + regexp-ast-analysis@0.7.1: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -4046,6 +5372,10 @@ snapshots: dependencies: jsesc: 0.5.0 + repeating@2.0.1: + dependencies: + is-finite: 1.1.0 + require-directory@2.1.1: {} requires-port@1.0.0: {} @@ -4065,14 +5395,26 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + retry@0.12.0: {} + reusify@1.0.4: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rimraf@5.0.5: + dependencies: + glob: 10.4.5 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} scslre@0.3.0: @@ -4081,10 +5423,18 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + semver@5.7.2: {} + semver@6.3.1: {} + semver@7.6.3: {} + set-blocking@2.0.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4093,6 +5443,8 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + simple-concat@1.0.1: {} simple-get@4.0.1: @@ -4105,8 +5457,29 @@ snapshots: slashes@3.0.12: {} + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + socks: 2.8.6 + transitivePeerDependencies: + - supports-color + + socks@2.8.6: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} + source-map@0.5.7: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -4132,6 +5505,14 @@ snapshots: cli-cursor: 3.1.0 strip-ansi: 5.2.0 + split2@4.2.0: {} + + sprintf-js@1.1.3: {} + + ssri@10.0.6: + dependencies: + minipass: 7.1.2 + stable-hash@0.0.4: {} string-width@4.2.3: @@ -4140,10 +5521,20 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + strip-ansi@5.2.0: dependencies: ansi-regex: 4.1.1 @@ -4152,6 +5543,10 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -4160,6 +5555,8 @@ snapshots: strip-json-comments@3.1.1: {} + supports-color@2.0.0: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -4196,14 +5593,31 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + tinyexec@0.3.1: {} + tlds@1.259.0: {} + tldts-core@6.1.66: {} tldts@6.1.66: dependencies: tldts-core: 6.1.66 + to-fast-properties@1.0.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4227,10 +5641,21 @@ snapshots: dependencies: tldts: 6.1.66 + tr46@0.0.3: {} + + trim-right@1.0.1: {} + ts-api-utils@1.4.2(typescript@5.7.2): dependencies: typescript: 5.7.2 + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + + tslib@2.6.2: {} + tslib@2.8.1: {} tsx@4.19.2: @@ -4256,6 +5681,8 @@ snapshots: typescript@5.7.2: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} undici-types@6.20.0: {} @@ -4264,6 +5691,14 @@ snapshots: undici@7.2.0: {} + unique-filename@3.0.0: + dependencies: + unique-slug: 4.0.0 + + unique-slug@4.0.0: + dependencies: + imurmurhash: 0.1.4 + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -4322,16 +5757,31 @@ snapshots: wanakana@5.3.1: {} + webidl-conversions@3.0.1: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 whatwg-mimetype@4.0.0: {} + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -4340,6 +5790,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} xml-name-validator@4.0.0: {} @@ -4348,6 +5804,8 @@ snapshots: y18n@5.0.8: {} + yallist@4.0.0: {} + yaml-eslint-parser@1.2.3: dependencies: eslint-visitor-keys: 3.4.3 diff --git a/scripts/infra/navidrome/find-untagged-mbz.ts b/scripts/infra/navidrome/find-untagged-mbz.ts new file mode 100644 index 0000000..a54e8be --- /dev/null +++ b/scripts/infra/navidrome/find-untagged-mbz.ts @@ -0,0 +1,22 @@ +import { fetchSongsIter } from '../../../utils/navidrome.ts' + +const IGNORE_PATHS = [ + 's3/Electronic/_Compilations/keygenjukebox/', +] + +let count = 0 +for await (const song of fetchSongsIter()) { + if (IGNORE_PATHS.some(path => song.path.startsWith(path))) { + continue + } + + for (const field of ['mbzRecordingID', 'mbzReleaseTrackId', 'mbzAlbumId', 'mbzReleaseGroupId']) { + if (!song[field]) { + console.log('found missing %s: %s - %s (%s)', field, song.artist, song.title, song.path) + count++ + break + } + } +} + +console.log('found %d tracks without mbz ids', count) diff --git a/scripts/infra/navidrome/find-untagged-multiartists.ts b/scripts/infra/navidrome/find-untagged-multiartists.ts new file mode 100644 index 0000000..a14138e --- /dev/null +++ b/scripts/infra/navidrome/find-untagged-multiartists.ts @@ -0,0 +1,21 @@ +import { fetchSongsIter } from '../../../utils/navidrome.ts' + +const WHITELIST_ARTISTS = new Set([ + 'betwixt & between', + '10th avenue cafe/tak', + 'overmind and potatoes', +]) + +let count = 0 +for await (const song of fetchSongsIter()) { + if ( + (!song.participants?.artist || song.participants.artist.length === 1) + && song.artist.match(/, | and | & |\/| x | feat\. /i) + && !WHITELIST_ARTISTS.has(song.artist.toLowerCase()) + ) { + console.log('possible multiartist: %s - %s (%s)', song.artist, song.title, song.path) + count++ + } +} + +console.log('found %d possible multiartists', count) diff --git a/scripts/media/deezer-dl.ts b/scripts/media/deezer-dl.ts index 9797c3b..96923db 100644 --- a/scripts/media/deezer-dl.ts +++ b/scripts/media/deezer-dl.ts @@ -311,12 +311,93 @@ function getTrackName(track: GwTrack) { return name } +// todo +// async function resolveMusicbrainzIds(albumId: number) { +// const deezerUrl = `https://www.deezer.com/album/${albumId}` +// // try odesli api to fetch extra links +// const odesliRes = await ffetch('https://api.song.link/v1-alpha.1/links', { +// query: { +// url: deezerUrl, +// key: '71d7be8a-3a76-459b-b21e-8f0350374984', +// }, +// }).parsedJson(z.object({ +// linksByPlatform: z.record(z.string(), z.object({ +// url: z.string(), +// })), +// })).catch(() => null) + +// const urls = [deezerUrl] +// if (odesliRes) { +// for (const { url } of Object.values(odesliRes.linksByPlatform)) { +// urls.push(url) +// } +// } + +// // try to resolve musicbrainz album id +// const mbRes1 = await ffetch('https://musicbrainz.org/ws/2/url', { +// query: { +// resource: urls, +// inc: 'release-rels', +// }, +// }).parsedJson(z.object({ +// urls: z.array(z.object({ +// relations: z.array(z.any()), +// })), +// })) + +// const uniqueMbIds = new Set() +// for (const { relations } of mbRes1.urls) { +// for (const rel of relations) { +// if (rel['target-type'] !== 'release') continue + +// uniqueMbIds.add(rel.release.id) +// } +// } + +// if (uniqueMbIds.size === 0) return null +// const releaseMbId = uniqueMbIds.values().next().value + +// // resolve the rest of the ids from the release +// const releaseRes = await ffetch(`https://musicbrainz.org/ws/2/release/${releaseMbId}`, { +// query: { +// inc: 'artists recordings', +// }, +// }).parsedJson(z.object({ +// 'artist-credit': z.array(z.object({ +// artist: z.object({ +// id: z.string(), +// }), +// })).optional(), +// 'media': z.array(z.object({ +// id: z.string(), +// tracks: z.array(z.object({ +// position: z.number(), +// title: z.string(), +// id: z.string(), +// recording: z.object({ +// id: z.string(), +// }), +// })), +// })).optional(), +// })) + +// return { +// release: releaseMbId, +// artists: releaseRes['artist-credit']?.map(it => it.artist.id) ?? [], +// tracks: releaseRes['media']?.[0] +// } +// } + async function downloadTrack(track: GwTrack, opts: { destination: string album?: GwAlbum }) { const albumUrl = `https://cdn-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/1500x1500-000000-80-0-0.jpg` - const [getUrlRes, albumAb, lyricsRes] = await Promise.all([ + const [ + getUrlRes, + albumAb, + lyricsRes, + ] = await Promise.all([ ffetch.post('https://media.deezer.com/v1/get_url', { json: { license_token: userData.USER.OPTIONS.license_token, @@ -343,6 +424,8 @@ async function downloadTrack(track: GwTrack, opts: { }), ]) + // console.dir(getUrlRes, { depth: null }) + const albumCoverPath = join(`assets/deezer-tmp-${track.SNG_ID}.jpg`) await writeFile(albumCoverPath, new Uint8Array(albumAb)) @@ -487,6 +570,10 @@ async function downloadTrack(track: GwTrack, opts: { params.push(`--set-tag=COPYRIGHT=${opts.album.COPYRIGHT}`) } + if (lyricsLrc) { + params.push(`--set-tag=LYRICS=${lyricsLrc}`) + } + params.push(filename) await $`metaflac ${params}` @@ -605,9 +692,10 @@ async function downloadArtist(artistId: string) { spinnies.succeed('collect', { text: `collected ${albums.length} albums with a total of ${trackCount} tracks` }) } - // fixme: singles should always contain artist name and be saved in artist root dir - // fixme: "featured" albums (i.e. when main artist of the album is not the one we're dling) should have album artist name in its dirname + // fixme: "featured" albums/tracks (i.e. when main artist of the album is not the one we're dling) should have album artist name in its dirname + // fixme: singles should be saved in artist root dir // todo: automatic musicbrainz matching + // todo: automatic genius/musixmatch matching for lyrics if unavailable directly from deezer await asyncPool(albums, async (alb) => { const tracks = await gwLightApi({ @@ -784,6 +872,14 @@ if (url.match(/^(artist|album|track):(\d+)$/)) { node: z.object({ id: z.string(), title: z.string(), + contributors: z.object({ + edges: z.array(z.object({ + node: z.object({ + id: z.string(), + name: z.string(), + }), + })), + }), }), })), }), @@ -801,7 +897,7 @@ if (url.match(/^(artist|album|track):(\d+)$/)) { } for (const [i, { node }] of iter.enumerate(searchResult.instantSearch.results.tracks.edges)) { - console.log(`track:${node.id}: ${node.title}`) + console.log(`track:${node.id}: ${node.contributors.edges.map(it => it.node.name).join(', ')} - ${node.title}`) } const uri = await question('option > ') diff --git a/utils/captcha.ts b/utils/captcha.ts deleted file mode 100644 index 11d4f60..0000000 --- a/utils/captcha.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { sleep } from '@fuman/utils' -import { z } from 'zod' -import { ffetch } from './fetch.ts' -import { getEnv } from './misc.ts' - -const CreateTaskResponse = z.object({ - errorId: z.number(), - errorCode: z.string().optional().nullable(), - taskId: z.number(), -}) - -const GetTaskResultResponse = z.object({ - errorId: z.number(), - errorCode: z.string().optional().nullable(), - status: z.enum(['ready', 'processing']), - solution: z.unknown().optional(), -}) - -export async function solveCaptcha(task: unknown) { - const res = await ffetch.post('https://api.capmonster.cloud/createTask', { - json: { - clientKey: getEnv('CAPMONSTER_API_TOKEN'), - task, - }, - }).parsedJson(CreateTaskResponse) - - if (res.errorId) { - throw new Error(`createTask error ${res.errorId}: ${res.errorCode}`) - } - - const taskId = res.taskId - - await sleep(5_000) - - let requestCount = 0 - - while (true) { - requestCount += 1 - if (requestCount > 100) { - // "Limit: 120 requests per task. If the limit is exceeded, the user's account may be temporarily locked." - // just to be safe - throw new Error('captcha request count exceeded') - } - - const res = await ffetch.post('https://api.capmonster.cloud/getTaskResult', { - json: { - clientKey: getEnv('CAPMONSTER_API_TOKEN'), - taskId, - }, - }).parsedJson(GetTaskResultResponse) - - if (res.errorId) { - throw new Error(`getTaskResult error ${res.errorId}: ${res.errorCode}`) - } - - if (res.status === 'ready') { - return res.solution - } - - await sleep(2_000) - } -} - -export async function solveRecaptcha(params?: { - url: string - siteKey: string - s?: string - userAgent?: string - cookies?: string - isInvisible?: boolean -}) { - const res = await solveCaptcha({ - type: 'RecaptchaV2TaskProxyless', - websiteURL: params?.url, - websiteKey: params?.siteKey, - recaptchaDataSValue: params?.s, - userAgent: params?.userAgent, - cookies: params?.cookies, - isInvisible: params?.isInvisible, - }) - - if (typeof res !== 'object' || !res || !('gRecaptchaResponse' in res) || typeof res.gRecaptchaResponse !== 'string') { - throw new Error('invalid recaptcha response') - } - - return res.gRecaptchaResponse -} diff --git a/utils/navidrome.ts b/utils/navidrome.ts index d99477d..cd896a9 100644 --- a/utils/navidrome.ts +++ b/utils/navidrome.ts @@ -33,6 +33,16 @@ export const NavidromeSong = z.object({ libraryPath: z.string(), duration: z.number(), size: z.number(), + participants: z.object({ + artist: z.object({ + id: z.string(), + name: z.string(), + }).array().optional(), + }).optional(), + mbzRecordingID: z.string().optional(), + mbzReleaseTrackId: z.string().optional(), + mbzAlbumId: z.string().optional(), + mbzReleaseGroupId: z.string().optional(), }) export type NavidromeSong = z.infer @@ -43,7 +53,7 @@ export async function fetchSongs(offset: number, pageSize: number) { _start: offset, _end: offset + pageSize, _order: 'ASC', - _sort: 'title', + _sort: 'path', }, }).parsedJson(z.array(NavidromeSong)) } From 728699b3ecd1eb8e57673ee5aba4b2492b659709 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Thu, 14 Aug 2025 09:21:11 +0000 Subject: [PATCH 52/61] chore: update public repo --- package.json | 10 + pnpm-lock.yaml | 1464 ++++++++++++++++- scripts/infra/navidrome/find-untagged-mbz.ts | 22 + .../navidrome/find-untagged-multiartists.ts | 21 + scripts/media/deezer-dl.ts | 104 +- scripts/misc/twitch-autoreg.ts | 365 ++++ utils/captcha.ts | 87 - utils/navidrome.ts | 12 +- 8 files changed, 1990 insertions(+), 95 deletions(-) create mode 100644 scripts/infra/navidrome/find-untagged-mbz.ts create mode 100644 scripts/infra/navidrome/find-untagged-multiartists.ts create mode 100644 scripts/misc/twitch-autoreg.ts delete mode 100644 utils/captcha.ts diff --git a/package.json b/package.json index 3269071..43e33d7 100644 --- a/package.json +++ b/package.json @@ -11,20 +11,28 @@ "@types/better-sqlite3": "^7.6.12", "@types/plist": "^3.0.5", "@types/spinnies": "^0.5.3", + "babel-generator": "^6.26.1", + "babel-traverse": "^6.26.0", + "babylon": "^6.18.0", "better-sqlite3": "^11.8.1", "canvas": "^3.1.0", "cheerio": "^1.0.0", "egoroof-blowfish": "4.0.1", "es-main": "^1.3.0", "filesize": "^10.1.6", + "imapflow": "^1.0.193", "json5": "^2.2.3", "kuromoji": "^0.1.2", + "mailparser": "^3.7.4", "nanoid": "^5.0.9", + "node-libcurl-ja3": "^5.0.3", + "patchright": "^1.52.5", "plist": "^3.1.0", "qrcode-terminal": "^0.12.0", "spinnies": "^0.5.1", "tough-cookie": "^5.0.0", "tough-cookie-file-store": "^2.0.3", + "ts-morph": "^26.0.0", "tsx": "^4.19.2", "undici": "^7.2.0", "wanakana": "^5.3.1" @@ -33,6 +41,7 @@ "@antfu/eslint-config": "3.10.0", "@fuman/fetch": "0.1.0", "@fuman/utils": "0.0.14", + "@types/mailparser": "^3.4.6", "@types/node": "22.10.0", "domhandler": "^5.0.3", "dotenv": "16.4.5", @@ -43,6 +52,7 @@ "pnpm": { "onlyBuiltDependencies": [ "better-sqlite3", + "node-libcurl-ja3", "canvas" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 871673a..e95a7c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,15 @@ importers: '@types/spinnies': specifier: ^0.5.3 version: 0.5.3 + babel-generator: + specifier: ^6.26.1 + version: 6.26.1 + babel-traverse: + specifier: ^6.26.0 + version: 6.26.0 + babylon: + specifier: ^6.18.0 + version: 6.18.0 better-sqlite3: specifier: ^11.8.1 version: 11.8.1 @@ -50,15 +59,27 @@ importers: filesize: specifier: ^10.1.6 version: 10.1.6 + imapflow: + specifier: ^1.0.193 + version: 1.0.193 json5: specifier: ^2.2.3 version: 2.2.3 kuromoji: specifier: ^0.1.2 version: 0.1.2 + mailparser: + specifier: ^3.7.4 + version: 3.7.4 nanoid: specifier: ^5.0.9 version: 5.0.9 + node-libcurl-ja3: + specifier: ^5.0.3 + version: 5.0.3(encoding@0.1.13) + patchright: + specifier: ^1.52.5 + version: 1.52.5 plist: specifier: ^3.1.0 version: 3.1.0 @@ -74,6 +95,9 @@ importers: tough-cookie-file-store: specifier: ^2.0.3 version: 2.0.3 + ts-morph: + specifier: ^26.0.0 + version: 26.0.0 tsx: specifier: ^4.19.2 version: 4.19.2 @@ -93,6 +117,9 @@ importers: '@fuman/utils': specifier: 0.0.14 version: 0.0.14 + '@types/mailparser': + specifier: ^3.4.6 + version: 3.4.6 '@types/node': specifier: 22.10.0 version: 22.10.0 @@ -467,9 +494,25 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + '@mtcute/core@0.19.1': resolution: {integrity: sha512-Xfq1T03kNRcyomkQl1d426piCnbgKZiYV2z/rM4BBrTIMDwGdxUpp66FPTj7N+MHm1/bVoJNFTUO67nBrLmqdA==} @@ -506,16 +549,34 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@npmcli/agent@2.2.2': + resolution: {integrity: sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@npmcli/fs@3.1.1': + resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@stylistic/eslint-plugin@2.11.0': resolution: {integrity: sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=8.40.0' + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + '@types/better-sqlite3@7.6.12': resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} @@ -537,6 +598,9 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/mailparser@3.4.6': + resolution: {integrity: sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -652,6 +716,13 @@ packages: resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} engines: {node: '>=10.0.0'} + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -662,9 +733,25 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -673,6 +760,14 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -681,16 +776,59 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + are-we-there-yet@4.0.2: + resolution: {integrity: sha512-ncSWAawFhKMJDTdoAeOV+jyW1VCMj5QIAwULIBV0SSR7B/RLPPEQiknKcg/RIIZlUQrxELpsxMiTUoAQ4sIUyg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + babel-code-frame@6.26.0: + resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==} + + babel-generator@6.26.1: + resolution: {integrity: sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==} + + babel-messages@6.23.0: + resolution: {integrity: sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==} + + babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + + babel-traverse@6.26.0: + resolution: {integrity: sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==} + + babel-types@6.26.0: + resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==} + + babylon@6.18.0: + resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} + hasBin: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -734,6 +872,10 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + cacache@18.0.4: + resolution: {integrity: sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==} + engines: {node: ^16.14.0 || >=18.0.0} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -748,6 +890,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -769,6 +915,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + ci-info@4.1.0: resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} engines: {node: '>=8'} @@ -777,6 +927,10 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -785,6 +939,9 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -798,6 +955,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + comment-parser@1.4.1: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} @@ -808,9 +969,16 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + core-js-compat@3.39.0: resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -827,6 +995,14 @@ packages: engines: {node: '>=4'} hasBin: true + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -844,6 +1020,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -858,10 +1043,21 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-indent@4.0.0: + resolution: {integrity: sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==} + engines: {node: '>=0.10.0'} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -906,6 +1102,9 @@ packages: doublearray@0.0.2: resolution: {integrity: sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + egoroof-blowfish@4.0.1: resolution: {integrity: sha512-e2gdfyLZrDvyuHX2NkRPFxEuYCIddp+W3MucqjBw9h9nineZUWynuuqQh+R5sNT9IVopPFBHwcnBYHqTcB1Vdw==} @@ -915,9 +1114,19 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding-japanese@2.2.0: + resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==} + engines: {node: '>=8.10.0'} + encoding-sniffer@0.2.0: resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -936,6 +1145,13 @@ packages: resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} engines: {node: '>=0.12'} + env-paths@2.2.0: + resolution: {integrity: sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -1157,6 +1373,9 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + exponential-backoff@3.1.2: + resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1164,12 +1383,20 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -1207,9 +1434,29 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1218,6 +1465,16 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + gauge@5.0.2: + resolution: {integrity: sha512-pMaFftXPtiGIHCJHdcUUx9Rby/rFT/Kkt3fIIGCs+9PMDIljSyRiqraTlxNtBReJRDfUefpa263RQ3vnp5G/LQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1236,6 +1493,14 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -1248,12 +1513,20 @@ packages: resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} engines: {node: '>=18'} + globals@9.18.0: + resolution: {integrity: sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==} + engines: {node: '>=0.10.0'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1262,22 +1535,51 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1289,6 +1591,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + imapflow@1.0.193: + resolution: {integrity: sha512-q5saDQTYM1RrUMm/LOWQ42XnBtKqpJc4gwXOz8ki6k7xA6DUQ6pP+Cbj7t7p+L4vRvcBbNF6ukpuxiIuEGYtKQ==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -1301,12 +1606,23 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1322,6 +1638,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finite@1.1.0: + resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -1330,6 +1650,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1337,6 +1660,16 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-tokens@3.0.2: + resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1344,6 +1677,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsdoc-type-pratt-parser@4.1.0: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} @@ -1352,6 +1688,10 @@ packages: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true + jsesc@1.3.0: + resolution: {integrity: sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==} + hasBin: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -1384,13 +1724,28 @@ packages: kuromoji@0.1.2: resolution: {integrity: sha512-V0dUf+C2LpcPEXhoHLMAop/bOht16Dyr+mDiIE39yX3vqau7p80De/koFqpiTcL1zzdZlc3xuHZ8u5gjYRfFaQ==} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libbase64@1.3.0: + resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} + + libmime@5.3.7: + resolution: {integrity: sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==} + + libqp@2.1.1: + resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + local-pkg@0.5.1: resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} engines: {node: '>=14'} @@ -1415,9 +1770,33 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string@0.30.14: resolution: {integrity: sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==} + mailparser@3.7.4: + resolution: {integrity: sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==} + + mailsplit@5.4.5: + resolution: {integrity: sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==} + + mailsplit@5.4.6: + resolution: {integrity: sha512-M+cqmzaPG/mEiCDmqQUz8L177JZLZmXAUpq38owtpq2xlXlTSw+kntnxRt2xsxVFFV6+T8Mj/U0l5s7s6e0rNw==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-fetch-happen@13.0.1: + resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==} + engines: {node: ^16.14.0 || >=18.0.0} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -1558,6 +1937,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1568,15 +1951,63 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@3.0.5: + resolution: {integrity: sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.7.3: resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nan@https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e: + resolution: {tarball: https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e} + version: 2.22.0 + nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1597,6 +2028,10 @@ packages: resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} engines: {node: '>=18'} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + node-abi@3.71.0: resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} engines: {node: '>=10'} @@ -1604,15 +2039,69 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp@10.2.0: + resolution: {integrity: sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==} + engines: {node: ^16.14.0 || >=18.0.0} + hasBin: true + + node-libcurl-ja3@5.0.3: + resolution: {integrity: sha512-GTSkrcktAPbcSm/3GiAd8J2zbERSxsDD/8Lf4/tobMoIAQw1aLmtOqadDBE/GpFNRCyhMNSVg0xTDDvuB8Blhg==} + engines: {node: '>=20'} + os: [darwin, linux] + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + nodemailer@7.0.4: + resolution: {integrity: sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==} + engines: {node: '>=6.0.0'} + + nodemailer@7.0.5: + resolution: {integrity: sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==} + engines: {node: '>=6.0.0'} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + npmlog@7.0.1: + resolution: {integrity: sha512-uJ0YFk/mCQpLBt+bxN88AKd+gyqZvZDbtiNxk6Waqcj2aPRyfVx8ITawkyQynxUagInjdYT1+qj4NfA5KJJUxg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1640,10 +2129,17 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.5: resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==} @@ -1672,10 +2168,30 @@ packages: parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + patchright-core@1.52.5: + resolution: {integrity: sha512-8rnLVEK9jDZWzFPy2hCQrp4xhU7zgE8IqseZyjGkgxf+jpAWTuGNgIAlcsKZMfQrDL8j1mZgRIGNAQT00nk6QA==} + engines: {node: '>=18'} + hasBin: true + + patchright@1.52.5: + resolution: {integrity: sha512-wmRpsF9n02j0S+YDk0U3ouuWipHbUowwxbf/4K4G9ng311vvugoo8WndbU/fCsGtme8gYNfcEGcpfF6/L1NXHg==} + engines: {node: '>=18'} + hasBin: true + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1683,9 +2199,16 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1697,6 +2220,16 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + pkg-types@1.2.1: resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} @@ -1725,12 +2258,27 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1745,6 +2293,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -1761,10 +2312,17 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -1777,6 +2335,10 @@ packages: resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} hasBin: true + repeating@2.0.1: + resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==} + engines: {node: '>=0.10.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1799,16 +2361,34 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1816,15 +2396,25 @@ packages: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1836,6 +2426,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -1848,10 +2442,29 @@ packages: slashes@3.0.12: resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.6: + resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -1870,6 +2483,17 @@ packages: spinnies@0.5.1: resolution: {integrity: sha512-WpjSXv9NQz0nU3yCT9TFEOfpFrXADY9C5fG6eAJqixLhvTX1jP3w92Y8IE5oafIe42nlF9otjhllnXN/QCaB3A==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + ssri@10.0.6: + resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} @@ -1877,9 +2501,17 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + strip-ansi@5.2.0: resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} engines: {node: '>=6'} @@ -1888,6 +2520,10 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -1900,6 +2536,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -1931,9 +2571,20 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tlds@1.259.0: + resolution: {integrity: sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==} + hasBin: true + tldts-core@6.1.66: resolution: {integrity: sha512-s07jJruSwndD2X8bVjwioPfqpIc1pDTzszPe9pL1Skbh4bjytL85KNQ3tolqLbCvpQHawIsGfFi9dgerWjqW4g==} @@ -1941,6 +2592,10 @@ packages: resolution: {integrity: sha512-l3ciXsYFel/jSRfESbyKYud1nOw7WfhrBEF9I3UiarYk/qEaOOwu3qXNECHw4fHGHGTEOuhf/VdKgoDX5M/dhQ==} hasBin: true + to-fast-properties@1.0.3: + resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==} + engines: {node: '>=0.10.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1961,12 +2616,25 @@ packages: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + trim-right@1.0.1: + resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} + engines: {node: '>=0.10.0'} + ts-api-utils@1.4.2: resolution: {integrity: sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1999,6 +2667,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -2013,6 +2684,14 @@ packages: resolution: {integrity: sha512-klt+0S55GBViA9nsq48/NSCo4YX5mjydjypxD7UmHh/brMu8h/Mhd/F7qAeoH2NOO8SDTk6kjnTFc4WpzmfYpQ==} engines: {node: '>=20.18.1'} + unique-filename@3.0.0: + resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + unique-slug@4.0.0: + resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -2057,6 +2736,9 @@ packages: resolution: {integrity: sha512-OSDqupzTlzl2LGyqTdhcXcl6ezMiFhcUwLBP8YKaBIbMYW1wAwDvupw2T9G9oVaKT9RmaSpyTXjxddFPUcFFIw==} engines: {node: '>=12'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2065,11 +2747,22 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2078,6 +2771,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2093,6 +2790,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml-eslint-parser@1.2.3: resolution: {integrity: sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==} engines: {node: ^14.17.0 || >=16.0.0} @@ -2320,7 +3020,7 @@ snapshots: '@eslint/config-array@0.19.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.7 + debug: 4.4.1 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -2330,7 +3030,7 @@ snapshots: '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.4.1 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -2409,8 +3109,38 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/sourcemap-codec@1.5.0': {} + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.3 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + '@mtcute/core@0.19.1': dependencies: '@fuman/io': 0.0.4 @@ -2473,8 +3203,30 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@npmcli/agent@2.2.2': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 10.4.3 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@3.1.1': + dependencies: + semver: 7.6.3 + + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.1.1': {} + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@stylistic/eslint-plugin@2.11.0(eslint@9.15.0)(typescript@5.7.2)': dependencies: '@typescript-eslint/utils': 8.16.0(eslint@9.15.0)(typescript@5.7.2) @@ -2487,6 +3239,12 @@ snapshots: - supports-color - typescript + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.0.3 + path-browserify: 1.0.1 + '@types/better-sqlite3@7.6.12': dependencies: '@types/node': 22.10.0 @@ -2512,6 +3270,11 @@ snapshots: '@types/node': 22.10.0 optional: true + '@types/mailparser@3.4.6': + dependencies: + '@types/node': 22.10.0 + iconv-lite: 0.6.3 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -2656,12 +3419,29 @@ snapshots: '@xmldom/xmldom@0.8.10': {} + abbrev@1.1.1: {} + + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 acorn@8.14.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -2669,10 +3449,16 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@2.1.1: {} + ansi-regex@4.1.1: {} ansi-regex@5.0.1: {} + ansi-regex@6.1.0: {} + + ansi-styles@2.2.1: {} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 @@ -2681,14 +3467,76 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.1: {} + + aproba@2.1.0: {} + are-docs-informative@0.0.2: {} + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + are-we-there-yet@4.0.2: {} + argparse@2.0.1: {} async@2.6.4: dependencies: lodash: 4.17.21 + atomic-sleep@1.0.0: {} + + babel-code-frame@6.26.0: + dependencies: + chalk: 1.1.3 + esutils: 2.0.3 + js-tokens: 3.0.2 + + babel-generator@6.26.1: + dependencies: + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + detect-indent: 4.0.0 + jsesc: 1.3.0 + lodash: 4.17.21 + source-map: 0.5.7 + trim-right: 1.0.1 + + babel-messages@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-runtime@6.26.0: + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + + babel-traverse@6.26.0: + dependencies: + babel-code-frame: 6.26.0 + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + debug: 2.6.9 + globals: 9.18.0 + invariant: 2.2.4 + lodash: 4.17.21 + transitivePeerDependencies: + - supports-color + + babel-types@6.26.0: + dependencies: + babel-runtime: 6.26.0 + esutils: 2.0.3 + lodash: 4.17.21 + to-fast-properties: 1.0.3 + + babylon@6.18.0: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -2742,6 +3590,21 @@ snapshots: builtin-modules@3.3.0: {} + cacache@18.0.4: + dependencies: + '@npmcli/fs': 3.1.1 + fs-minipass: 3.0.3 + glob: 10.4.5 + lru-cache: 10.4.3 + minipass: 7.1.2 + minipass-collect: 2.0.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 4.0.0 + ssri: 10.0.6 + tar: 6.2.1 + unique-filename: 3.0.0 + callsites@3.1.0: {} caniuse-lite@1.0.30001684: {} @@ -2753,6 +3616,14 @@ snapshots: ccount@2.0.1: {} + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -2791,12 +3662,16 @@ snapshots: chownr@1.1.4: {} + chownr@2.0.0: {} + ci-info@4.1.0: {} clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 + clean-stack@2.2.0: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -2807,6 +3682,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + code-block-writer@13.0.3: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -2819,16 +3696,22 @@ snapshots: color-name@1.1.4: {} + color-support@1.1.3: {} + comment-parser@1.4.1: {} concat-map@0.0.1: {} confbox@0.1.8: {} + console-control-strings@1.1.0: {} + core-js-compat@3.39.0: dependencies: browserslist: 4.24.2 + core-js@2.6.12: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2847,6 +3730,10 @@ snapshots: cssesc@3.0.0: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -2855,6 +3742,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.1: + dependencies: + ms: 2.1.3 + decode-named-character-reference@1.0.2: dependencies: character-entities: 2.0.2 @@ -2867,8 +3758,16 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + + delegates@1.0.0: {} + dequal@2.0.3: {} + detect-indent@4.0.0: + dependencies: + repeating: 2.0.1 + detect-libc@2.0.3: {} devlop@1.1.0: @@ -2923,17 +3822,28 @@ snapshots: doublearray@0.0.2: {} + eastasianwidth@0.2.0: {} + egoroof-blowfish@4.0.1: {} electron-to-chromium@1.5.65: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + + encoding-japanese@2.2.0: {} + encoding-sniffer@0.2.0: dependencies: iconv-lite: 0.6.3 whatwg-encoding: 3.1.1 + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -2949,6 +3859,10 @@ snapshots: entities@6.0.0: {} + env-paths@2.2.0: {} + + err-code@2.0.3: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -3227,7 +4141,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.3.7 + debug: 4.4.1 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -3277,6 +4191,8 @@ snapshots: expand-template@2.0.3: {} + exponential-backoff@3.1.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -3287,10 +4203,20 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fast-redact@3.5.0: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -3326,13 +4252,54 @@ snapshots: flatted@3.3.2: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fs-constants@1.0.0: {} + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + gauge@3.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + gauge@5.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 4.1.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + get-caller-file@2.0.5: {} get-tsconfig@4.8.1: @@ -3349,6 +4316,24 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -3357,20 +4342,38 @@ snapshots: globals@15.12.0: {} + globals@9.18.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + has-flag@3.0.0: {} has-flag@4.0.0: {} + has-unicode@2.0.1: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + hosted-git-info@2.8.9: {} + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -3385,6 +4388,13 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + htmlparser2@9.1.0: dependencies: domelementtype: 2.3.0 @@ -3392,6 +4402,29 @@ snapshots: domutils: 3.1.0 entities: 4.5.0 + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -3400,6 +4433,18 @@ snapshots: ignore@5.3.2: {} + imapflow@1.0.193: + dependencies: + encoding-japanese: 2.2.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + mailsplit: 5.4.6 + nodemailer: 7.0.5 + pino: 9.7.0 + socks: 2.8.6 + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -3409,10 +4454,24 @@ snapshots: indent-string@4.0.0: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} ini@1.3.8: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + is-arrayish@0.2.1: {} is-builtin-module@3.2.1: @@ -3425,26 +4484,44 @@ snapshots: is-extglob@2.1.1: {} + is-finite@1.1.0: {} + is-fullwidth-code-point@3.0.0: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-lambda@1.0.1: {} + is-number@7.0.0: {} isexe@2.0.0: {} + isexe@3.1.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-tokens@3.0.2: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: dependencies: argparse: 2.0.1 + jsbn@1.1.0: {} + jsdoc-type-pratt-parser@4.1.0: {} jsesc@0.5.0: {} + jsesc@1.3.0: {} + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -3474,13 +4551,30 @@ snapshots: doublearray: 0.0.2 zlibjs: 0.3.1 + leac@0.6.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + libbase64@1.3.0: {} + + libmime@5.3.7: + dependencies: + encoding-japanese: 2.2.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libqp: 2.1.1 + + libqp@2.1.1: {} + lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + local-pkg@0.5.1: dependencies: mlly: 1.7.3 @@ -3502,10 +4596,62 @@ snapshots: longest-streak@3.1.0: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + magic-string@0.30.14: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + mailparser@3.7.4: + dependencies: + encoding-japanese: 2.2.0 + he: 1.2.0 + html-to-text: 9.0.5 + iconv-lite: 0.6.3 + libmime: 5.3.7 + linkify-it: 5.0.0 + mailsplit: 5.4.5 + nodemailer: 7.0.4 + punycode.js: 2.3.1 + tlds: 1.259.0 + + mailsplit@5.4.5: + dependencies: + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + + mailsplit@5.4.6: + dependencies: + libbase64: 1.3.0 + libmime: 5.3.7 + libqp: 2.1.1 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-fetch-happen@13.0.1: + dependencies: + '@npmcli/agent': 2.2.2 + cacache: 18.0.4 + http-cache-semantics: 4.2.0 + is-lambda: 1.0.1 + minipass: 7.1.2 + minipass-fetch: 3.0.5 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + proc-log: 4.2.0 + promise-retry: 2.0.1 + ssri: 10.0.6 + transitivePeerDependencies: + - supports-color + markdown-table@3.0.4: {} mdast-util-find-and-replace@3.0.1: @@ -3814,6 +4960,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -3824,8 +4974,47 @@ snapshots: minimist@1.2.8: {} + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.2 + + minipass-fetch@3.0.5: + dependencies: + minipass: 7.1.2 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + mkdirp-classic@0.5.3: {} + mkdirp@1.0.4: {} + mlly@1.7.3: dependencies: acorn: 8.14.0 @@ -3833,8 +5022,12 @@ snapshots: pkg-types: 1.2.1 ufo: 1.5.4 + ms@2.0.0: {} + ms@2.1.3: {} + nan@https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e: {} + nanoid@3.3.8: {} nanoid@5.0.9: {} @@ -3845,14 +5038,62 @@ snapshots: natural-orderby@5.0.0: {} + negotiator@0.6.4: {} + node-abi@3.71.0: dependencies: semver: 7.6.3 node-addon-api@7.1.1: {} + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-gyp@10.2.0: + dependencies: + env-paths: 2.2.0 + exponential-backoff: 3.1.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + make-fetch-happen: 13.0.1 + nopt: 7.2.1 + proc-log: 4.2.0 + semver: 7.6.3 + tar: 6.2.1 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + + node-libcurl-ja3@5.0.3(encoding@0.1.13): + dependencies: + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + env-paths: 2.2.0 + nan: https://codeload.github.com/JCMais/nan/tar.gz/0ec2eca8b2fd7518affb3945d087e393ad839b7e + node-gyp: 10.2.0 + npmlog: 7.0.1 + rimraf: 5.0.5 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + - supports-color + node-releases@2.0.18: {} + nodemailer@7.0.4: {} + + nodemailer@7.0.5: {} + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -3860,10 +5101,28 @@ snapshots: semver: 5.7.2 validate-npm-package-license: 3.0.4 + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + npmlog@7.0.1: + dependencies: + are-we-there-yet: 4.0.2 + console-control-strings: 1.1.0 + gauge: 5.0.2 + set-blocking: 2.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 + object-assign@4.1.1: {} + + on-exit-leak-free@2.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3897,8 +5156,14 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.5: {} parent-module@1.0.1: @@ -3932,20 +5197,64 @@ snapshots: dependencies: entities: 4.5.0 + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + patchright-core@1.52.5: {} + + patchright@1.52.5: + dependencies: + patchright-core: 1.52.5 + optionalDependencies: + fsevents: 2.3.2 + + path-browserify@1.0.1: {} + path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + pathe@1.1.2: {} + peberminta@0.9.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.2: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.7.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + pkg-types@1.2.1: dependencies: confbox: 0.1.8 @@ -3988,6 +5297,15 @@ snapshots: prelude-ls@1.2.1: {} + proc-log@4.2.0: {} + + process-warning@5.0.0: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + psl@1.15.0: dependencies: punycode: 2.3.1 @@ -3997,6 +5315,8 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 + punycode.js@2.3.1: {} + punycode@2.3.1: {} qrcode-terminal@0.12.0: {} @@ -4005,6 +5325,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -4031,10 +5353,14 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + real-require@0.2.0: {} + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.1 + regenerator-runtime@0.11.1: {} + regexp-ast-analysis@0.7.1: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -4046,6 +5372,10 @@ snapshots: dependencies: jsesc: 0.5.0 + repeating@2.0.1: + dependencies: + is-finite: 1.1.0 + require-directory@2.1.1: {} requires-port@1.0.0: {} @@ -4065,14 +5395,26 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + retry@0.12.0: {} + reusify@1.0.4: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rimraf@5.0.5: + dependencies: + glob: 10.4.5 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} scslre@0.3.0: @@ -4081,10 +5423,18 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + semver@5.7.2: {} + semver@6.3.1: {} + semver@7.6.3: {} + set-blocking@2.0.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4093,6 +5443,8 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + simple-concat@1.0.1: {} simple-get@4.0.1: @@ -4105,8 +5457,29 @@ snapshots: slashes@3.0.12: {} + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + socks: 2.8.6 + transitivePeerDependencies: + - supports-color + + socks@2.8.6: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} + source-map@0.5.7: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -4132,6 +5505,14 @@ snapshots: cli-cursor: 3.1.0 strip-ansi: 5.2.0 + split2@4.2.0: {} + + sprintf-js@1.1.3: {} + + ssri@10.0.6: + dependencies: + minipass: 7.1.2 + stable-hash@0.0.4: {} string-width@4.2.3: @@ -4140,10 +5521,20 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + strip-ansi@5.2.0: dependencies: ansi-regex: 4.1.1 @@ -4152,6 +5543,10 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -4160,6 +5555,8 @@ snapshots: strip-json-comments@3.1.1: {} + supports-color@2.0.0: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -4196,14 +5593,31 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + tinyexec@0.3.1: {} + tlds@1.259.0: {} + tldts-core@6.1.66: {} tldts@6.1.66: dependencies: tldts-core: 6.1.66 + to-fast-properties@1.0.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4227,10 +5641,21 @@ snapshots: dependencies: tldts: 6.1.66 + tr46@0.0.3: {} + + trim-right@1.0.1: {} + ts-api-utils@1.4.2(typescript@5.7.2): dependencies: typescript: 5.7.2 + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + + tslib@2.6.2: {} + tslib@2.8.1: {} tsx@4.19.2: @@ -4256,6 +5681,8 @@ snapshots: typescript@5.7.2: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} undici-types@6.20.0: {} @@ -4264,6 +5691,14 @@ snapshots: undici@7.2.0: {} + unique-filename@3.0.0: + dependencies: + unique-slug: 4.0.0 + + unique-slug@4.0.0: + dependencies: + imurmurhash: 0.1.4 + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -4322,16 +5757,31 @@ snapshots: wanakana@5.3.1: {} + webidl-conversions@3.0.1: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 whatwg-mimetype@4.0.0: {} + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -4340,6 +5790,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} xml-name-validator@4.0.0: {} @@ -4348,6 +5804,8 @@ snapshots: y18n@5.0.8: {} + yallist@4.0.0: {} + yaml-eslint-parser@1.2.3: dependencies: eslint-visitor-keys: 3.4.3 diff --git a/scripts/infra/navidrome/find-untagged-mbz.ts b/scripts/infra/navidrome/find-untagged-mbz.ts new file mode 100644 index 0000000..a54e8be --- /dev/null +++ b/scripts/infra/navidrome/find-untagged-mbz.ts @@ -0,0 +1,22 @@ +import { fetchSongsIter } from '../../../utils/navidrome.ts' + +const IGNORE_PATHS = [ + 's3/Electronic/_Compilations/keygenjukebox/', +] + +let count = 0 +for await (const song of fetchSongsIter()) { + if (IGNORE_PATHS.some(path => song.path.startsWith(path))) { + continue + } + + for (const field of ['mbzRecordingID', 'mbzReleaseTrackId', 'mbzAlbumId', 'mbzReleaseGroupId']) { + if (!song[field]) { + console.log('found missing %s: %s - %s (%s)', field, song.artist, song.title, song.path) + count++ + break + } + } +} + +console.log('found %d tracks without mbz ids', count) diff --git a/scripts/infra/navidrome/find-untagged-multiartists.ts b/scripts/infra/navidrome/find-untagged-multiartists.ts new file mode 100644 index 0000000..a14138e --- /dev/null +++ b/scripts/infra/navidrome/find-untagged-multiartists.ts @@ -0,0 +1,21 @@ +import { fetchSongsIter } from '../../../utils/navidrome.ts' + +const WHITELIST_ARTISTS = new Set([ + 'betwixt & between', + '10th avenue cafe/tak', + 'overmind and potatoes', +]) + +let count = 0 +for await (const song of fetchSongsIter()) { + if ( + (!song.participants?.artist || song.participants.artist.length === 1) + && song.artist.match(/, | and | & |\/| x | feat\. /i) + && !WHITELIST_ARTISTS.has(song.artist.toLowerCase()) + ) { + console.log('possible multiartist: %s - %s (%s)', song.artist, song.title, song.path) + count++ + } +} + +console.log('found %d possible multiartists', count) diff --git a/scripts/media/deezer-dl.ts b/scripts/media/deezer-dl.ts index 9797c3b..96923db 100644 --- a/scripts/media/deezer-dl.ts +++ b/scripts/media/deezer-dl.ts @@ -311,12 +311,93 @@ function getTrackName(track: GwTrack) { return name } +// todo +// async function resolveMusicbrainzIds(albumId: number) { +// const deezerUrl = `https://www.deezer.com/album/${albumId}` +// // try odesli api to fetch extra links +// const odesliRes = await ffetch('https://api.song.link/v1-alpha.1/links', { +// query: { +// url: deezerUrl, +// key: '71d7be8a-3a76-459b-b21e-8f0350374984', +// }, +// }).parsedJson(z.object({ +// linksByPlatform: z.record(z.string(), z.object({ +// url: z.string(), +// })), +// })).catch(() => null) + +// const urls = [deezerUrl] +// if (odesliRes) { +// for (const { url } of Object.values(odesliRes.linksByPlatform)) { +// urls.push(url) +// } +// } + +// // try to resolve musicbrainz album id +// const mbRes1 = await ffetch('https://musicbrainz.org/ws/2/url', { +// query: { +// resource: urls, +// inc: 'release-rels', +// }, +// }).parsedJson(z.object({ +// urls: z.array(z.object({ +// relations: z.array(z.any()), +// })), +// })) + +// const uniqueMbIds = new Set() +// for (const { relations } of mbRes1.urls) { +// for (const rel of relations) { +// if (rel['target-type'] !== 'release') continue + +// uniqueMbIds.add(rel.release.id) +// } +// } + +// if (uniqueMbIds.size === 0) return null +// const releaseMbId = uniqueMbIds.values().next().value + +// // resolve the rest of the ids from the release +// const releaseRes = await ffetch(`https://musicbrainz.org/ws/2/release/${releaseMbId}`, { +// query: { +// inc: 'artists recordings', +// }, +// }).parsedJson(z.object({ +// 'artist-credit': z.array(z.object({ +// artist: z.object({ +// id: z.string(), +// }), +// })).optional(), +// 'media': z.array(z.object({ +// id: z.string(), +// tracks: z.array(z.object({ +// position: z.number(), +// title: z.string(), +// id: z.string(), +// recording: z.object({ +// id: z.string(), +// }), +// })), +// })).optional(), +// })) + +// return { +// release: releaseMbId, +// artists: releaseRes['artist-credit']?.map(it => it.artist.id) ?? [], +// tracks: releaseRes['media']?.[0] +// } +// } + async function downloadTrack(track: GwTrack, opts: { destination: string album?: GwAlbum }) { const albumUrl = `https://cdn-images.dzcdn.net/images/cover/${track.ALB_PICTURE}/1500x1500-000000-80-0-0.jpg` - const [getUrlRes, albumAb, lyricsRes] = await Promise.all([ + const [ + getUrlRes, + albumAb, + lyricsRes, + ] = await Promise.all([ ffetch.post('https://media.deezer.com/v1/get_url', { json: { license_token: userData.USER.OPTIONS.license_token, @@ -343,6 +424,8 @@ async function downloadTrack(track: GwTrack, opts: { }), ]) + // console.dir(getUrlRes, { depth: null }) + const albumCoverPath = join(`assets/deezer-tmp-${track.SNG_ID}.jpg`) await writeFile(albumCoverPath, new Uint8Array(albumAb)) @@ -487,6 +570,10 @@ async function downloadTrack(track: GwTrack, opts: { params.push(`--set-tag=COPYRIGHT=${opts.album.COPYRIGHT}`) } + if (lyricsLrc) { + params.push(`--set-tag=LYRICS=${lyricsLrc}`) + } + params.push(filename) await $`metaflac ${params}` @@ -605,9 +692,10 @@ async function downloadArtist(artistId: string) { spinnies.succeed('collect', { text: `collected ${albums.length} albums with a total of ${trackCount} tracks` }) } - // fixme: singles should always contain artist name and be saved in artist root dir - // fixme: "featured" albums (i.e. when main artist of the album is not the one we're dling) should have album artist name in its dirname + // fixme: "featured" albums/tracks (i.e. when main artist of the album is not the one we're dling) should have album artist name in its dirname + // fixme: singles should be saved in artist root dir // todo: automatic musicbrainz matching + // todo: automatic genius/musixmatch matching for lyrics if unavailable directly from deezer await asyncPool(albums, async (alb) => { const tracks = await gwLightApi({ @@ -784,6 +872,14 @@ if (url.match(/^(artist|album|track):(\d+)$/)) { node: z.object({ id: z.string(), title: z.string(), + contributors: z.object({ + edges: z.array(z.object({ + node: z.object({ + id: z.string(), + name: z.string(), + }), + })), + }), }), })), }), @@ -801,7 +897,7 @@ if (url.match(/^(artist|album|track):(\d+)$/)) { } for (const [i, { node }] of iter.enumerate(searchResult.instantSearch.results.tracks.edges)) { - console.log(`track:${node.id}: ${node.title}`) + console.log(`track:${node.id}: ${node.contributors.edges.map(it => it.node.name).join(', ')} - ${node.title}`) } const uri = await question('option > ') diff --git a/scripts/misc/twitch-autoreg.ts b/scripts/misc/twitch-autoreg.ts new file mode 100644 index 0000000..37d82e1 --- /dev/null +++ b/scripts/misc/twitch-autoreg.ts @@ -0,0 +1,365 @@ +import type { Browser } from 'patchright' +import type { EmailVerificationProvider } from '../../utils/temkakit/email-verification.ts' +import { writeFile } from 'node:fs/promises' +import { faker } from '@faker-js/faker' +import { sleep } from '@fuman/utils' +import { load } from 'cheerio' +import { Cookie, CookieJar } from 'tough-cookie' +import { ffetch as ffetchBase } from '../../utils/fetch.ts' +import { AnymessageEmailVerificationProvider } from '../../utils/temkakit/anymessage.ts' +import { solveKasadaSalamoonder } from '../../utils/temkakit/kasada-solver.ts' +import { createLibcurlFetch } from '../../utils/temkakit/libcurl.ts' + +// half broken, unfinished + +function getProxy() { + // return { + // user: 'JaTjXK', + // pass: 'WYsU4C', + // host: '38.152.247.16', + // port: 9785, + // } + return { + user: '', + pass: '', + host: '127.0.0.1', + port: 7891, + } +} + +function proxyToUrl(proxy: { user: string, pass: string, host: string, port: number }) { + return `http://${proxy.user}:${proxy.pass}@${proxy.host}:${proxy.port}` +} + +const THREADS = 1 +const ACCOUNTS_COUNT = 2 + +const TWITCH_PJS = 'https://k.twitchcdn.net/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/p.js' + +async function twitchAutoreg(options: { + // browser: Browser + emailProvider: EmailVerificationProvider + log?: (format: string, ...args: any[]) => void + proxy?: string +}) { + const { + // browser, + proxy, + emailProvider, + log = (fmt, ...args) => console.log(fmt, ...args), + } = options + const jar = new CookieJar() + + log('proxy', proxy) + const ffetch = ffetchBase.extend({ + cookies: jar, + fetch: createLibcurlFetch({ proxy }), + }) + + log('fetching main page') + const mainPage = await ffetch('https://www.twitch.tv/').text() + const twilightBuildId = mainPage.match(/window.__twilightBuildID="([^"]+)"/)?.[1] + if (!twilightBuildId) { + throw new Error('failed to get twilightBuildId') + } + + await jar.setCookie(new Cookie({ + key: 'api_token', + value: `twilight.${faker.string.hexadecimal({ length: 32 })}`, + domain: 'twitch.tv', + path: '/', + secure: true, + sameSite: 'None', + hostOnly: false, + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }), 'https://www.twitch.tv') + await jar.setCookie(new Cookie({ + key: 'experiment_overrides', + value: encodeURIComponent(JSON.stringify({ experiments: {}, disabled: [] })), + domain: 'twitch.tv', + path: '/', + secure: true, + sameSite: 'None', + hostOnly: false, + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }), 'https://www.twitch.tv') + + const deviceId = faker.string.alphanumeric({ length: 32 }) + const sessionId = faker.string.hexadecimal({ length: 16 }) + + log('generating integrity token') + + // const kasadaSolver = await createKasadaSolver({ + // pageUrl: 'https://www.twitch.tv/', + // scriptUrl: '', + // browser, + // beforePageLoad: async (page) => { + // await syncCookiesIntoBrowser(jar, page.context()) + // }, + // requests: [ + // { + // protocol: 'https', + // method: 'POST', + // domain: 'gql.twitch.tv', + // path: '/integrity', + // }, + // { + // protocol: 'https', + // method: 'POST', + // domain: 'passport.twitch.tv', + // path: '/integrity', + // }, + // { + // protocol: 'https', + // method: 'POST', + // domain: 'passport.twitch.tv', + // path: '/protected_register', + // }, + // { + // protocol: 'https', + // method: 'POST', + // domain: 'passport.twitch.tv', + // path: '/protected_login', + // }, + // ], + // }) + + const commonHeaders: Record = { + 'X-Device-Id': deviceId, + 'Client-Id': 'kimne78kx3ncx6brgo4mv6wki5h1ko', + 'Client-Request-Id': faker.string.alphanumeric({ length: 32 }), + 'Client-Session-Id': sessionId, + 'Client-Version': twilightBuildId, + } + + const kasadaSolution = await solveKasadaSalamoonder({ pjs: TWITCH_PJS }) + // const integrityToken = await kasadaSolver.request({ + // url: 'https://gql.twitch.tv/integrity', + // method: 'POST', + // headers: commonHeaders, + // }) as { token: string } + const integrityToken = await ffetch('https://gql.twitch.tv/integrity', { + method: 'POST', + headers: { + ...commonHeaders, + ...kasadaSolution, + }, + }).json() as { token: string } + + const ffetchGql = ffetchBase.extend({ + headers: { + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + ...commonHeaders, + 'Client-Integrity': integrityToken.token, + }, + }) + + // await syncCookiesFromBrowser(kasadaSolver.page.context(), jar) + + let username + while (true) { + username = faker.internet.username().toLowerCase().replace(/[^a-z0-9]/gi, '') + log('checking username', username) + const r = await ffetchGql.post('https://gql.twitch.tv/gql', { + json: [ + { + operationName: 'UsernameValidator_User', + variables: { username }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: 'fd1085cf8350e309b725cf8ca91cd90cac03909a3edeeedbd0872ac912f3d660', + }, + }, + }, + ], + }).json() as any + + if (r[0].errors) { + throw new Error(`failed to check username:${JSON.stringify(r[0].errors)}`) + } + + if (r[0].data.isUsernameAvailable) { + log('username is available: %s', username) + break + } + + await sleep(1000) + } + + log('ordering email') + + const email = await emailProvider.getEmail() + + log('got email: %s, registering', email) + + const password = faker.internet.password({ length: 16, pattern: /[a-z0-9]/ }) + const birthday = faker.date.birthdate({ min: 18, max: 25, mode: 'age' }) + const registerBody: Record = { + username, + password, + email, + birthday: { + day: birthday.getDate(), + month: birthday.getMonth() + 1, + year: birthday.getFullYear(), + isOver18: true, + }, + email_marketing_opt_in: false, + client_id: 'kimne78kx3ncx6brgo4mv6wki5h1ko', + is_password_guide: 'nist', + } + + for (let i = 0; i < 5; i++) { + // const r1 = await kasadaSolver.request({ + // url: 'https://passport.twitch.tv/protected_register', + // method: 'POST', + // body: JSON.stringify(registerBody), + // headers: { + // 'Content-Type': 'text/plain;charset=UTF-8', + // 'Accept': '*/*', + // }, + // credentials: 'include', + // }) as { error_code: number } + log('solving kasada') + const kasadaSolution = await solveKasadaSalamoonder({ pjs: TWITCH_PJS }) + const r1 = await ffetch.post('https://passport.twitch.tv/protected_register', { + validateResponse: false, + json: registerBody, + headers: { + ...kasadaSolution, + }, + }).json() as { error_code: number } + + log('r1', r1) + + if (i < 4 && r1.error_code === 5025) { + log('integrity failed, retrying...') + continue + } + + if (r1.error_code !== 2026) { + await emailProvider.dispose() + throw new Error(`failed to register: ${JSON.stringify(r1)}`) + } + + break + } + + log('waiting for code') + const message = await emailProvider.waitForMessage({ timeout: 90_000 }) + const message$ = load(message) + const code = message$('center p[style^=background]').text() // what the fuck is this selector + // const code = await question('code: ') + + if (!code.match(/^\d{6}$/)) { + log('āŒ invalid code parsed: %s', code) + log(message) + await emailProvider.dispose() + throw new Error(`invalid code parsed:${code}`) + } + + log('code: %s', code) + + registerBody.email_verification_code = code + for (let i = 0; i < 5; i++) { + // const r2 = await kasadaSolver.request({ + // url: 'https://passport.twitch.tv/protected_register', + // method: 'POST', + // body: JSON.stringify(registerBody), + // headers: { + // 'Content-Type': 'text/plain;charset=UTF-8', + // }, + // credentials: 'include', + // }) as { error_code: number } + log('solving kasada') + const kasadaSolution = await solveKasadaSalamoonder({ pjs: TWITCH_PJS }) + const r2 = await ffetch.post('https://passport.twitch.tv/protected_register', { + json: registerBody, + validateResponse: false, + headers: { + ...kasadaSolution, + }, + }).json() as { error_code: number } + + if (i < 4 && r2.error_code === 5025) { + log('integrity failed, retrying...') + continue + } + + if (r2.error_code) { + await emailProvider.dispose() + throw new Error(`āŒ failed to register:${r2.error_code}`) + } + + break + } + + // await syncCookiesFromBrowser(kasadaSolver.page.context(), jar) + + log('авторег работает!') + await emailProvider.dispose() + + return { + username, + password, + email, + cookies: await jar.store.getAllCookies(), + } +} + +let started = 0 +let completed = 0 +await Promise.all(Array.from({ length: THREADS }).map(async (_, idx) => { + const emailProvider = new AnymessageEmailVerificationProvider({ + site: 'twitch.tv', + domain: 'hotmail.com', + }) + + let browser: Browser | null = null + while (true) { + if (started >= ACCOUNTS_COUNT) { + break + } + + started++ + + const log = (fmt: string, ...args: any[]) => console.log(`[worker ${idx}] ${fmt}`, ...args) + + try { + const proxy = getProxy() + + // browser = await chromium.launch({ + // channel: 'chrome', + // headless: false, + // env: { + // TZ: 'Europe/Amsterdam', + // }, + // proxy: { + // server: `http://${proxy.host}:${proxy.port}`, + // username: proxy.user, + // password: proxy.pass, + // }, + // }) + + const acct = await twitchAutoreg({ + // browser, + proxy: proxyToUrl(proxy), + emailProvider, + log, + }) + + await writeFile('assets/twitch-accs.txt', `${JSON.stringify(acct)}\n`, { flag: 'a' }) + + completed++ + log('completed: %d/%d', completed, ACCOUNTS_COUNT) + } catch (e) { + log('autoreg error: %s', e) + // await browser?.close() + browser = null + started-- + } + } +})) diff --git a/utils/captcha.ts b/utils/captcha.ts deleted file mode 100644 index 11d4f60..0000000 --- a/utils/captcha.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { sleep } from '@fuman/utils' -import { z } from 'zod' -import { ffetch } from './fetch.ts' -import { getEnv } from './misc.ts' - -const CreateTaskResponse = z.object({ - errorId: z.number(), - errorCode: z.string().optional().nullable(), - taskId: z.number(), -}) - -const GetTaskResultResponse = z.object({ - errorId: z.number(), - errorCode: z.string().optional().nullable(), - status: z.enum(['ready', 'processing']), - solution: z.unknown().optional(), -}) - -export async function solveCaptcha(task: unknown) { - const res = await ffetch.post('https://api.capmonster.cloud/createTask', { - json: { - clientKey: getEnv('CAPMONSTER_API_TOKEN'), - task, - }, - }).parsedJson(CreateTaskResponse) - - if (res.errorId) { - throw new Error(`createTask error ${res.errorId}: ${res.errorCode}`) - } - - const taskId = res.taskId - - await sleep(5_000) - - let requestCount = 0 - - while (true) { - requestCount += 1 - if (requestCount > 100) { - // "Limit: 120 requests per task. If the limit is exceeded, the user's account may be temporarily locked." - // just to be safe - throw new Error('captcha request count exceeded') - } - - const res = await ffetch.post('https://api.capmonster.cloud/getTaskResult', { - json: { - clientKey: getEnv('CAPMONSTER_API_TOKEN'), - taskId, - }, - }).parsedJson(GetTaskResultResponse) - - if (res.errorId) { - throw new Error(`getTaskResult error ${res.errorId}: ${res.errorCode}`) - } - - if (res.status === 'ready') { - return res.solution - } - - await sleep(2_000) - } -} - -export async function solveRecaptcha(params?: { - url: string - siteKey: string - s?: string - userAgent?: string - cookies?: string - isInvisible?: boolean -}) { - const res = await solveCaptcha({ - type: 'RecaptchaV2TaskProxyless', - websiteURL: params?.url, - websiteKey: params?.siteKey, - recaptchaDataSValue: params?.s, - userAgent: params?.userAgent, - cookies: params?.cookies, - isInvisible: params?.isInvisible, - }) - - if (typeof res !== 'object' || !res || !('gRecaptchaResponse' in res) || typeof res.gRecaptchaResponse !== 'string') { - throw new Error('invalid recaptcha response') - } - - return res.gRecaptchaResponse -} diff --git a/utils/navidrome.ts b/utils/navidrome.ts index d99477d..cd896a9 100644 --- a/utils/navidrome.ts +++ b/utils/navidrome.ts @@ -33,6 +33,16 @@ export const NavidromeSong = z.object({ libraryPath: z.string(), duration: z.number(), size: z.number(), + participants: z.object({ + artist: z.object({ + id: z.string(), + name: z.string(), + }).array().optional(), + }).optional(), + mbzRecordingID: z.string().optional(), + mbzReleaseTrackId: z.string().optional(), + mbzAlbumId: z.string().optional(), + mbzReleaseGroupId: z.string().optional(), }) export type NavidromeSong = z.infer @@ -43,7 +53,7 @@ export async function fetchSongs(offset: number, pageSize: number) { _start: offset, _end: offset + pageSize, _order: 'ASC', - _sort: 'title', + _sort: 'path', }, }).parsedJson(z.array(NavidromeSong)) } From a40bf6b47258ea6fcffaffaf28f5cd1618f6e8c8 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sun, 14 Sep 2025 21:52:13 +0000 Subject: [PATCH 53/61] chore: update public repo --- scripts/media/deezer-dl.ts | 157 ++++++++++++--------------------- scripts/media/soundcloud-dl.ts | 2 +- scripts/misc/yamusic-token.ts | 87 ++++++++++++++++++ utils/fs.ts | 16 ++++ utils/media-metadata.ts | 102 +++++++++++++++++++++ utils/opus.ts | 30 ------- 6 files changed, 261 insertions(+), 133 deletions(-) create mode 100644 scripts/misc/yamusic-token.ts create mode 100644 utils/media-metadata.ts delete mode 100644 utils/opus.ts diff --git a/scripts/media/deezer-dl.ts b/scripts/media/deezer-dl.ts index 96923db..73ae653 100644 --- a/scripts/media/deezer-dl.ts +++ b/scripts/media/deezer-dl.ts @@ -12,7 +12,8 @@ import { FileCookieStore } from 'tough-cookie-file-store' import { z } from 'zod' import { $, question } from 'zx' import { ffetch as ffetchBase } from '../../utils/fetch.ts' -import { sanitizeFilename } from '../../utils/fs.ts' +import { sanitizeFilename, writeWebStreamToFile } from '../../utils/fs.ts' +import { generateFfmpegMetadataFlags, pipeIntoProc, runMetaflac } from '../../utils/media-metadata.ts' import { getEnv } from '../../utils/misc.ts' const jar = new CookieJar(new FileCookieStore('./assets/deezer-cookies.json')) @@ -151,7 +152,7 @@ const GwTrack = z.object({ SNG_ID: z.string(), SNG_TITLE: z.string(), TRACK_NUMBER: z.string(), - VERSION: z.string(), + VERSION: z.string().optional(), MD5_ORIGIN: z.string(), FILESIZE_AAC_64: z.coerce.number(), FILESIZE_MP3_64: z.coerce.number(), @@ -182,6 +183,8 @@ const GwAlbum = z.object({ ART_ID: z.string(), ART_NAME: z.string(), })), + TYPE: z.string().optional(), // "0" = single, "1" = normal album, "3" = ep + ROLE_ID: z.number().optional(), // 0 = own album, 5 = "featured on" album COPYRIGHT: z.string(), PRODUCER_LINE: z.string(), DIGITAL_RELEASE_DATE: z.string(), @@ -471,112 +474,48 @@ async function downloadTrack(track: GwTrack, opts: { '0:a', '-c', 'copy', - '-metadata', - `title=${getTrackName(track)}`, - '-metadata', - `album=${track.ALB_TITLE}`, - '-metadata', - `year=${track.DIGITAL_RELEASE_DATE}`, - '-metadata', - `comment=ripped from deezer (id: ${track.SNG_ID})`, - '-metadata', - `track=${track.TRACK_NUMBER}`, - '-metadata', - `disc=${track.DISK_NUMBER}`, + ...generateFfmpegMetadataFlags({ + title: getTrackName(track), + album: opts.album?.ALB_TITLE ?? track.ALB_TITLE, + year: track.DIGITAL_RELEASE_DATE, + comment: `ripped from deezer (id: ${track.SNG_ID})`, + track: track.TRACK_NUMBER, + disc: track.DISK_NUMBER, + composer: track.SNG_CONTRIBUTORS?.composer, + artist: track.ARTISTS?.map(it => it.ART_NAME) ?? track.ART_NAME, + }), filename, ] - if (opts.album) { - params.push('-metadata', `album=${opts.album.ALB_TITLE}`) - } - - if (track.SNG_CONTRIBUTORS?.composer) { - for (const composer of track.SNG_CONTRIBUTORS.composer) { - params.push('-metadata', `composer=${composer}`) - } - } - - if (track.ARTISTS?.length) { - for (const artist of track.ARTISTS) { - params.push('-metadata', `artist=${artist.ART_NAME}`) - } - } else { - params.push('-metadata', `artist=${track.ART_NAME}`) - } - if (lyricsLrc) { await writeFile(`${opts.destination}.lrc`, lyricsLrc) } const proc = $`ffmpeg ${params}` - const pipe = Readable.fromWeb(decStream as any).pipe(proc.stdin) - await new Promise((resolve, reject) => { - pipe.on('error', reject) - pipe.on('finish', resolve) - }) + await pipeIntoProc(proc, decStream) await proc } else { - const fd = await open(filename, 'w+') - const writer = fd.createWriteStream() + await writeWebStreamToFile(decStream, filename) - for await (const chunk of decStream as any) { - writer.write(chunk) - } - - writer.end() - - await new Promise((resolve, reject) => { - writer.on('error', reject) - writer.on('finish', resolve) + await runMetaflac({ + path: filename, + tags: { + TITLE: getTrackName(track), + ALBUM: track.ALB_TITLE, + DATE: track.DIGITAL_RELEASE_DATE ?? asNonNull(opts.album?.DIGITAL_RELEASE_DATE), + DISCNUMBER: track.DISK_NUMBER, + TRACKNUMBER: track.TRACK_NUMBER, + COMMENT: `ripped from deezer (id: ${track.SNG_ID})`, + ARTIST: track.ARTISTS?.map(it => it.ART_NAME) ?? track.ART_NAME, + COMPOSER: track.SNG_CONTRIBUTORS?.composer, + MAIN_ARTIST: track.SNG_CONTRIBUTORS?.main_artist, + ISRC: track.ISRC, + PRODUCER: opts.album?.PRODUCER_LINE, + COPYRIGHT: opts.album?.COPYRIGHT, + LYRICS: lyricsLrc, + }, + coverPath: albumCoverPath, }) - - const params: string[] = [ - '--remove-all-tags', - `--set-tag=TITLE=${getTrackName(track)}`, - `--set-tag=ALBUM=${track.ALB_TITLE}`, - `--set-tag=DATE=${track.DIGITAL_RELEASE_DATE ?? asNonNull(opts.album?.DIGITAL_RELEASE_DATE)}`, - `--set-tag=DISCNUMBER=${track.DISK_NUMBER}`, - `--set-tag=TRACKNUMBER=${track.TRACK_NUMBER}`, - `--set-tag=COMMENT=ripped from deezer (id: ${track.SNG_ID})`, - `--import-picture-from=${albumCoverPath}`, - ] - - if (track.ARTISTS) { - for (const artist of track.ARTISTS) { - params.push(`--set-tag=ARTIST=${artist.ART_NAME}`) - } - } else { - params.push(`--set-tag=ARTIST=${track.ART_NAME}`) - } - - if (track.SNG_CONTRIBUTORS?.composer) { - for (const composer of track.SNG_CONTRIBUTORS.composer) { - params.push(`--set-tag=COMPOSER=${composer}`) - } - } - - if (track.SNG_CONTRIBUTORS?.main_artist) { - for (const mainArtist of track.SNG_CONTRIBUTORS.main_artist) { - params.push(`--set-tag=MAIN_ARTIST=${mainArtist}`) - } - } - - if (track.ISRC) { - params.push(`--set-tag=ISRC=${track.ISRC}`) - } - - if (opts.album) { - params.push(`--set-tag=PRODUCER=${opts.album.PRODUCER_LINE}`) - params.push(`--set-tag=COPYRIGHT=${opts.album.COPYRIGHT}`) - } - - if (lyricsLrc) { - params.push(`--set-tag=LYRICS=${lyricsLrc}`) - } - - params.push(filename) - - await $`metaflac ${params}` } await rm(albumCoverPath, { force: true }) @@ -595,7 +534,7 @@ async function downloadTrackList(tracks: GwTrack[], opts: { const isMultiDisc = tracks.some(it => it.DISK_NUMBER !== '1') const firstTrackArtistString = getTrackArtistString(tracks[0]) - const isVariousArtists = tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) + const isDifferentArtists = tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) await asyncPool(tracks, async (track) => { let filename = '' @@ -605,7 +544,7 @@ async function downloadTrackList(tracks: GwTrack[], opts: { } filename = `${track.TRACK_NUMBER.padStart(2, '0')}. ` } - if (isVariousArtists) { + if (isDifferentArtists) { filename += `${getTrackArtistString(track)} - ` } filename += `${getTrackName(track)}` @@ -636,7 +575,11 @@ const GwPageArtist = z.object({ }), }) -async function downloadArtist(artistId: string) { +async function downloadArtist(options: { + artistId: string + includeFeaturedAlbums?: boolean +}) { + const { artistId, includeFeaturedAlbums = false } = options const artistInfo = await gwLightApi({ method: 'deezer.pageArtist', token: userData.checkForm, @@ -681,6 +624,8 @@ async function downloadArtist(artistId: string) { }) for (const alb of res.data) { + if (!includeFeaturedAlbums && alb.ROLE_ID === 5) continue + albums.push(alb) trackCount += asNonNull(alb.SONGS).total } @@ -692,7 +637,6 @@ async function downloadArtist(artistId: string) { spinnies.succeed('collect', { text: `collected ${albums.length} albums with a total of ${trackCount} tracks` }) } - // fixme: "featured" albums/tracks (i.e. when main artist of the album is not the one we're dling) should have album artist name in its dirname // fixme: singles should be saved in artist root dir // todo: automatic musicbrainz matching // todo: automatic genius/musixmatch matching for lyrics if unavailable directly from deezer @@ -713,11 +657,16 @@ async function downloadArtist(artistId: string) { assert(tracks.total === asNonNull(alb.SONGS).total) assert(tracks.data.length === asNonNull(alb.SONGS).total) + let folderName = alb.ALB_TITLE + if (alb.ROLE_ID === 5) { + folderName = `${artistInfo.DATA.ART_NAME} - ${folderName}` + } + await downloadTrackList(tracks.data, { destination: join( 'assets/deezer-dl', sanitizeFilename(artistInfo.DATA.ART_NAME), - sanitizeFilename(alb.ALB_TITLE), + sanitizeFilename(folderName), ), album: alb, poolLimit: 4, @@ -827,7 +776,11 @@ async function downloadByUri(uri: string) { } if (type === 'artist') { - await downloadArtist(id) + const includeFeaturedAlbums = await question('include featured albums? (y/N) > ') + await downloadArtist({ + artistId: id, + includeFeaturedAlbums: includeFeaturedAlbums.toLowerCase() === 'y', + }) } } diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index ac459dc..e931f92 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -10,7 +10,7 @@ import { $, ProcessOutput, question } from 'zx' import { downloadFile, ffetch as ffetchBase } from '../../utils/fetch.ts' import { sanitizeFilename } from '../../utils/fs.ts' import { chunks, getEnv } from '../../utils/misc.ts' -import { generateOpusImageBlob } from '../../utils/opus.ts' +import { generateOpusImageBlob } from '../../utils/media-metadata.ts' const ffetchApi = ffetchBase.extend({ baseUrl: 'https://api-v2.soundcloud.com', diff --git a/scripts/misc/yamusic-token.ts b/scripts/misc/yamusic-token.ts new file mode 100644 index 0000000..d248d26 --- /dev/null +++ b/scripts/misc/yamusic-token.ts @@ -0,0 +1,87 @@ +import { randomBytes } from 'node:crypto' +import { faker } from '@faker-js/faker' +import { question } from 'zx' +import { ffetch } from '../../utils/fetch.ts' + +// log in with your yandex account in the browser, then go to music.yandex.ru and open devtools +// find long ass string in "Cookie" header from the requests to music.yandex.ru, it must contain "Session_id" cookie. +// make sure to copy it completely (on firefox this requires toggling "Raw") +// looks something like: is_gdpr=0; is_gdpr=0; is_gdpr_b=COnCMBCR0wIoAg==; _yasc=ctfv6IPUcb+Lk+jqYr0thW1STKmQC5yB4IJUM5Gn.... +const cookies = await question('music.yandex.ru cookies > ') + +const parsed = new Map(cookies.split('; ').map((cookie) => { + const [name, value] = cookie.split('=') + return [name, value] +})) + +if (!parsed.has('Session_id')) { + throw new Error('Session_id cookie not found') +} + +const deviceId = randomBytes(16).toString('hex') +const uuid = randomBytes(16).toString('hex') +const genRequestId = () => `${uuid}${Math.floor(Date.now())}` +const query = { + manufacturer: 'Google', + model: 'Pixel 9 Pro XL', + app_platform: 'Android 16 (REL)', + am_version_name: '7.46.0(746003972)', + app_id: 'ru.yandex.music', + app_version_name: '2025.09.2 #114gpr', + am_app: 'ru.yandex.music 2025.09.2 #114gpr', + deviceid: deviceId, + device_id: deviceId, + uuid, +} + +const res = await ffetch('https://mobileproxy.passport.yandex.net/1/bundle/oauth/token_by_sessionid', { + query: { + ...query, + request_id: genRequestId(), + }, + form: { + client_id: 'c0ebe342af7d48fbbbfcf2d2eedb8f9e', + client_secret: 'ad0a908f0aa341a182a37ecd75bc319e', + grant_type: 'sessionid', + host: 'yandex.ru', + }, + headers: { + 'Accept': '*/*', + 'User-Agent': 'com.yandex.mobile.auth.sdk/7.46.0.746003972 (Google Pixel 9 Pro XL; Android 16) PassportSDK/7.46.0.746003972', + 'Accept-Language': 'en-RU;q=1, ru-RU;q=0.9', + 'Ya-Client-Host': 'passport.yandex.ru', + 'Ya-Client-Cookie': cookies, + }, +}).json() as any + +if (res.status !== 'ok') { + console.error('Unexpected response:', res) + process.exit(1) +} + +console.log('res', res) + +const res2 = await ffetch('https://mobileproxy.passport.yandex.net/1/token', { + query: { + ...query, + request_id: genRequestId(), + }, + form: { + access_token: res.access_token, + client_id: '23cabbbdc6cd418abb4b39c32c41195d', + client_secret: '53bc75238f0c4d08a118e51fe9203300', + grant_type: 'x-token', + }, +}).json() as any + +if (!res2.access_token) { + console.error('Unexpected response:', res2) + process.exit(1) +} + +console.log('res2', res2) + +console.log('') +console.log('Your auth token is:') +console.log(res2.access_token) +console.log('Expires at:', new Date(Date.now() + res.expires_in * 1000).toLocaleString('ru-RU')) diff --git a/utils/fs.ts b/utils/fs.ts index 11ab17f..b2d9bcc 100644 --- a/utils/fs.ts +++ b/utils/fs.ts @@ -21,3 +21,19 @@ export async function directoryExists(path: string): Promise { export function sanitizeFilename(filename: string) { return filename.replace(/[/\\?%*:|"<>]/g, '_') } + +export async function writeWebStreamToFile(stream: ReadableStream, path: string) { + const fd = await fsp.open(path, 'w+') + const writer = fd.createWriteStream() + + for await (const chunk of stream as any) { + writer.write(chunk) + } + + writer.end() + + await new Promise((resolve, reject) => { + writer.on('error', reject) + writer.on('finish', resolve) + }) +} diff --git a/utils/media-metadata.ts b/utils/media-metadata.ts new file mode 100644 index 0000000..3b6ad13 --- /dev/null +++ b/utils/media-metadata.ts @@ -0,0 +1,102 @@ +import type { ProcessPromise } from 'zx' +import { Readable } from 'node:stream' +import { Bytes, write } from '@fuman/io' +import { $ } from 'zx' + +export async function generateOpusImageBlob(image: Uint8Array) { + // todo we should probably not use ffprobe here but whatever lol + const proc = $`ffprobe -of json -v error -show_entries stream=codec_name,width,height pipe:0` + proc.stdin.write(image) + proc.stdin.end() + const json = await proc.json() + + const img = json.streams[0] + // https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8 + const mime = img.codec_name === 'mjpeg' ? 'image/jpeg' : 'image/png' + const description = 'Cover Artwork' + + const res = Bytes.alloc(image.length + 128) + write.uint32be(res, 3) // picture type = album cover + write.uint32be(res, mime.length) + write.rawString(res, mime) + write.uint32be(res, description.length) + write.rawString(res, description) + write.uint32be(res, img.width) + write.uint32be(res, img.height) + write.uint32be(res, 0) // color depth + write.uint32be(res, 0) // color index (unused, for gifs) + write.uint32be(res, image.length) + write.bytes(res, image) + + return res.result() +} + +export async function runMetaflac(options: { + path: string + tags: Partial + > + coverPath?: string +}) { + const params: string[] = [ + '--remove-all-tags', + ] + + for (const [key, value] of Object.entries(options.tags)) { + if (value == null) continue + if (Array.isArray(value)) { + for (const v of value) { + params.push(`--set-tag=${key}=${v}`) + } + } else { + params.push(`--set-tag=${key}=${value}`) + } + } + + if (options.coverPath) { + params.push(`--import-picture-from=${options.coverPath}`) + } + + params.push(options.path) + + await $`metaflac ${params}` +} + +export function generateFfmpegMetadataFlags(metadata: Partial>) { + const res: string[] = [] + + for (const [key, value] of Object.entries(metadata)) { + if (value == null) continue + if (Array.isArray(value)) { + for (const v of value) { + res.push('-metadata', `${key}=${v}`) + } + } else { + res.push('-metadata', `${key}=${value}`) + } + } + + return res +} + +export async function pipeIntoProc(proc: ProcessPromise, stream: ReadableStream) { + const pipe = Readable.fromWeb(stream as any).pipe(proc.stdin) + await new Promise((resolve, reject) => { + pipe.on('error', reject) + pipe.on('finish', resolve) + }) +} diff --git a/utils/opus.ts b/utils/opus.ts deleted file mode 100644 index 06ff13b..0000000 --- a/utils/opus.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Bytes, write } from '@fuman/io' -import { $ } from 'zx' - -export async function generateOpusImageBlob(image: Uint8Array) { - // todo we should probably not use ffprobe here but whatever lol - const proc = $`ffprobe -of json -v error -show_entries stream=codec_name,width,height pipe:0` - proc.stdin.write(image) - proc.stdin.end() - const json = await proc.json() - - const img = json.streams[0] - // https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8 - const mime = img.codec_name === 'mjpeg' ? 'image/jpeg' : 'image/png' - const description = 'Cover Artwork' - - const res = Bytes.alloc(image.length + 128) - write.uint32be(res, 3) // picture type = album cover - write.uint32be(res, mime.length) - write.rawString(res, mime) - write.uint32be(res, description.length) - write.rawString(res, description) - write.uint32be(res, img.width) - write.uint32be(res, img.height) - write.uint32be(res, 0) // color depth - write.uint32be(res, 0) // color index (unused, for gifs) - write.uint32be(res, image.length) - write.bytes(res, image) - - return res.result() -} From 96ca247fcb510c8575a724e6e1d1cd1da0f45291 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sun, 14 Sep 2025 21:52:13 +0000 Subject: [PATCH 54/61] chore: update public repo --- scripts/media/deezer-dl.ts | 157 ++++++++++++--------------------- scripts/media/soundcloud-dl.ts | 2 +- scripts/misc/yamusic-token.ts | 87 ++++++++++++++++++ utils/fs.ts | 16 ++++ utils/media-metadata.ts | 102 +++++++++++++++++++++ utils/opus.ts | 30 ------- 6 files changed, 261 insertions(+), 133 deletions(-) create mode 100644 scripts/misc/yamusic-token.ts create mode 100644 utils/media-metadata.ts delete mode 100644 utils/opus.ts diff --git a/scripts/media/deezer-dl.ts b/scripts/media/deezer-dl.ts index 96923db..73ae653 100644 --- a/scripts/media/deezer-dl.ts +++ b/scripts/media/deezer-dl.ts @@ -12,7 +12,8 @@ import { FileCookieStore } from 'tough-cookie-file-store' import { z } from 'zod' import { $, question } from 'zx' import { ffetch as ffetchBase } from '../../utils/fetch.ts' -import { sanitizeFilename } from '../../utils/fs.ts' +import { sanitizeFilename, writeWebStreamToFile } from '../../utils/fs.ts' +import { generateFfmpegMetadataFlags, pipeIntoProc, runMetaflac } from '../../utils/media-metadata.ts' import { getEnv } from '../../utils/misc.ts' const jar = new CookieJar(new FileCookieStore('./assets/deezer-cookies.json')) @@ -151,7 +152,7 @@ const GwTrack = z.object({ SNG_ID: z.string(), SNG_TITLE: z.string(), TRACK_NUMBER: z.string(), - VERSION: z.string(), + VERSION: z.string().optional(), MD5_ORIGIN: z.string(), FILESIZE_AAC_64: z.coerce.number(), FILESIZE_MP3_64: z.coerce.number(), @@ -182,6 +183,8 @@ const GwAlbum = z.object({ ART_ID: z.string(), ART_NAME: z.string(), })), + TYPE: z.string().optional(), // "0" = single, "1" = normal album, "3" = ep + ROLE_ID: z.number().optional(), // 0 = own album, 5 = "featured on" album COPYRIGHT: z.string(), PRODUCER_LINE: z.string(), DIGITAL_RELEASE_DATE: z.string(), @@ -471,112 +474,48 @@ async function downloadTrack(track: GwTrack, opts: { '0:a', '-c', 'copy', - '-metadata', - `title=${getTrackName(track)}`, - '-metadata', - `album=${track.ALB_TITLE}`, - '-metadata', - `year=${track.DIGITAL_RELEASE_DATE}`, - '-metadata', - `comment=ripped from deezer (id: ${track.SNG_ID})`, - '-metadata', - `track=${track.TRACK_NUMBER}`, - '-metadata', - `disc=${track.DISK_NUMBER}`, + ...generateFfmpegMetadataFlags({ + title: getTrackName(track), + album: opts.album?.ALB_TITLE ?? track.ALB_TITLE, + year: track.DIGITAL_RELEASE_DATE, + comment: `ripped from deezer (id: ${track.SNG_ID})`, + track: track.TRACK_NUMBER, + disc: track.DISK_NUMBER, + composer: track.SNG_CONTRIBUTORS?.composer, + artist: track.ARTISTS?.map(it => it.ART_NAME) ?? track.ART_NAME, + }), filename, ] - if (opts.album) { - params.push('-metadata', `album=${opts.album.ALB_TITLE}`) - } - - if (track.SNG_CONTRIBUTORS?.composer) { - for (const composer of track.SNG_CONTRIBUTORS.composer) { - params.push('-metadata', `composer=${composer}`) - } - } - - if (track.ARTISTS?.length) { - for (const artist of track.ARTISTS) { - params.push('-metadata', `artist=${artist.ART_NAME}`) - } - } else { - params.push('-metadata', `artist=${track.ART_NAME}`) - } - if (lyricsLrc) { await writeFile(`${opts.destination}.lrc`, lyricsLrc) } const proc = $`ffmpeg ${params}` - const pipe = Readable.fromWeb(decStream as any).pipe(proc.stdin) - await new Promise((resolve, reject) => { - pipe.on('error', reject) - pipe.on('finish', resolve) - }) + await pipeIntoProc(proc, decStream) await proc } else { - const fd = await open(filename, 'w+') - const writer = fd.createWriteStream() + await writeWebStreamToFile(decStream, filename) - for await (const chunk of decStream as any) { - writer.write(chunk) - } - - writer.end() - - await new Promise((resolve, reject) => { - writer.on('error', reject) - writer.on('finish', resolve) + await runMetaflac({ + path: filename, + tags: { + TITLE: getTrackName(track), + ALBUM: track.ALB_TITLE, + DATE: track.DIGITAL_RELEASE_DATE ?? asNonNull(opts.album?.DIGITAL_RELEASE_DATE), + DISCNUMBER: track.DISK_NUMBER, + TRACKNUMBER: track.TRACK_NUMBER, + COMMENT: `ripped from deezer (id: ${track.SNG_ID})`, + ARTIST: track.ARTISTS?.map(it => it.ART_NAME) ?? track.ART_NAME, + COMPOSER: track.SNG_CONTRIBUTORS?.composer, + MAIN_ARTIST: track.SNG_CONTRIBUTORS?.main_artist, + ISRC: track.ISRC, + PRODUCER: opts.album?.PRODUCER_LINE, + COPYRIGHT: opts.album?.COPYRIGHT, + LYRICS: lyricsLrc, + }, + coverPath: albumCoverPath, }) - - const params: string[] = [ - '--remove-all-tags', - `--set-tag=TITLE=${getTrackName(track)}`, - `--set-tag=ALBUM=${track.ALB_TITLE}`, - `--set-tag=DATE=${track.DIGITAL_RELEASE_DATE ?? asNonNull(opts.album?.DIGITAL_RELEASE_DATE)}`, - `--set-tag=DISCNUMBER=${track.DISK_NUMBER}`, - `--set-tag=TRACKNUMBER=${track.TRACK_NUMBER}`, - `--set-tag=COMMENT=ripped from deezer (id: ${track.SNG_ID})`, - `--import-picture-from=${albumCoverPath}`, - ] - - if (track.ARTISTS) { - for (const artist of track.ARTISTS) { - params.push(`--set-tag=ARTIST=${artist.ART_NAME}`) - } - } else { - params.push(`--set-tag=ARTIST=${track.ART_NAME}`) - } - - if (track.SNG_CONTRIBUTORS?.composer) { - for (const composer of track.SNG_CONTRIBUTORS.composer) { - params.push(`--set-tag=COMPOSER=${composer}`) - } - } - - if (track.SNG_CONTRIBUTORS?.main_artist) { - for (const mainArtist of track.SNG_CONTRIBUTORS.main_artist) { - params.push(`--set-tag=MAIN_ARTIST=${mainArtist}`) - } - } - - if (track.ISRC) { - params.push(`--set-tag=ISRC=${track.ISRC}`) - } - - if (opts.album) { - params.push(`--set-tag=PRODUCER=${opts.album.PRODUCER_LINE}`) - params.push(`--set-tag=COPYRIGHT=${opts.album.COPYRIGHT}`) - } - - if (lyricsLrc) { - params.push(`--set-tag=LYRICS=${lyricsLrc}`) - } - - params.push(filename) - - await $`metaflac ${params}` } await rm(albumCoverPath, { force: true }) @@ -595,7 +534,7 @@ async function downloadTrackList(tracks: GwTrack[], opts: { const isMultiDisc = tracks.some(it => it.DISK_NUMBER !== '1') const firstTrackArtistString = getTrackArtistString(tracks[0]) - const isVariousArtists = tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) + const isDifferentArtists = tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) await asyncPool(tracks, async (track) => { let filename = '' @@ -605,7 +544,7 @@ async function downloadTrackList(tracks: GwTrack[], opts: { } filename = `${track.TRACK_NUMBER.padStart(2, '0')}. ` } - if (isVariousArtists) { + if (isDifferentArtists) { filename += `${getTrackArtistString(track)} - ` } filename += `${getTrackName(track)}` @@ -636,7 +575,11 @@ const GwPageArtist = z.object({ }), }) -async function downloadArtist(artistId: string) { +async function downloadArtist(options: { + artistId: string + includeFeaturedAlbums?: boolean +}) { + const { artistId, includeFeaturedAlbums = false } = options const artistInfo = await gwLightApi({ method: 'deezer.pageArtist', token: userData.checkForm, @@ -681,6 +624,8 @@ async function downloadArtist(artistId: string) { }) for (const alb of res.data) { + if (!includeFeaturedAlbums && alb.ROLE_ID === 5) continue + albums.push(alb) trackCount += asNonNull(alb.SONGS).total } @@ -692,7 +637,6 @@ async function downloadArtist(artistId: string) { spinnies.succeed('collect', { text: `collected ${albums.length} albums with a total of ${trackCount} tracks` }) } - // fixme: "featured" albums/tracks (i.e. when main artist of the album is not the one we're dling) should have album artist name in its dirname // fixme: singles should be saved in artist root dir // todo: automatic musicbrainz matching // todo: automatic genius/musixmatch matching for lyrics if unavailable directly from deezer @@ -713,11 +657,16 @@ async function downloadArtist(artistId: string) { assert(tracks.total === asNonNull(alb.SONGS).total) assert(tracks.data.length === asNonNull(alb.SONGS).total) + let folderName = alb.ALB_TITLE + if (alb.ROLE_ID === 5) { + folderName = `${artistInfo.DATA.ART_NAME} - ${folderName}` + } + await downloadTrackList(tracks.data, { destination: join( 'assets/deezer-dl', sanitizeFilename(artistInfo.DATA.ART_NAME), - sanitizeFilename(alb.ALB_TITLE), + sanitizeFilename(folderName), ), album: alb, poolLimit: 4, @@ -827,7 +776,11 @@ async function downloadByUri(uri: string) { } if (type === 'artist') { - await downloadArtist(id) + const includeFeaturedAlbums = await question('include featured albums? (y/N) > ') + await downloadArtist({ + artistId: id, + includeFeaturedAlbums: includeFeaturedAlbums.toLowerCase() === 'y', + }) } } diff --git a/scripts/media/soundcloud-dl.ts b/scripts/media/soundcloud-dl.ts index ac459dc..e931f92 100644 --- a/scripts/media/soundcloud-dl.ts +++ b/scripts/media/soundcloud-dl.ts @@ -10,7 +10,7 @@ import { $, ProcessOutput, question } from 'zx' import { downloadFile, ffetch as ffetchBase } from '../../utils/fetch.ts' import { sanitizeFilename } from '../../utils/fs.ts' import { chunks, getEnv } from '../../utils/misc.ts' -import { generateOpusImageBlob } from '../../utils/opus.ts' +import { generateOpusImageBlob } from '../../utils/media-metadata.ts' const ffetchApi = ffetchBase.extend({ baseUrl: 'https://api-v2.soundcloud.com', diff --git a/scripts/misc/yamusic-token.ts b/scripts/misc/yamusic-token.ts new file mode 100644 index 0000000..d248d26 --- /dev/null +++ b/scripts/misc/yamusic-token.ts @@ -0,0 +1,87 @@ +import { randomBytes } from 'node:crypto' +import { faker } from '@faker-js/faker' +import { question } from 'zx' +import { ffetch } from '../../utils/fetch.ts' + +// log in with your yandex account in the browser, then go to music.yandex.ru and open devtools +// find long ass string in "Cookie" header from the requests to music.yandex.ru, it must contain "Session_id" cookie. +// make sure to copy it completely (on firefox this requires toggling "Raw") +// looks something like: is_gdpr=0; is_gdpr=0; is_gdpr_b=COnCMBCR0wIoAg==; _yasc=ctfv6IPUcb+Lk+jqYr0thW1STKmQC5yB4IJUM5Gn.... +const cookies = await question('music.yandex.ru cookies > ') + +const parsed = new Map(cookies.split('; ').map((cookie) => { + const [name, value] = cookie.split('=') + return [name, value] +})) + +if (!parsed.has('Session_id')) { + throw new Error('Session_id cookie not found') +} + +const deviceId = randomBytes(16).toString('hex') +const uuid = randomBytes(16).toString('hex') +const genRequestId = () => `${uuid}${Math.floor(Date.now())}` +const query = { + manufacturer: 'Google', + model: 'Pixel 9 Pro XL', + app_platform: 'Android 16 (REL)', + am_version_name: '7.46.0(746003972)', + app_id: 'ru.yandex.music', + app_version_name: '2025.09.2 #114gpr', + am_app: 'ru.yandex.music 2025.09.2 #114gpr', + deviceid: deviceId, + device_id: deviceId, + uuid, +} + +const res = await ffetch('https://mobileproxy.passport.yandex.net/1/bundle/oauth/token_by_sessionid', { + query: { + ...query, + request_id: genRequestId(), + }, + form: { + client_id: 'c0ebe342af7d48fbbbfcf2d2eedb8f9e', + client_secret: 'ad0a908f0aa341a182a37ecd75bc319e', + grant_type: 'sessionid', + host: 'yandex.ru', + }, + headers: { + 'Accept': '*/*', + 'User-Agent': 'com.yandex.mobile.auth.sdk/7.46.0.746003972 (Google Pixel 9 Pro XL; Android 16) PassportSDK/7.46.0.746003972', + 'Accept-Language': 'en-RU;q=1, ru-RU;q=0.9', + 'Ya-Client-Host': 'passport.yandex.ru', + 'Ya-Client-Cookie': cookies, + }, +}).json() as any + +if (res.status !== 'ok') { + console.error('Unexpected response:', res) + process.exit(1) +} + +console.log('res', res) + +const res2 = await ffetch('https://mobileproxy.passport.yandex.net/1/token', { + query: { + ...query, + request_id: genRequestId(), + }, + form: { + access_token: res.access_token, + client_id: '23cabbbdc6cd418abb4b39c32c41195d', + client_secret: '53bc75238f0c4d08a118e51fe9203300', + grant_type: 'x-token', + }, +}).json() as any + +if (!res2.access_token) { + console.error('Unexpected response:', res2) + process.exit(1) +} + +console.log('res2', res2) + +console.log('') +console.log('Your auth token is:') +console.log(res2.access_token) +console.log('Expires at:', new Date(Date.now() + res.expires_in * 1000).toLocaleString('ru-RU')) diff --git a/utils/fs.ts b/utils/fs.ts index 11ab17f..b2d9bcc 100644 --- a/utils/fs.ts +++ b/utils/fs.ts @@ -21,3 +21,19 @@ export async function directoryExists(path: string): Promise { export function sanitizeFilename(filename: string) { return filename.replace(/[/\\?%*:|"<>]/g, '_') } + +export async function writeWebStreamToFile(stream: ReadableStream, path: string) { + const fd = await fsp.open(path, 'w+') + const writer = fd.createWriteStream() + + for await (const chunk of stream as any) { + writer.write(chunk) + } + + writer.end() + + await new Promise((resolve, reject) => { + writer.on('error', reject) + writer.on('finish', resolve) + }) +} diff --git a/utils/media-metadata.ts b/utils/media-metadata.ts new file mode 100644 index 0000000..3b6ad13 --- /dev/null +++ b/utils/media-metadata.ts @@ -0,0 +1,102 @@ +import type { ProcessPromise } from 'zx' +import { Readable } from 'node:stream' +import { Bytes, write } from '@fuman/io' +import { $ } from 'zx' + +export async function generateOpusImageBlob(image: Uint8Array) { + // todo we should probably not use ffprobe here but whatever lol + const proc = $`ffprobe -of json -v error -show_entries stream=codec_name,width,height pipe:0` + proc.stdin.write(image) + proc.stdin.end() + const json = await proc.json() + + const img = json.streams[0] + // https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8 + const mime = img.codec_name === 'mjpeg' ? 'image/jpeg' : 'image/png' + const description = 'Cover Artwork' + + const res = Bytes.alloc(image.length + 128) + write.uint32be(res, 3) // picture type = album cover + write.uint32be(res, mime.length) + write.rawString(res, mime) + write.uint32be(res, description.length) + write.rawString(res, description) + write.uint32be(res, img.width) + write.uint32be(res, img.height) + write.uint32be(res, 0) // color depth + write.uint32be(res, 0) // color index (unused, for gifs) + write.uint32be(res, image.length) + write.bytes(res, image) + + return res.result() +} + +export async function runMetaflac(options: { + path: string + tags: Partial + > + coverPath?: string +}) { + const params: string[] = [ + '--remove-all-tags', + ] + + for (const [key, value] of Object.entries(options.tags)) { + if (value == null) continue + if (Array.isArray(value)) { + for (const v of value) { + params.push(`--set-tag=${key}=${v}`) + } + } else { + params.push(`--set-tag=${key}=${value}`) + } + } + + if (options.coverPath) { + params.push(`--import-picture-from=${options.coverPath}`) + } + + params.push(options.path) + + await $`metaflac ${params}` +} + +export function generateFfmpegMetadataFlags(metadata: Partial>) { + const res: string[] = [] + + for (const [key, value] of Object.entries(metadata)) { + if (value == null) continue + if (Array.isArray(value)) { + for (const v of value) { + res.push('-metadata', `${key}=${v}`) + } + } else { + res.push('-metadata', `${key}=${value}`) + } + } + + return res +} + +export async function pipeIntoProc(proc: ProcessPromise, stream: ReadableStream) { + const pipe = Readable.fromWeb(stream as any).pipe(proc.stdin) + await new Promise((resolve, reject) => { + pipe.on('error', reject) + pipe.on('finish', resolve) + }) +} diff --git a/utils/opus.ts b/utils/opus.ts deleted file mode 100644 index 06ff13b..0000000 --- a/utils/opus.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Bytes, write } from '@fuman/io' -import { $ } from 'zx' - -export async function generateOpusImageBlob(image: Uint8Array) { - // todo we should probably not use ffprobe here but whatever lol - const proc = $`ffprobe -of json -v error -show_entries stream=codec_name,width,height pipe:0` - proc.stdin.write(image) - proc.stdin.end() - const json = await proc.json() - - const img = json.streams[0] - // https://www.rfc-editor.org/rfc/rfc9639.html#section-8.8 - const mime = img.codec_name === 'mjpeg' ? 'image/jpeg' : 'image/png' - const description = 'Cover Artwork' - - const res = Bytes.alloc(image.length + 128) - write.uint32be(res, 3) // picture type = album cover - write.uint32be(res, mime.length) - write.rawString(res, mime) - write.uint32be(res, description.length) - write.rawString(res, description) - write.uint32be(res, img.width) - write.uint32be(res, img.height) - write.uint32be(res, 0) // color depth - write.uint32be(res, 0) // color index (unused, for gifs) - write.uint32be(res, image.length) - write.bytes(res, image) - - return res.result() -} From 5fce1ce2750002995ee5b925393b63bfd7007bf2 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sun, 21 Sep 2025 19:26:47 +0000 Subject: [PATCH 55/61] chore: update public repo --- scripts/misc/spotify-albums-weekday-stats.ts | 223 +++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 scripts/misc/spotify-albums-weekday-stats.ts diff --git a/scripts/misc/spotify-albums-weekday-stats.ts b/scripts/misc/spotify-albums-weekday-stats.ts new file mode 100644 index 0000000..6b537f5 --- /dev/null +++ b/scripts/misc/spotify-albums-weekday-stats.ts @@ -0,0 +1,223 @@ +#!/usr/bin/env tsx + +import { ffetch } from '../../utils/fetch.ts' +import { getEnv } from '../../utils/misc.ts' + +// context: had a discussion in a group chat about which day of the week albums are usually released on, needed a way to find out +// the script is mostly vibe-coded but i have no intentions to run it more than once so who cares + +interface SpotifyTrack { + track: { + id: string + name: string + album: { + id: string + name: string + release_date: string + release_date_precision: 'year' | 'month' | 'day' + } + artists: Array<{ + name: string + }> + } +} + +interface SpotifyAlbum { + id: string + name: string + release_date: string + release_date_precision: 'year' | 'month' | 'day' + artists: Array<{ + name: string + }> +} + +interface SpotifyResponse { + items: T[] + next: string | null + total: number +} + +class SpotifyClient { + private accessToken: string + private baseUrl = 'https://api.spotify.com/v1' + + constructor(accessToken: string) { + this.accessToken = accessToken + } + + private async makeRequest(endpoint: string): Promise { + const response = await ffetch(endpoint, { + baseUrl: this.baseUrl, + headers: { + 'Authorization': `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error(`Spotify API error: ${response.status} ${response.statusText}: ${await response.text()}`) + } + + return response.json() + } + + async getLikedTracks(): Promise { + const allTracks: SpotifyTrack[] = [] + let url = '/me/tracks?limit=50' + + while (url) { + const response = await this.makeRequest>(url) + allTracks.push(...response.items) + console.log(`Fetched ${allTracks.length} out of ${response.total} tracks`) + url = response.next ? response.next.replace(this.baseUrl, '') : '' + } + + return allTracks + } + + async getAlbum(albumId: string): Promise { + return this.makeRequest(`/albums/${albumId}`) + } +} + +interface DayStats { + [key: string]: { + count: number + albums: Array<{ + name: string + artist: string + releaseDate: string + }> + } +} + +function getDayOfWeek(dateString: string, precision: string): string { + if (precision === 'year') { + return 'Unknown (Year only)' + } + + if (precision === 'month') { + return 'Unknown (Month only)' + } + + try { + const date = new Date(dateString) + if (Number.isNaN(date.getTime())) { + return 'Unknown (Invalid date)' + } + + const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] + return days[date.getDay()] + } catch (error) { + return 'Unknown (Parse error)' + } +} + +async function main() { + const accessToken = getEnv('SPOTIFY_API_TOKEN') + + if (!accessToken) { + console.error('Error: SPOTIFY_API_TOKEN environment variable is required') + process.exit(1) + } + + console.log('šŸŽµ Fetching your liked tracks from Spotify...') + + const spotify = new SpotifyClient(accessToken) + + try { + const likedTracks = await spotify.getLikedTracks() + console.log(`Found ${likedTracks.length} liked tracks`) + + const processedAlbums = new Set() + const dayStats: DayStats = {} + + // First, count unique albums from tracks + const uniqueAlbumIds = new Set() + for (const track of likedTracks) { + uniqueAlbumIds.add(track.track.album.id) + } + + console.log(`šŸ“Š Analyzing ${uniqueAlbumIds.size} unique album release dates...`) + + let processedCount = 0 + let skippedCount = 0 + + for (const track of likedTracks) { + const albumId = track.track.album.id + + // Skip if we've already processed this album + if (processedAlbums.has(albumId)) { + skippedCount++ + continue + } + + processedAlbums.add(albumId) + processedCount++ + + try { + // Get detailed album info + const album = await spotify.getAlbum(albumId) + const dayOfWeek = getDayOfWeek(album.release_date, album.release_date_precision) + + if (!dayStats[dayOfWeek]) { + dayStats[dayOfWeek] = { + count: 0, + albums: [], + } + } + + dayStats[dayOfWeek].count++ + dayStats[dayOfWeek].albums.push({ + name: album.name, + artist: album.artists.map(a => a.name).join(', '), + releaseDate: album.release_date, + }) + + // Progress reporting + if (processedCount % 10 === 0 || processedCount === uniqueAlbumIds.size) { + console.log(`Progress: ${processedCount}/${uniqueAlbumIds.size} albums processed (${skippedCount} skipped)`) + } + + // Add a small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)) + } catch (error) { + console.warn(`Failed to fetch album info for ${track.track.album.name}: ${error}`) + } + } + + console.log('\nšŸ“ˆ Album Release Day Statistics') + console.log('='.repeat(50)) + + // Sort by count (descending) + const sortedStats = Object.entries(dayStats) + .sort(([,a], [,b]) => b.count - a.count) + + for (const [day, stats] of sortedStats) { + console.log(`\n${day}: ${stats.count} albums`) + console.log('-'.repeat(30)) + + // Show top 5 albums for this day + const topAlbums = stats.albums.slice(0, 5) + for (const album of topAlbums) { + console.log(` • ${album.name} by ${album.artist} (${album.releaseDate})`) + } + + if (stats.albums.length > 5) { + console.log(` ... and ${stats.albums.length - 5} more`) + } + } + + console.log('\nšŸ“Š Summary:') + console.log(`Total unique albums found: ${uniqueAlbumIds.size}`) + console.log(`Total unique albums analyzed: ${processedAlbums.size}`) + console.log(`Albums skipped (duplicates): ${skippedCount}`) + console.log(`Total liked tracks: ${likedTracks.length}`) + } catch (error) { + console.error('Error:', error) + process.exit(1) + } +} + +await main() From 261c7eefa0c677aefa38709ee0f155cc67f75883 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sun, 21 Sep 2025 19:26:47 +0000 Subject: [PATCH 56/61] chore: update public repo --- scripts/misc/spotify-albums-weekday-stats.ts | 223 +++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 scripts/misc/spotify-albums-weekday-stats.ts diff --git a/scripts/misc/spotify-albums-weekday-stats.ts b/scripts/misc/spotify-albums-weekday-stats.ts new file mode 100644 index 0000000..6b537f5 --- /dev/null +++ b/scripts/misc/spotify-albums-weekday-stats.ts @@ -0,0 +1,223 @@ +#!/usr/bin/env tsx + +import { ffetch } from '../../utils/fetch.ts' +import { getEnv } from '../../utils/misc.ts' + +// context: had a discussion in a group chat about which day of the week albums are usually released on, needed a way to find out +// the script is mostly vibe-coded but i have no intentions to run it more than once so who cares + +interface SpotifyTrack { + track: { + id: string + name: string + album: { + id: string + name: string + release_date: string + release_date_precision: 'year' | 'month' | 'day' + } + artists: Array<{ + name: string + }> + } +} + +interface SpotifyAlbum { + id: string + name: string + release_date: string + release_date_precision: 'year' | 'month' | 'day' + artists: Array<{ + name: string + }> +} + +interface SpotifyResponse { + items: T[] + next: string | null + total: number +} + +class SpotifyClient { + private accessToken: string + private baseUrl = 'https://api.spotify.com/v1' + + constructor(accessToken: string) { + this.accessToken = accessToken + } + + private async makeRequest(endpoint: string): Promise { + const response = await ffetch(endpoint, { + baseUrl: this.baseUrl, + headers: { + 'Authorization': `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error(`Spotify API error: ${response.status} ${response.statusText}: ${await response.text()}`) + } + + return response.json() + } + + async getLikedTracks(): Promise { + const allTracks: SpotifyTrack[] = [] + let url = '/me/tracks?limit=50' + + while (url) { + const response = await this.makeRequest>(url) + allTracks.push(...response.items) + console.log(`Fetched ${allTracks.length} out of ${response.total} tracks`) + url = response.next ? response.next.replace(this.baseUrl, '') : '' + } + + return allTracks + } + + async getAlbum(albumId: string): Promise { + return this.makeRequest(`/albums/${albumId}`) + } +} + +interface DayStats { + [key: string]: { + count: number + albums: Array<{ + name: string + artist: string + releaseDate: string + }> + } +} + +function getDayOfWeek(dateString: string, precision: string): string { + if (precision === 'year') { + return 'Unknown (Year only)' + } + + if (precision === 'month') { + return 'Unknown (Month only)' + } + + try { + const date = new Date(dateString) + if (Number.isNaN(date.getTime())) { + return 'Unknown (Invalid date)' + } + + const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] + return days[date.getDay()] + } catch (error) { + return 'Unknown (Parse error)' + } +} + +async function main() { + const accessToken = getEnv('SPOTIFY_API_TOKEN') + + if (!accessToken) { + console.error('Error: SPOTIFY_API_TOKEN environment variable is required') + process.exit(1) + } + + console.log('šŸŽµ Fetching your liked tracks from Spotify...') + + const spotify = new SpotifyClient(accessToken) + + try { + const likedTracks = await spotify.getLikedTracks() + console.log(`Found ${likedTracks.length} liked tracks`) + + const processedAlbums = new Set() + const dayStats: DayStats = {} + + // First, count unique albums from tracks + const uniqueAlbumIds = new Set() + for (const track of likedTracks) { + uniqueAlbumIds.add(track.track.album.id) + } + + console.log(`šŸ“Š Analyzing ${uniqueAlbumIds.size} unique album release dates...`) + + let processedCount = 0 + let skippedCount = 0 + + for (const track of likedTracks) { + const albumId = track.track.album.id + + // Skip if we've already processed this album + if (processedAlbums.has(albumId)) { + skippedCount++ + continue + } + + processedAlbums.add(albumId) + processedCount++ + + try { + // Get detailed album info + const album = await spotify.getAlbum(albumId) + const dayOfWeek = getDayOfWeek(album.release_date, album.release_date_precision) + + if (!dayStats[dayOfWeek]) { + dayStats[dayOfWeek] = { + count: 0, + albums: [], + } + } + + dayStats[dayOfWeek].count++ + dayStats[dayOfWeek].albums.push({ + name: album.name, + artist: album.artists.map(a => a.name).join(', '), + releaseDate: album.release_date, + }) + + // Progress reporting + if (processedCount % 10 === 0 || processedCount === uniqueAlbumIds.size) { + console.log(`Progress: ${processedCount}/${uniqueAlbumIds.size} albums processed (${skippedCount} skipped)`) + } + + // Add a small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)) + } catch (error) { + console.warn(`Failed to fetch album info for ${track.track.album.name}: ${error}`) + } + } + + console.log('\nšŸ“ˆ Album Release Day Statistics') + console.log('='.repeat(50)) + + // Sort by count (descending) + const sortedStats = Object.entries(dayStats) + .sort(([,a], [,b]) => b.count - a.count) + + for (const [day, stats] of sortedStats) { + console.log(`\n${day}: ${stats.count} albums`) + console.log('-'.repeat(30)) + + // Show top 5 albums for this day + const topAlbums = stats.albums.slice(0, 5) + for (const album of topAlbums) { + console.log(` • ${album.name} by ${album.artist} (${album.releaseDate})`) + } + + if (stats.albums.length > 5) { + console.log(` ... and ${stats.albums.length - 5} more`) + } + } + + console.log('\nšŸ“Š Summary:') + console.log(`Total unique albums found: ${uniqueAlbumIds.size}`) + console.log(`Total unique albums analyzed: ${processedAlbums.size}`) + console.log(`Albums skipped (duplicates): ${skippedCount}`) + console.log(`Total liked tracks: ${likedTracks.length}`) + } catch (error) { + console.error('Error:', error) + process.exit(1) + } +} + +await main() From 732192050aad2c05c631ea25a5ca2c2cb8b5b2a7 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 25 Oct 2025 04:16:32 +0000 Subject: [PATCH 57/61] chore: update public repo --- scripts/media/tidal-dl.ts | 375 ++++++++++++++++++++++++++++++++++++++ utils/media-metadata.ts | 25 ++- utils/mpd.ts | 123 +++++++++++++ 3 files changed, 520 insertions(+), 3 deletions(-) create mode 100644 scripts/media/tidal-dl.ts create mode 100644 utils/mpd.ts diff --git a/scripts/media/tidal-dl.ts b/scripts/media/tidal-dl.ts new file mode 100644 index 0000000..69a08e0 --- /dev/null +++ b/scripts/media/tidal-dl.ts @@ -0,0 +1,375 @@ +import { randomUUID } from 'node:crypto' +import { mkdir, rm, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { asyncPool, base64, todo, unknownToError, utf8 } from '@fuman/utils' +import Spinnies from 'spinnies' +import { z } from 'zod' +import { $, question } from 'zx' +import { ffetch as ffetchBase } from '../../utils/fetch.ts' +import { sanitizeFilename } from '../../utils/fs.ts' +import { pipeIntoProc, runMetaflac, writeIntoProc } from '../../utils/media-metadata.ts' +import { getEnv } from '../../utils/misc.ts' +import { concatMpdSegments, parseSimpleMpd } from '../../utils/mpd.ts' +import { createLibcurlFetch } from '../../utils/temkakit/libcurl.ts' + +const oauthResponse = await ffetchBase('https://auth.tidal.com/v1/oauth2/token', { + form: { + client_id: '49YxDN9a2aFV6RTG', + grant_type: 'refresh_token', + scope: 'r_usr w_usr', + refresh_token: getEnv('TIDAL_REFRESH_TOKEN'), + }, +}).parsedJson(z.object({ + access_token: z.string(), + user: z.object({ + username: z.string(), + countryCode: z.string(), + }), +})) + +console.log('Logged in as %s', oauthResponse.user.username) + +const ffetch = ffetchBase.extend({ + headers: { + 'accept': '*/*', + 'Authorization': `Bearer ${oauthResponse.access_token}`, + 'accept-language': 'en-US,en;q=0.5', + 'accept-encoding': 'gzip, deflate, br', + 'referer': 'https://tidal.com/', + 'origin': 'https://tidal.com', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + }, + // for some reason the request sometimes hangs indefinitely, so we need to timeout + timeout: 5000, + retry: { + maxRetries: 10, + // onError: (err, req) => { + // console.log('%s: error: %s', req.url, err) + // return true + // }, + }, +}) + +const PlaybackInfoResult = z.object({ + albumPeakAmplitude: z.number(), + albumReplayGain: z.number(), + assetPresentation: z.string(), + audioMode: z.string(), + audioQuality: z.enum(['HIGH', 'LOSSLESS', 'HI_RES_LOSSLESS']), + bitDepth: z.number(), + manifest: z.string(), + manifestHash: z.string(), + manifestMimeType: z.literal('application/dash+xml'), + sampleRate: z.number(), + streamingSessionId: z.string(), + trackId: z.number(), + trackPeakAmplitude: z.number(), + trackReplayGain: z.number(), +}) + +const streamingSessionId = randomUUID() + +const TidalTrack = z.object({ + id: z.number(), + album: z.object({ + id: z.number(), + cover: z.string(), + }), + artists: z.array(z.object({ + id: z.number(), + name: z.string(), + })), + isrc: z.string().nullable(), + trackNumber: z.number(), + volumeNumber: z.number(), + title: z.string(), + copyright: z.string().nullable(), + version: z.string().nullable(), + bpm: z.number().nullable(), +}) +type TidalTrack = z.infer + +function getTrackName(track: TidalTrack) { + let name = track.title + if (track.version) { + name += ` ${track.version}` + } + return name +} + +function getTrackArtistString(track: TidalTrack | TidalAlbum) { + return track.artists.map(it => it.name).join(', ') +} + +function getAlbumCoverUrl(uuid: string) { + return `https://resources.tidal.com/images/${uuid.replace(/-/g, '/')}/1280x1280.jpg` +} + +const TidalAlbum = z.object({ + id: z.number(), + title: z.string(), + cover: z.string(), + releaseDate: z.string(), + artists: z.array(z.object({ + id: z.number(), + name: z.string(), + })), +}) +type TidalAlbum = z.infer + +const COMMON_QUERY = { + countryCode: oauthResponse.user.countryCode, + locale: 'en_US', + deviceType: 'BROWSER', +} + +async function downloadTrack(options: { + track: TidalTrack + album: TidalAlbum + albumCoverPath?: string + destination: string +}) { + const { track, album, albumCoverPath, destination } = options + const [playbackRes, lyricsRes, creditsRes] = [ + await ffetch(`https://tidal.com/v1/tracks/${track.id}/playbackinfo`, { + query: { + audioquality: 'HI_RES_LOSSLESS', + playbackmode: 'STREAM', + assetpresentation: 'FULL', + }, + headers: { + 'x-tidal-streamingsessionid': streamingSessionId, + 'x-tidal-token': '49YxDN9a2aFV6RTG', + }, + }).parsedJson(PlaybackInfoResult), + await ffetch(`https://tidal.com/v1/tracks/${track.id}/lyrics`, { + query: { + ...COMMON_QUERY, + }, + }).parsedJson(z.object({ + lyrics: z.string(), + // subtitles = timestamped lyrics + subtitles: z.string().nullable(), + })).catch(() => null), + await ffetch(`https://tidal.com/v1/tracks/${track.id}/credits`, { + query: { + limit: 100, + includeContributors: true, + ...COMMON_QUERY, + }, + }).parsedJson(z.array(z.object({ + type: z.string(), + contributors: z.array(z.object({ + id: z.number(), + name: z.string(), + })), + }))), + ] + + const manifest = base64.decode(playbackRes.manifest) + + const ext = playbackRes.audioQuality === 'HIGH' ? 'm4a' : 'flac' + const destFile = `${destination}.${ext}` + + await mkdir(dirname(destFile), { recursive: true }) + + const lyricsLrc = lyricsRes ? lyricsRes.subtitles ?? lyricsRes.lyrics : undefined + const keyedCredits = creditsRes + ? Object.fromEntries(creditsRes.map(it => [it.type, it.contributors.map(it => it.name)])) + : undefined + + const params: string[] = [ + '-y', + '-i', + 'pipe:0', + '-c', + 'copy', + '-loglevel', + 'error', + '-hide_banner', + destFile, + ] + + const proc = $`ffmpeg ${params}` + await pipeIntoProc(proc, concatMpdSegments({ + mpd: parseSimpleMpd(utf8.decoder.decode(manifest)), + fetch: async url => new Uint8Array(await ffetch(url).arrayBuffer()), + })) + await proc + + if (ext === 'flac') { + await runMetaflac({ + path: destFile, + tags: { + TITLE: getTrackName(track), + ALBUM: album.title, + DATE: album.releaseDate, + DISCNUMBER: track.volumeNumber, + TRACKNUMBER: track.trackNumber, + COMMENT: `ripped from tidal (id: ${track.id})`, + ARTIST: track.artists.map(it => it.name), + COPYRIGHT: track.copyright, + LYRICS: lyricsLrc, + REPLAYGAIN_ALBUM_GAIN: playbackRes.albumReplayGain, + REPLAYGAIN_ALBUM_PEAK: playbackRes.albumPeakAmplitude, + REPLAYGAIN_TRACK_GAIN: playbackRes.trackReplayGain, + REPLAYGAIN_TRACK_PEAK: playbackRes.trackPeakAmplitude, + PRODUCER: keyedCredits?.Producer, + COMPOSER: keyedCredits?.Composer, + LYRICIST: keyedCredits?.Lyricist, + PERFORMER: keyedCredits?.['Vocal accompaniment']?.map(it => `${it} (Vocal)`), + ISRC: track.isrc, + BPM: track.bpm, + }, + coverPath: albumCoverPath, + }) + } else { + console.log('warn: m4a tagging not yet implemented') + } +} + +async function fetchAlbumTracks(albumId: number) { + let offset = 0 + const tracks: TidalTrack[] = [] + while (true) { + const res = await ffetch(`https://tidal.com/v1/albums/${albumId}/items`, { query: { + ...COMMON_QUERY, + replace: true, + offset, + limit: 100, + } }).parsedJson(z.object({ + items: z.array(z.object({ + item: TidalTrack, + type: z.literal('track'), + })), + totalNumberOfItems: z.number(), + })) + + for (const item of res.items) { + tracks.push(item.item) + } + if (tracks.length >= res.totalNumberOfItems) break + offset += 100 + } + + return tracks +} + +async function downloadTrackList(opts: { + tracks: TidalTrack[] + albums: Map + albumCoverPaths: Map + destination: string + includeTrackNumber?: boolean + onDownloadStart?: (track: TidalTrack) => void + onDownloadEnd?: (track: TidalTrack, error: Error | null) => void +}) { + await mkdir(opts.destination, { recursive: true }) + + const isMultiDisc = opts.tracks.some(it => it.volumeNumber !== 1) + const firstTrackArtistString = getTrackArtistString(opts.tracks[0]) + const isDifferentArtists = opts.tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) + + await asyncPool(opts.tracks, async (track) => { + let filename = '' + if (opts.includeTrackNumber) { + if (isMultiDisc) { + filename = `${track.volumeNumber}-` + } + filename = `${track.trackNumber.toString().padStart(2, '0')}. ` + } + if (isDifferentArtists) { + filename += `${getTrackArtistString(track)} - ` + } + filename += `${getTrackName(track)}` + + const filenamePath = join(opts.destination, sanitizeFilename(filename)) + + try { + opts.onDownloadStart?.(track) + await downloadTrack({ + track, + album: opts.albums.get(track.album.id)!, + albumCoverPath: opts.albumCoverPaths.get(track.album.id)!, + destination: filenamePath, + }) + opts.onDownloadEnd?.(track, null) + } catch (e) { + opts.onDownloadEnd?.(track, unknownToError(e)) + } + }, { limit: 8 }) +} + +const url = process.argv[2] ?? await question('url or search > ') + +/* eslint-disable no-cond-assign */ + +let m +if ((m = url.match(/\/track\/(\d+)/))) { + const track = await ffetch(`https://tidal.com/v1/tracks/${m[1]}`, { query: COMMON_QUERY }) + .parsedJson(TidalTrack) + const [albumRes, albumCoverRes] = await Promise.all([ + ffetch(`https://tidal.com/v1/albums/${track.album.id}`, { query: COMMON_QUERY }).parsedJson(TidalAlbum), + ffetch(getAlbumCoverUrl(track.album.cover)).arrayBuffer(), + ]) + + const tmpAlbumCoverPath = join(`assets/tidal-${track.album.cover}.jpg`) + await writeFile(tmpAlbumCoverPath, new Uint8Array(albumCoverRes)) + + await downloadTrack({ + track, + album: albumRes, + albumCoverPath: tmpAlbumCoverPath, + destination: join('assets/tidal-dl', sanitizeFilename(`${getTrackArtistString(track)} - ${getTrackName(track)}`)), + }) + + await rm(tmpAlbumCoverPath) +} else if ((m = url.match(/\/album\/(\d+)/))) { + const [albumRes, albumTracks] = await Promise.all([ + ffetch(`https://tidal.com/v1/albums/${m[1]}`, { query: COMMON_QUERY }).parsedJson(TidalAlbum), + fetchAlbumTracks(m[1]), + ]) + + console.log(`downloading album ${albumRes.title} with ${albumTracks.length} tracks`) + + const outDir = join('assets/tidal-dl', `${getTrackArtistString(albumRes)} - ${sanitizeFilename(albumRes.title)}`) + await mkdir(outDir, { recursive: true }) + + const albumCoverRes = await ffetch(getAlbumCoverUrl(albumRes.cover)).arrayBuffer() + await writeFile(join(outDir, 'cover.jpg'), new Uint8Array(albumCoverRes)) + + const spinnies = new Spinnies() + spinnies.add('download', { text: 'downloading album...' }) + + const errors = new Map() + await downloadTrackList({ + tracks: albumTracks, + albums: new Map([[albumRes.id, albumRes]]), + albumCoverPaths: new Map([[albumRes.id, join(outDir, 'cover.jpg')]]), + destination: outDir, + includeTrackNumber: true, + onDownloadStart(track) { + spinnies.add(`${track.id}`, { text: getTrackName(track) }) + }, + onDownloadEnd(track, error) { + spinnies.remove(`${track.id}`) + if (error) { + errors.set(track.id, error) + } + spinnies.remove(`${track.id}`) + }, + }) + + spinnies.succeed('download', { text: 'downloaded album' }) + + if (errors.size) { + console.error('errors:') + for (const [id, error] of errors) { + console.error(` ${id}: ${error.message}`) + } + } +} else { + todo('unsupported url') +} diff --git a/utils/media-metadata.ts b/utils/media-metadata.ts index 3b6ad13..3dab934 100644 --- a/utils/media-metadata.ts +++ b/utils/media-metadata.ts @@ -43,13 +43,19 @@ export async function runMetaflac(options: { | 'TRACKNUMBER' | 'COMMENT' | 'PRODUCER' + | 'LYRICIST' + | 'PERFORMER' | 'COPYRIGHT' | 'ISRC' | 'LYRICS' - | 'MAIN_ARTIST', + | 'MAIN_ARTIST' + | 'REPLAYGAIN_ALBUM_GAIN' + | 'REPLAYGAIN_TRACK_GAIN' + | 'REPLAYGAIN_ALBUM_PEAK' + | 'REPLAYGAIN_TRACK_PEAK' + | 'BPM', string | number | string[] | null - > - > + >> coverPath?: string }) { const params: string[] = [ @@ -100,3 +106,16 @@ export async function pipeIntoProc(proc: ProcessPromise, stream: ReadableStream) pipe.on('finish', resolve) }) } + +export async function writeIntoProc(proc: ProcessPromise, data: Uint8Array) { + return new Promise((resolve, reject) => { + proc.stdin.write(data, (err) => { + if (err) { + reject(err) + } else { + proc.stdin.end() + resolve() + } + }) + }) +} diff --git a/utils/mpd.ts b/utils/mpd.ts new file mode 100644 index 0000000..93b0fe3 --- /dev/null +++ b/utils/mpd.ts @@ -0,0 +1,123 @@ +import { assert, ConditionVariable } from '@fuman/utils' +import { load } from 'cheerio' +import { ffetch } from './fetch.ts' +import { writeWebStreamToFile } from './fs.ts' + +interface SimpleMpd { + codecs: string + initUrl: string + segmentUrls: string[] +} + +export function parseSimpleMpd(xml: string): SimpleMpd { + const $ = load(xml, { xml: true }) + + const period = $('Period') + assert(period.length === 1, 'expected exactly one period') + + const adaptations = period.find('AdaptationSet') + assert(adaptations.length === 1, 'expected exactly one adaptation set') + + const representation = adaptations.find('Representation') + assert(representation.length === 1, 'expected exactly one representation') + + const segmentTemplate = representation.find('SegmentTemplate') + assert(segmentTemplate.length === 1, 'expected exactly one segment template') + + const initUrl = segmentTemplate.attr('initialization') + const templateUrl = segmentTemplate.attr('media') + const startNum = segmentTemplate.attr('startNumber') + + assert(initUrl !== undefined, 'expected initialization url') + assert(templateUrl !== undefined, 'expected template url') + assert(!templateUrl.match(/\$(RepresentationID|Bandwidth|Time)\$/), 'unsupported template url') + assert(startNum !== undefined, 'expected start number') + + const timeline = segmentTemplate.find('SegmentTimeline') + assert(timeline.length === 1, 'expected exactly one segment timeline') + + const segments = timeline.find('S') + assert(segments.length > 0, 'expected at least one segment') + + const segmentUrls: string[] = [] + + let segmentNum = Number(startNum) + for (const segment of segments) { + const duration = $(segment).attr('d') + assert(duration !== undefined, 'expected duration') + const r = $(segment).attr('r') + const repeats = r ? Number.parseInt(r) + 1 : 1 + + for (let i = 0; i < repeats; i++) { + segmentUrls.push(templateUrl.replace('$Number$', String(segmentNum))) + segmentNum++ + } + } + + return { + codecs: representation.attr('codecs')!, + initUrl, + segmentUrls, + } +} + +export function concatMpdSegments(options: { + mpd: SimpleMpd + fetch: (url: string) => Promise + poolSize?: number +}): ReadableStream { + const { mpd, fetch, poolSize = 8 } = options + + let nextSegmentIdx = -1 + let nextWorkerSegmentIdx = -1 + const nextSegmentCv = new ConditionVariable() + const buffer: Record = {} + + const downloadSegment = async (idx = nextWorkerSegmentIdx++) => { + // console.log('downloading segment %s', idx) + const url = idx === -1 ? mpd.initUrl : mpd.segmentUrls[idx] + const chunk = await fetch(url) + buffer[idx] = chunk + + if (idx === nextSegmentIdx) { + nextSegmentCv.notify() + } + + if (nextWorkerSegmentIdx < mpd.segmentUrls.length) { + return downloadSegment() + } + } + + let error: unknown + void Promise.all(Array.from({ + length: Math.min(poolSize, mpd.segmentUrls.length), + }, downloadSegment)) + .catch((e) => { + error = e + nextSegmentCv.notify() + }) + + return new ReadableStream({ + async start(controller) { + while (true) { + await nextSegmentCv.wait() + if (error) { + controller.error(error) + return + } + + while (nextSegmentIdx in buffer) { + const buf = buffer[nextSegmentIdx] + delete buffer[nextSegmentIdx] + nextSegmentIdx++ + controller.enqueue(buf) + } + + if (nextSegmentIdx >= mpd.segmentUrls.length) { + controller.close() + return + } + } + }, + }) +} From 171ba5de7aa685100596b9d7a9134317c32f8faf Mon Sep 17 00:00:00 2001 From: desu-bot Date: Sat, 25 Oct 2025 04:16:32 +0000 Subject: [PATCH 58/61] chore: update public repo --- scripts/media/tidal-dl.ts | 375 ++++++++++++++++++++++++++++++++++++++ utils/media-metadata.ts | 25 ++- utils/mpd.ts | 123 +++++++++++++ 3 files changed, 520 insertions(+), 3 deletions(-) create mode 100644 scripts/media/tidal-dl.ts create mode 100644 utils/mpd.ts diff --git a/scripts/media/tidal-dl.ts b/scripts/media/tidal-dl.ts new file mode 100644 index 0000000..69a08e0 --- /dev/null +++ b/scripts/media/tidal-dl.ts @@ -0,0 +1,375 @@ +import { randomUUID } from 'node:crypto' +import { mkdir, rm, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { asyncPool, base64, todo, unknownToError, utf8 } from '@fuman/utils' +import Spinnies from 'spinnies' +import { z } from 'zod' +import { $, question } from 'zx' +import { ffetch as ffetchBase } from '../../utils/fetch.ts' +import { sanitizeFilename } from '../../utils/fs.ts' +import { pipeIntoProc, runMetaflac, writeIntoProc } from '../../utils/media-metadata.ts' +import { getEnv } from '../../utils/misc.ts' +import { concatMpdSegments, parseSimpleMpd } from '../../utils/mpd.ts' +import { createLibcurlFetch } from '../../utils/temkakit/libcurl.ts' + +const oauthResponse = await ffetchBase('https://auth.tidal.com/v1/oauth2/token', { + form: { + client_id: '49YxDN9a2aFV6RTG', + grant_type: 'refresh_token', + scope: 'r_usr w_usr', + refresh_token: getEnv('TIDAL_REFRESH_TOKEN'), + }, +}).parsedJson(z.object({ + access_token: z.string(), + user: z.object({ + username: z.string(), + countryCode: z.string(), + }), +})) + +console.log('Logged in as %s', oauthResponse.user.username) + +const ffetch = ffetchBase.extend({ + headers: { + 'accept': '*/*', + 'Authorization': `Bearer ${oauthResponse.access_token}`, + 'accept-language': 'en-US,en;q=0.5', + 'accept-encoding': 'gzip, deflate, br', + 'referer': 'https://tidal.com/', + 'origin': 'https://tidal.com', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + }, + // for some reason the request sometimes hangs indefinitely, so we need to timeout + timeout: 5000, + retry: { + maxRetries: 10, + // onError: (err, req) => { + // console.log('%s: error: %s', req.url, err) + // return true + // }, + }, +}) + +const PlaybackInfoResult = z.object({ + albumPeakAmplitude: z.number(), + albumReplayGain: z.number(), + assetPresentation: z.string(), + audioMode: z.string(), + audioQuality: z.enum(['HIGH', 'LOSSLESS', 'HI_RES_LOSSLESS']), + bitDepth: z.number(), + manifest: z.string(), + manifestHash: z.string(), + manifestMimeType: z.literal('application/dash+xml'), + sampleRate: z.number(), + streamingSessionId: z.string(), + trackId: z.number(), + trackPeakAmplitude: z.number(), + trackReplayGain: z.number(), +}) + +const streamingSessionId = randomUUID() + +const TidalTrack = z.object({ + id: z.number(), + album: z.object({ + id: z.number(), + cover: z.string(), + }), + artists: z.array(z.object({ + id: z.number(), + name: z.string(), + })), + isrc: z.string().nullable(), + trackNumber: z.number(), + volumeNumber: z.number(), + title: z.string(), + copyright: z.string().nullable(), + version: z.string().nullable(), + bpm: z.number().nullable(), +}) +type TidalTrack = z.infer + +function getTrackName(track: TidalTrack) { + let name = track.title + if (track.version) { + name += ` ${track.version}` + } + return name +} + +function getTrackArtistString(track: TidalTrack | TidalAlbum) { + return track.artists.map(it => it.name).join(', ') +} + +function getAlbumCoverUrl(uuid: string) { + return `https://resources.tidal.com/images/${uuid.replace(/-/g, '/')}/1280x1280.jpg` +} + +const TidalAlbum = z.object({ + id: z.number(), + title: z.string(), + cover: z.string(), + releaseDate: z.string(), + artists: z.array(z.object({ + id: z.number(), + name: z.string(), + })), +}) +type TidalAlbum = z.infer + +const COMMON_QUERY = { + countryCode: oauthResponse.user.countryCode, + locale: 'en_US', + deviceType: 'BROWSER', +} + +async function downloadTrack(options: { + track: TidalTrack + album: TidalAlbum + albumCoverPath?: string + destination: string +}) { + const { track, album, albumCoverPath, destination } = options + const [playbackRes, lyricsRes, creditsRes] = [ + await ffetch(`https://tidal.com/v1/tracks/${track.id}/playbackinfo`, { + query: { + audioquality: 'HI_RES_LOSSLESS', + playbackmode: 'STREAM', + assetpresentation: 'FULL', + }, + headers: { + 'x-tidal-streamingsessionid': streamingSessionId, + 'x-tidal-token': '49YxDN9a2aFV6RTG', + }, + }).parsedJson(PlaybackInfoResult), + await ffetch(`https://tidal.com/v1/tracks/${track.id}/lyrics`, { + query: { + ...COMMON_QUERY, + }, + }).parsedJson(z.object({ + lyrics: z.string(), + // subtitles = timestamped lyrics + subtitles: z.string().nullable(), + })).catch(() => null), + await ffetch(`https://tidal.com/v1/tracks/${track.id}/credits`, { + query: { + limit: 100, + includeContributors: true, + ...COMMON_QUERY, + }, + }).parsedJson(z.array(z.object({ + type: z.string(), + contributors: z.array(z.object({ + id: z.number(), + name: z.string(), + })), + }))), + ] + + const manifest = base64.decode(playbackRes.manifest) + + const ext = playbackRes.audioQuality === 'HIGH' ? 'm4a' : 'flac' + const destFile = `${destination}.${ext}` + + await mkdir(dirname(destFile), { recursive: true }) + + const lyricsLrc = lyricsRes ? lyricsRes.subtitles ?? lyricsRes.lyrics : undefined + const keyedCredits = creditsRes + ? Object.fromEntries(creditsRes.map(it => [it.type, it.contributors.map(it => it.name)])) + : undefined + + const params: string[] = [ + '-y', + '-i', + 'pipe:0', + '-c', + 'copy', + '-loglevel', + 'error', + '-hide_banner', + destFile, + ] + + const proc = $`ffmpeg ${params}` + await pipeIntoProc(proc, concatMpdSegments({ + mpd: parseSimpleMpd(utf8.decoder.decode(manifest)), + fetch: async url => new Uint8Array(await ffetch(url).arrayBuffer()), + })) + await proc + + if (ext === 'flac') { + await runMetaflac({ + path: destFile, + tags: { + TITLE: getTrackName(track), + ALBUM: album.title, + DATE: album.releaseDate, + DISCNUMBER: track.volumeNumber, + TRACKNUMBER: track.trackNumber, + COMMENT: `ripped from tidal (id: ${track.id})`, + ARTIST: track.artists.map(it => it.name), + COPYRIGHT: track.copyright, + LYRICS: lyricsLrc, + REPLAYGAIN_ALBUM_GAIN: playbackRes.albumReplayGain, + REPLAYGAIN_ALBUM_PEAK: playbackRes.albumPeakAmplitude, + REPLAYGAIN_TRACK_GAIN: playbackRes.trackReplayGain, + REPLAYGAIN_TRACK_PEAK: playbackRes.trackPeakAmplitude, + PRODUCER: keyedCredits?.Producer, + COMPOSER: keyedCredits?.Composer, + LYRICIST: keyedCredits?.Lyricist, + PERFORMER: keyedCredits?.['Vocal accompaniment']?.map(it => `${it} (Vocal)`), + ISRC: track.isrc, + BPM: track.bpm, + }, + coverPath: albumCoverPath, + }) + } else { + console.log('warn: m4a tagging not yet implemented') + } +} + +async function fetchAlbumTracks(albumId: number) { + let offset = 0 + const tracks: TidalTrack[] = [] + while (true) { + const res = await ffetch(`https://tidal.com/v1/albums/${albumId}/items`, { query: { + ...COMMON_QUERY, + replace: true, + offset, + limit: 100, + } }).parsedJson(z.object({ + items: z.array(z.object({ + item: TidalTrack, + type: z.literal('track'), + })), + totalNumberOfItems: z.number(), + })) + + for (const item of res.items) { + tracks.push(item.item) + } + if (tracks.length >= res.totalNumberOfItems) break + offset += 100 + } + + return tracks +} + +async function downloadTrackList(opts: { + tracks: TidalTrack[] + albums: Map + albumCoverPaths: Map + destination: string + includeTrackNumber?: boolean + onDownloadStart?: (track: TidalTrack) => void + onDownloadEnd?: (track: TidalTrack, error: Error | null) => void +}) { + await mkdir(opts.destination, { recursive: true }) + + const isMultiDisc = opts.tracks.some(it => it.volumeNumber !== 1) + const firstTrackArtistString = getTrackArtistString(opts.tracks[0]) + const isDifferentArtists = opts.tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) + + await asyncPool(opts.tracks, async (track) => { + let filename = '' + if (opts.includeTrackNumber) { + if (isMultiDisc) { + filename = `${track.volumeNumber}-` + } + filename = `${track.trackNumber.toString().padStart(2, '0')}. ` + } + if (isDifferentArtists) { + filename += `${getTrackArtistString(track)} - ` + } + filename += `${getTrackName(track)}` + + const filenamePath = join(opts.destination, sanitizeFilename(filename)) + + try { + opts.onDownloadStart?.(track) + await downloadTrack({ + track, + album: opts.albums.get(track.album.id)!, + albumCoverPath: opts.albumCoverPaths.get(track.album.id)!, + destination: filenamePath, + }) + opts.onDownloadEnd?.(track, null) + } catch (e) { + opts.onDownloadEnd?.(track, unknownToError(e)) + } + }, { limit: 8 }) +} + +const url = process.argv[2] ?? await question('url or search > ') + +/* eslint-disable no-cond-assign */ + +let m +if ((m = url.match(/\/track\/(\d+)/))) { + const track = await ffetch(`https://tidal.com/v1/tracks/${m[1]}`, { query: COMMON_QUERY }) + .parsedJson(TidalTrack) + const [albumRes, albumCoverRes] = await Promise.all([ + ffetch(`https://tidal.com/v1/albums/${track.album.id}`, { query: COMMON_QUERY }).parsedJson(TidalAlbum), + ffetch(getAlbumCoverUrl(track.album.cover)).arrayBuffer(), + ]) + + const tmpAlbumCoverPath = join(`assets/tidal-${track.album.cover}.jpg`) + await writeFile(tmpAlbumCoverPath, new Uint8Array(albumCoverRes)) + + await downloadTrack({ + track, + album: albumRes, + albumCoverPath: tmpAlbumCoverPath, + destination: join('assets/tidal-dl', sanitizeFilename(`${getTrackArtistString(track)} - ${getTrackName(track)}`)), + }) + + await rm(tmpAlbumCoverPath) +} else if ((m = url.match(/\/album\/(\d+)/))) { + const [albumRes, albumTracks] = await Promise.all([ + ffetch(`https://tidal.com/v1/albums/${m[1]}`, { query: COMMON_QUERY }).parsedJson(TidalAlbum), + fetchAlbumTracks(m[1]), + ]) + + console.log(`downloading album ${albumRes.title} with ${albumTracks.length} tracks`) + + const outDir = join('assets/tidal-dl', `${getTrackArtistString(albumRes)} - ${sanitizeFilename(albumRes.title)}`) + await mkdir(outDir, { recursive: true }) + + const albumCoverRes = await ffetch(getAlbumCoverUrl(albumRes.cover)).arrayBuffer() + await writeFile(join(outDir, 'cover.jpg'), new Uint8Array(albumCoverRes)) + + const spinnies = new Spinnies() + spinnies.add('download', { text: 'downloading album...' }) + + const errors = new Map() + await downloadTrackList({ + tracks: albumTracks, + albums: new Map([[albumRes.id, albumRes]]), + albumCoverPaths: new Map([[albumRes.id, join(outDir, 'cover.jpg')]]), + destination: outDir, + includeTrackNumber: true, + onDownloadStart(track) { + spinnies.add(`${track.id}`, { text: getTrackName(track) }) + }, + onDownloadEnd(track, error) { + spinnies.remove(`${track.id}`) + if (error) { + errors.set(track.id, error) + } + spinnies.remove(`${track.id}`) + }, + }) + + spinnies.succeed('download', { text: 'downloaded album' }) + + if (errors.size) { + console.error('errors:') + for (const [id, error] of errors) { + console.error(` ${id}: ${error.message}`) + } + } +} else { + todo('unsupported url') +} diff --git a/utils/media-metadata.ts b/utils/media-metadata.ts index 3b6ad13..3dab934 100644 --- a/utils/media-metadata.ts +++ b/utils/media-metadata.ts @@ -43,13 +43,19 @@ export async function runMetaflac(options: { | 'TRACKNUMBER' | 'COMMENT' | 'PRODUCER' + | 'LYRICIST' + | 'PERFORMER' | 'COPYRIGHT' | 'ISRC' | 'LYRICS' - | 'MAIN_ARTIST', + | 'MAIN_ARTIST' + | 'REPLAYGAIN_ALBUM_GAIN' + | 'REPLAYGAIN_TRACK_GAIN' + | 'REPLAYGAIN_ALBUM_PEAK' + | 'REPLAYGAIN_TRACK_PEAK' + | 'BPM', string | number | string[] | null - > - > + >> coverPath?: string }) { const params: string[] = [ @@ -100,3 +106,16 @@ export async function pipeIntoProc(proc: ProcessPromise, stream: ReadableStream) pipe.on('finish', resolve) }) } + +export async function writeIntoProc(proc: ProcessPromise, data: Uint8Array) { + return new Promise((resolve, reject) => { + proc.stdin.write(data, (err) => { + if (err) { + reject(err) + } else { + proc.stdin.end() + resolve() + } + }) + }) +} diff --git a/utils/mpd.ts b/utils/mpd.ts new file mode 100644 index 0000000..93b0fe3 --- /dev/null +++ b/utils/mpd.ts @@ -0,0 +1,123 @@ +import { assert, ConditionVariable } from '@fuman/utils' +import { load } from 'cheerio' +import { ffetch } from './fetch.ts' +import { writeWebStreamToFile } from './fs.ts' + +interface SimpleMpd { + codecs: string + initUrl: string + segmentUrls: string[] +} + +export function parseSimpleMpd(xml: string): SimpleMpd { + const $ = load(xml, { xml: true }) + + const period = $('Period') + assert(period.length === 1, 'expected exactly one period') + + const adaptations = period.find('AdaptationSet') + assert(adaptations.length === 1, 'expected exactly one adaptation set') + + const representation = adaptations.find('Representation') + assert(representation.length === 1, 'expected exactly one representation') + + const segmentTemplate = representation.find('SegmentTemplate') + assert(segmentTemplate.length === 1, 'expected exactly one segment template') + + const initUrl = segmentTemplate.attr('initialization') + const templateUrl = segmentTemplate.attr('media') + const startNum = segmentTemplate.attr('startNumber') + + assert(initUrl !== undefined, 'expected initialization url') + assert(templateUrl !== undefined, 'expected template url') + assert(!templateUrl.match(/\$(RepresentationID|Bandwidth|Time)\$/), 'unsupported template url') + assert(startNum !== undefined, 'expected start number') + + const timeline = segmentTemplate.find('SegmentTimeline') + assert(timeline.length === 1, 'expected exactly one segment timeline') + + const segments = timeline.find('S') + assert(segments.length > 0, 'expected at least one segment') + + const segmentUrls: string[] = [] + + let segmentNum = Number(startNum) + for (const segment of segments) { + const duration = $(segment).attr('d') + assert(duration !== undefined, 'expected duration') + const r = $(segment).attr('r') + const repeats = r ? Number.parseInt(r) + 1 : 1 + + for (let i = 0; i < repeats; i++) { + segmentUrls.push(templateUrl.replace('$Number$', String(segmentNum))) + segmentNum++ + } + } + + return { + codecs: representation.attr('codecs')!, + initUrl, + segmentUrls, + } +} + +export function concatMpdSegments(options: { + mpd: SimpleMpd + fetch: (url: string) => Promise + poolSize?: number +}): ReadableStream { + const { mpd, fetch, poolSize = 8 } = options + + let nextSegmentIdx = -1 + let nextWorkerSegmentIdx = -1 + const nextSegmentCv = new ConditionVariable() + const buffer: Record = {} + + const downloadSegment = async (idx = nextWorkerSegmentIdx++) => { + // console.log('downloading segment %s', idx) + const url = idx === -1 ? mpd.initUrl : mpd.segmentUrls[idx] + const chunk = await fetch(url) + buffer[idx] = chunk + + if (idx === nextSegmentIdx) { + nextSegmentCv.notify() + } + + if (nextWorkerSegmentIdx < mpd.segmentUrls.length) { + return downloadSegment() + } + } + + let error: unknown + void Promise.all(Array.from({ + length: Math.min(poolSize, mpd.segmentUrls.length), + }, downloadSegment)) + .catch((e) => { + error = e + nextSegmentCv.notify() + }) + + return new ReadableStream({ + async start(controller) { + while (true) { + await nextSegmentCv.wait() + if (error) { + controller.error(error) + return + } + + while (nextSegmentIdx in buffer) { + const buf = buffer[nextSegmentIdx] + delete buffer[nextSegmentIdx] + nextSegmentIdx++ + controller.enqueue(buf) + } + + if (nextSegmentIdx >= mpd.segmentUrls.length) { + controller.close() + return + } + } + }, + }) +} From 3be094702691a1e747dc40d33d2d9e5f965774b7 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Thu, 30 Oct 2025 12:03:47 +0000 Subject: [PATCH 59/61] chore: update public repo --- scripts/media/tidal-dl.ts | 216 +++++++++++++++++++++++++++----------- utils/media-metadata.ts | 4 +- 2 files changed, 156 insertions(+), 64 deletions(-) diff --git a/scripts/media/tidal-dl.ts b/scripts/media/tidal-dl.ts index 69a08e0..cae999a 100644 --- a/scripts/media/tidal-dl.ts +++ b/scripts/media/tidal-dl.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto' import { mkdir, rm, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' -import { asyncPool, base64, todo, unknownToError, utf8 } from '@fuman/utils' +import { asyncPool, AsyncQueue, base64, todo, unknownToError, utf8 } from '@fuman/utils' import Spinnies from 'spinnies' import { z } from 'zod' import { $, question } from 'zx' @@ -44,7 +44,7 @@ const ffetch = ffetchBase.extend({ // for some reason the request sometimes hangs indefinitely, so we need to timeout timeout: 5000, retry: { - maxRetries: 10, + maxRetries: 3, // onError: (err, req) => { // console.log('%s: error: %s', req.url, err) // return true @@ -162,7 +162,6 @@ async function downloadTrack(options: { }).parsedJson(z.array(z.object({ type: z.string(), contributors: z.array(z.object({ - id: z.number(), name: z.string(), })), }))), @@ -230,31 +229,46 @@ async function downloadTrack(options: { } } -async function fetchAlbumTracks(albumId: number) { - let offset = 0 - const tracks: TidalTrack[] = [] +async function fetchPaginated(params: { + initialOffset?: number + fetch: (offset: number) => Promise<{ items: T[], hasMore: boolean }> +}): Promise { + let offset = params.initialOffset ?? 0 + const items: T[] = [] while (true) { - const res = await ffetch(`https://tidal.com/v1/albums/${albumId}/items`, { query: { - ...COMMON_QUERY, - replace: true, - offset, - limit: 100, - } }).parsedJson(z.object({ - items: z.array(z.object({ - item: TidalTrack, - type: z.literal('track'), - })), - totalNumberOfItems: z.number(), - })) - + const res = await params.fetch(offset) for (const item of res.items) { - tracks.push(item.item) + items.push(item) } - if (tracks.length >= res.totalNumberOfItems) break - offset += 100 + if (!res.hasMore) break + offset += res.items.length } - return tracks + return items +} + +async function fetchAlbumTracks(albumId: number) { + return fetchPaginated({ + fetch: async (offset) => { + const res = await ffetch(`https://tidal.com/v1/albums/${albumId}/items`, { query: { + ...COMMON_QUERY, + replace: true, + offset, + limit: 100, + } }).parsedJson(z.object({ + items: z.array(z.object({ + item: TidalTrack, + type: z.literal('track'), + })), + totalNumberOfItems: z.number(), + })) + + return { + items: res.items.map(it => it.item), + hasMore: res.totalNumberOfItems > offset + res.items.length, + } + }, + }) } async function downloadTrackList(opts: { @@ -272,7 +286,12 @@ async function downloadTrackList(opts: { const firstTrackArtistString = getTrackArtistString(opts.tracks[0]) const isDifferentArtists = opts.tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) - await asyncPool(opts.tracks, async (track) => { + const retries = new Map() + const queue = new AsyncQueue(opts.tracks) + + let finished = 0 + + await asyncPool(queue, async (track, idx) => { let filename = '' if (opts.includeTrackNumber) { if (isMultiDisc) { @@ -298,10 +317,72 @@ async function downloadTrackList(opts: { opts.onDownloadEnd?.(track, null) } catch (e) { opts.onDownloadEnd?.(track, unknownToError(e)) + + const n = retries.get(track.id) ?? 0 + if (n < 3) { + retries.set(track.id, n + 1) + queue.enqueue(track) + return + } + } + + finished += 1 + + if (finished === opts.tracks.length) { + queue.end() } }, { limit: 8 }) } +async function downloadAlbum(album: TidalAlbum | number) { + const [albumRes, albumTracks] = await Promise.all([ + typeof album === 'number' + ? ffetch(`https://tidal.com/v1/albums/${album}`, { query: COMMON_QUERY }).parsedJson(TidalAlbum) + : Promise.resolve(album), + fetchAlbumTracks(typeof album === 'number' ? album : album.id), + ]) + + console.log(`downloading album ${albumRes.title} with ${albumTracks.length} tracks`) + + const outDir = join('assets/tidal-dl', `${getTrackArtistString(albumRes)} - ${sanitizeFilename(albumRes.title)}`) + await mkdir(outDir, { recursive: true }) + + const albumCoverRes = await ffetch(getAlbumCoverUrl(albumRes.cover)).arrayBuffer() + await writeFile(join(outDir, 'cover.jpg'), new Uint8Array(albumCoverRes)) + + const spinnies = new Spinnies() + spinnies.add('download', { text: 'downloading album...' }) + + const errors = new Map() + await downloadTrackList({ + tracks: albumTracks, + albums: new Map([[albumRes.id, albumRes]]), + albumCoverPaths: new Map([[albumRes.id, join(outDir, 'cover.jpg')]]), + destination: outDir, + includeTrackNumber: true, + onDownloadStart(track) { + spinnies.add(`${track.id}`, { text: getTrackName(track) }) + errors.delete(track.id) + }, + onDownloadEnd(track, error) { + spinnies.remove(`${track.id}`) + if (error) { + errors.set(track.id, error) + } + spinnies.remove(`${track.id}`) + }, + }) + + spinnies.succeed('download', { text: 'downloaded album' }) + + if (errors.size) { + console.error('errors:') + for (const [id, error] of errors) { + console.error(` ${id}: ${error.message}`) + } + } +} + const url = process.argv[2] ?? await question('url or search > ') /* eslint-disable no-cond-assign */ @@ -327,48 +408,57 @@ if ((m = url.match(/\/track\/(\d+)/))) { await rm(tmpAlbumCoverPath) } else if ((m = url.match(/\/album\/(\d+)/))) { - const [albumRes, albumTracks] = await Promise.all([ - ffetch(`https://tidal.com/v1/albums/${m[1]}`, { query: COMMON_QUERY }).parsedJson(TidalAlbum), - fetchAlbumTracks(m[1]), + await downloadAlbum(m[1]) +} else if ((m = url.match(/\/artist\/(\d+)/))) { + const withAppearsOn = (await question('include appears on albums? (y/N) > ')).toLowerCase() === 'y' + + function fetchAlbumList(type: string): Promise { + return fetchPaginated({ + fetch: async (offset) => { + const r = await ffetch(`https://tidal.com/v2/artist/${type}/view-all`, { + query: { + itemId: m[1], + ...COMMON_QUERY, + platform: 'WEB', + limit: 50, + offset, + }, + headers: { + 'x-tidal-client-version': '2025.10.29', + }, + }).parsedJson(z.object({ + items: z.array(z.object({ + type: z.literal('ALBUM'), + data: TidalAlbum, + })), + })) + + return { + items: r.items.map(it => it.data), + hasMore: r.items.length === 50, + } + }, + }) + } + + const [albums, singles, appearsOn] = await Promise.all([ + fetchAlbumList('ARTIST_ALBUMS'), + fetchAlbumList('ARTIST_TOP_SINGLES'), + withAppearsOn ? fetchAlbumList('ARTIST_APPEARS_ON') : Promise.resolve([]), ]) - console.log(`downloading album ${albumRes.title} with ${albumTracks.length} tracks`) + // concat and dedupe + const seenIds = new Set() + const allAlbums: TidalAlbum[] = [] + for (const album of [...albums, ...singles, ...appearsOn]) { + if (seenIds.has(album.id)) continue + seenIds.add(album.id) + allAlbums.push(album) + } + console.log('found %d albums', allAlbums.length) - const outDir = join('assets/tidal-dl', `${getTrackArtistString(albumRes)} - ${sanitizeFilename(albumRes.title)}`) - await mkdir(outDir, { recursive: true }) - - const albumCoverRes = await ffetch(getAlbumCoverUrl(albumRes.cover)).arrayBuffer() - await writeFile(join(outDir, 'cover.jpg'), new Uint8Array(albumCoverRes)) - - const spinnies = new Spinnies() - spinnies.add('download', { text: 'downloading album...' }) - - const errors = new Map() - await downloadTrackList({ - tracks: albumTracks, - albums: new Map([[albumRes.id, albumRes]]), - albumCoverPaths: new Map([[albumRes.id, join(outDir, 'cover.jpg')]]), - destination: outDir, - includeTrackNumber: true, - onDownloadStart(track) { - spinnies.add(`${track.id}`, { text: getTrackName(track) }) - }, - onDownloadEnd(track, error) { - spinnies.remove(`${track.id}`) - if (error) { - errors.set(track.id, error) - } - spinnies.remove(`${track.id}`) - }, - }) - - spinnies.succeed('download', { text: 'downloaded album' }) - - if (errors.size) { - console.error('errors:') - for (const [id, error] of errors) { - console.error(` ${id}: ${error.message}`) - } + for (const album of allAlbums) { + await downloadAlbum(album) } } else { todo('unsupported url') diff --git a/utils/media-metadata.ts b/utils/media-metadata.ts index 3dab934..be5f195 100644 --- a/utils/media-metadata.ts +++ b/utils/media-metadata.ts @@ -100,8 +100,10 @@ export function generateFfmpegMetadataFlags(metadata: Partial((resolve, reject) => { + nodeStream.on('error', reject) + const pipe = nodeStream.pipe(proc.stdin) pipe.on('error', reject) pipe.on('finish', resolve) }) From da3ca482443afec12b7e00cc9b58019532f57bb4 Mon Sep 17 00:00:00 2001 From: desu-bot Date: Thu, 30 Oct 2025 12:03:47 +0000 Subject: [PATCH 60/61] chore: update public repo --- scripts/media/tidal-dl.ts | 216 +++++++++++++++++++++++++++----------- utils/media-metadata.ts | 4 +- 2 files changed, 156 insertions(+), 64 deletions(-) diff --git a/scripts/media/tidal-dl.ts b/scripts/media/tidal-dl.ts index 69a08e0..cae999a 100644 --- a/scripts/media/tidal-dl.ts +++ b/scripts/media/tidal-dl.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto' import { mkdir, rm, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' -import { asyncPool, base64, todo, unknownToError, utf8 } from '@fuman/utils' +import { asyncPool, AsyncQueue, base64, todo, unknownToError, utf8 } from '@fuman/utils' import Spinnies from 'spinnies' import { z } from 'zod' import { $, question } from 'zx' @@ -44,7 +44,7 @@ const ffetch = ffetchBase.extend({ // for some reason the request sometimes hangs indefinitely, so we need to timeout timeout: 5000, retry: { - maxRetries: 10, + maxRetries: 3, // onError: (err, req) => { // console.log('%s: error: %s', req.url, err) // return true @@ -162,7 +162,6 @@ async function downloadTrack(options: { }).parsedJson(z.array(z.object({ type: z.string(), contributors: z.array(z.object({ - id: z.number(), name: z.string(), })), }))), @@ -230,31 +229,46 @@ async function downloadTrack(options: { } } -async function fetchAlbumTracks(albumId: number) { - let offset = 0 - const tracks: TidalTrack[] = [] +async function fetchPaginated(params: { + initialOffset?: number + fetch: (offset: number) => Promise<{ items: T[], hasMore: boolean }> +}): Promise { + let offset = params.initialOffset ?? 0 + const items: T[] = [] while (true) { - const res = await ffetch(`https://tidal.com/v1/albums/${albumId}/items`, { query: { - ...COMMON_QUERY, - replace: true, - offset, - limit: 100, - } }).parsedJson(z.object({ - items: z.array(z.object({ - item: TidalTrack, - type: z.literal('track'), - })), - totalNumberOfItems: z.number(), - })) - + const res = await params.fetch(offset) for (const item of res.items) { - tracks.push(item.item) + items.push(item) } - if (tracks.length >= res.totalNumberOfItems) break - offset += 100 + if (!res.hasMore) break + offset += res.items.length } - return tracks + return items +} + +async function fetchAlbumTracks(albumId: number) { + return fetchPaginated({ + fetch: async (offset) => { + const res = await ffetch(`https://tidal.com/v1/albums/${albumId}/items`, { query: { + ...COMMON_QUERY, + replace: true, + offset, + limit: 100, + } }).parsedJson(z.object({ + items: z.array(z.object({ + item: TidalTrack, + type: z.literal('track'), + })), + totalNumberOfItems: z.number(), + })) + + return { + items: res.items.map(it => it.item), + hasMore: res.totalNumberOfItems > offset + res.items.length, + } + }, + }) } async function downloadTrackList(opts: { @@ -272,7 +286,12 @@ async function downloadTrackList(opts: { const firstTrackArtistString = getTrackArtistString(opts.tracks[0]) const isDifferentArtists = opts.tracks.some(it => getTrackArtistString(it) !== firstTrackArtistString) - await asyncPool(opts.tracks, async (track) => { + const retries = new Map() + const queue = new AsyncQueue(opts.tracks) + + let finished = 0 + + await asyncPool(queue, async (track, idx) => { let filename = '' if (opts.includeTrackNumber) { if (isMultiDisc) { @@ -298,10 +317,72 @@ async function downloadTrackList(opts: { opts.onDownloadEnd?.(track, null) } catch (e) { opts.onDownloadEnd?.(track, unknownToError(e)) + + const n = retries.get(track.id) ?? 0 + if (n < 3) { + retries.set(track.id, n + 1) + queue.enqueue(track) + return + } + } + + finished += 1 + + if (finished === opts.tracks.length) { + queue.end() } }, { limit: 8 }) } +async function downloadAlbum(album: TidalAlbum | number) { + const [albumRes, albumTracks] = await Promise.all([ + typeof album === 'number' + ? ffetch(`https://tidal.com/v1/albums/${album}`, { query: COMMON_QUERY }).parsedJson(TidalAlbum) + : Promise.resolve(album), + fetchAlbumTracks(typeof album === 'number' ? album : album.id), + ]) + + console.log(`downloading album ${albumRes.title} with ${albumTracks.length} tracks`) + + const outDir = join('assets/tidal-dl', `${getTrackArtistString(albumRes)} - ${sanitizeFilename(albumRes.title)}`) + await mkdir(outDir, { recursive: true }) + + const albumCoverRes = await ffetch(getAlbumCoverUrl(albumRes.cover)).arrayBuffer() + await writeFile(join(outDir, 'cover.jpg'), new Uint8Array(albumCoverRes)) + + const spinnies = new Spinnies() + spinnies.add('download', { text: 'downloading album...' }) + + const errors = new Map() + await downloadTrackList({ + tracks: albumTracks, + albums: new Map([[albumRes.id, albumRes]]), + albumCoverPaths: new Map([[albumRes.id, join(outDir, 'cover.jpg')]]), + destination: outDir, + includeTrackNumber: true, + onDownloadStart(track) { + spinnies.add(`${track.id}`, { text: getTrackName(track) }) + errors.delete(track.id) + }, + onDownloadEnd(track, error) { + spinnies.remove(`${track.id}`) + if (error) { + errors.set(track.id, error) + } + spinnies.remove(`${track.id}`) + }, + }) + + spinnies.succeed('download', { text: 'downloaded album' }) + + if (errors.size) { + console.error('errors:') + for (const [id, error] of errors) { + console.error(` ${id}: ${error.message}`) + } + } +} + const url = process.argv[2] ?? await question('url or search > ') /* eslint-disable no-cond-assign */ @@ -327,48 +408,57 @@ if ((m = url.match(/\/track\/(\d+)/))) { await rm(tmpAlbumCoverPath) } else if ((m = url.match(/\/album\/(\d+)/))) { - const [albumRes, albumTracks] = await Promise.all([ - ffetch(`https://tidal.com/v1/albums/${m[1]}`, { query: COMMON_QUERY }).parsedJson(TidalAlbum), - fetchAlbumTracks(m[1]), + await downloadAlbum(m[1]) +} else if ((m = url.match(/\/artist\/(\d+)/))) { + const withAppearsOn = (await question('include appears on albums? (y/N) > ')).toLowerCase() === 'y' + + function fetchAlbumList(type: string): Promise { + return fetchPaginated({ + fetch: async (offset) => { + const r = await ffetch(`https://tidal.com/v2/artist/${type}/view-all`, { + query: { + itemId: m[1], + ...COMMON_QUERY, + platform: 'WEB', + limit: 50, + offset, + }, + headers: { + 'x-tidal-client-version': '2025.10.29', + }, + }).parsedJson(z.object({ + items: z.array(z.object({ + type: z.literal('ALBUM'), + data: TidalAlbum, + })), + })) + + return { + items: r.items.map(it => it.data), + hasMore: r.items.length === 50, + } + }, + }) + } + + const [albums, singles, appearsOn] = await Promise.all([ + fetchAlbumList('ARTIST_ALBUMS'), + fetchAlbumList('ARTIST_TOP_SINGLES'), + withAppearsOn ? fetchAlbumList('ARTIST_APPEARS_ON') : Promise.resolve([]), ]) - console.log(`downloading album ${albumRes.title} with ${albumTracks.length} tracks`) + // concat and dedupe + const seenIds = new Set() + const allAlbums: TidalAlbum[] = [] + for (const album of [...albums, ...singles, ...appearsOn]) { + if (seenIds.has(album.id)) continue + seenIds.add(album.id) + allAlbums.push(album) + } + console.log('found %d albums', allAlbums.length) - const outDir = join('assets/tidal-dl', `${getTrackArtistString(albumRes)} - ${sanitizeFilename(albumRes.title)}`) - await mkdir(outDir, { recursive: true }) - - const albumCoverRes = await ffetch(getAlbumCoverUrl(albumRes.cover)).arrayBuffer() - await writeFile(join(outDir, 'cover.jpg'), new Uint8Array(albumCoverRes)) - - const spinnies = new Spinnies() - spinnies.add('download', { text: 'downloading album...' }) - - const errors = new Map() - await downloadTrackList({ - tracks: albumTracks, - albums: new Map([[albumRes.id, albumRes]]), - albumCoverPaths: new Map([[albumRes.id, join(outDir, 'cover.jpg')]]), - destination: outDir, - includeTrackNumber: true, - onDownloadStart(track) { - spinnies.add(`${track.id}`, { text: getTrackName(track) }) - }, - onDownloadEnd(track, error) { - spinnies.remove(`${track.id}`) - if (error) { - errors.set(track.id, error) - } - spinnies.remove(`${track.id}`) - }, - }) - - spinnies.succeed('download', { text: 'downloaded album' }) - - if (errors.size) { - console.error('errors:') - for (const [id, error] of errors) { - console.error(` ${id}: ${error.message}`) - } + for (const album of allAlbums) { + await downloadAlbum(album) } } else { todo('unsupported url') diff --git a/utils/media-metadata.ts b/utils/media-metadata.ts index 3dab934..be5f195 100644 --- a/utils/media-metadata.ts +++ b/utils/media-metadata.ts @@ -100,8 +100,10 @@ export function generateFfmpegMetadataFlags(metadata: Partial((resolve, reject) => { + nodeStream.on('error', reject) + const pipe = nodeStream.pipe(proc.stdin) pipe.on('error', reject) pipe.on('finish', resolve) }) From ef375d1188a6101345d3f0638b32548aef0c584d Mon Sep 17 00:00:00 2001 From: desu-bot Date: Wed, 17 Dec 2025 13:39:15 +0000 Subject: [PATCH 61/61] chore: update public repo