commit 0dbb36be9dc0abc997de49323af0267988ffe1aa Author: 史悦 Date: Wed Jan 7 16:46:09 2026 +0800 初始化提交 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f93dd78 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +npm-debug.log +Dockerfile* +docker-compose* +.dockerignore +.git +.github +.gitignore +README.md +LICENSE +dist +imgs + +backend/.vscode +backend/.data +backend/.profile/ +backend/foamzou +backend/node_modules +backend/bin/* + +frontend/node_modules +frontend/.vscode + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c93dde0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,139 @@ +# repo file +/backend/.profile +/backend/.data +/backend/bin/media-get* +/backend/public + +.vscode + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..64e7793 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# stage: build frontend +FROM surnet/alpine-node-opencv:16.13.0-4.5.1 AS FRONTEND_BUILD + +WORKDIR /app +RUN apk add --no-cache git && \ + npm install -g pnpm@7.8.0 + +ENV NODE_OPTIONS="--max-old-space-size=4096" + +COPY frontend frontend/ +WORKDIR /app/frontend +RUN pnpm install --verbose && \ + pnpm run build + +# stage: build backend +FROM surnet/alpine-node-opencv:16.13.0-4.5.1 AS BACKEND_BUILD + +WORKDIR /app +RUN apk add --no-cache git && \ + npm install -g pnpm@7.8.0 + +ENV CROSS_COMPILING=1 +ENV NODE_OPTIONS="--max-old-space-size=4096" + +COPY backend backend/ +COPY scripts scripts/ +COPY package.json ./ + +WORKDIR /app/backend +RUN pnpm install --production --verbose + +# Download media-get +RUN mkdir -p /app/backend/bin && \ + node /app/scripts/setup-for-build-docker.js + +# stage: final +FROM iamccc/alpine-node:16.20 + +WORKDIR /app + +# Install FFmpeg +COPY --from=pldin601/static-ffmpeg:22.04.061404-87ac0d7 /ffmpeg /ffprobe /usr/local/bin/ +RUN chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe + +ENV PATH="/usr/local/bin:${PATH}" +ENV NODE_ENV=production + +# Copy backend with media-get +COPY --from=BACKEND_BUILD /app/backend ./backend +COPY --from=FRONTEND_BUILD /app/frontend/dist ./backend/public + +# Ensure media-get is executable +RUN chmod +x /app/backend/bin/media-get + +EXPOSE 5566 + +CMD ["node", "backend/src/index.js"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cfcc004 --- /dev/null +++ b/README.md @@ -0,0 +1,187 @@ +# Melody + +## 项目介绍 + + + +大家好,我叫 Melody,你的音乐精灵,旨在帮助你更好地管理音乐。目前的主要能力是帮助你将喜欢的歌曲或者音频上传到音乐平台的云盘。 + +## 免责声明 +- 本项目所搜索的歌曲均来源于音乐平台的免费公开资源。请确保在使用本项目时遵守相关音乐平台的服务条款和版权规定。 +- 本项目仅供技术学习和交流使用,使用者仅限于个人学习,不得用于任何商业目的。使用者应自行承担因使用本项目而可能产生的法律责任。 + +为了避免不必要的纠纷和账号安全问题,本项目不会以任何形式提供在线 demo 服务 + + +## Feature + +- 支持在各大音乐和视频网站检索歌曲。目前支持 咪咕、网易云、QQ 音乐、酷狗、bilibili、抖音等站点。详情可以在我的 [media-get](https://github.com/foamzou/media-get#%E6%94%AF%E6%8C%81%E7%9A%84%E7%BD%91%E7%AB%99) 项目中查看 +- 支持一键下载到本地,一键上传到云盘 +- 支持定时上传网易云歌单歌曲到网易云云盘 +- 支持定时同步网易云歌单歌曲到本地 +- 用链接搜索歌曲(例如使用 b站或抖音的视频链接进行搜索,可以将对应的音频自动上传到音乐云盘) +- 一键“解锁”无法播放的歌曲(一键检测变灰的歌曲,自动从公共资源搜索最佳资源,自动上传到云盘,自动匹配歌曲信息。代替繁琐的人工操作,实现可播放)(实验性功能,目前仅支持网易云) +- PC 端、移动端适配良好(支持 PWA) +- 部署简单,支持 docker + +## 安装和启动 + +### 方式一:Docker 安装 + +1. 在你的宿主机创建一个目录,例如: `~/melody-profile` +2. 创建镜像,有两种方式选择(注意修改下面的宿主机目录为你实际的): + - 从 hub.docker.com 拉取 + ``` + docker run -d -p 5566:5566 -v ~/melody-profile:/app/backend/.profile -v /你宿主机的路径:/app/melody-data foamzou/melody:latest + ``` + - 从代码编译镜像(若你的 docker 不支持 DOCKER_BUILDKIT,则去掉) + ``` + DOCKER_BUILDKIT=1 docker build -t melody . + docker run -d -p 5566:5566 -v ~/melody-profile:/app/backend/.profile -v /你宿主机的路径:/app/melody-data melody + ``` +3. 后续更新(以从 hub.docker.com 更新为例) + ``` + docker pull docker.io/foamzou/melody:latest + docker kill + docker run -d -p 5566:5566 -v ~/melody-profile:/app/backend/.profile -v /你宿主机的路径:/app/melody-data foamzou/melody:latest + ``` + +### 方式二:源码安装 + +1. 依赖 + + 确保以下两个依赖是安装好的 + + 1. node.js ([官网下载](https://nodejs.org/zh-cn/download/)) + 2. FFmpeg ([windows 安装介绍](https://zhuanlan.zhihu.com/p/400952501)) + +2. 下载源码、初始化服务、运行服务 + + ``` + git clone https://github.com/foamzou/melody.git + cd melody && npm run init && npm run app + ``` + +3. 若后面代码有更新,下次执行该命令即可更新 + ``` + npm run update && npm run app + ``` + +### 方式三:通过第三方部署 +
+通过宝塔面板一键部署 + +#### 前提 + +* 仅适用于宝塔面板 9.2.0 及以上版本 +* 安装宝塔面板,前往[宝塔面板](https://www.bt.cn/new/download.html)官网,选择正式版的脚本下载安装 + +#### 部署 + +1. 登录宝塔面板,在左侧菜单栏中点击 `Docker` +2. 首次会提示安装`Docker`和`Docker Compose`服务,点击立即安装,若已安装请忽略。 +3. 安装完成后在`Docker-应用商店-实用工具`中找到 `Melody`,点击`安装`,也可以在搜索框直接搜索`melody`。 +4. 设置域名等基本信息,点击`确定` +* 说明: + * 名称:应用名称,默认`melody_随机字符` + * 版本选择:默认`latest` + * 域名:如您需要通过域名访问,请在此处填写您的域名 + * 允许外部访问:如您需通过`IP+Port`直接访问,请勾选,如您已经设置了域名,请不要勾选此处 + * 端口:默认`5568`,可自行修改 + * CPU 限制:0 为不限制,根据实际需要设置 + * 内存限制:0 为不限制,根据实际需要设置 +5. 提交后面板会自动进行应用初始化,大概需要`1-3`分钟,初始化完成后即可访问。 +
+ +### 配置你的账号(可选) + +默认的 melody key 为: `melody`,若你的服务部署在私有网络,则可以不用修改(网易云账号、密码可以在 web 页面设置)。 + +若需要修改或添加新账号,则编辑 `backend/.profile/accounts.json` (安装方式为 docker 的则为:`你的宿主机目录/accounts.json` ) 。 + +1. 该 JSON 中的 key 是 `Melody Key`,是你在网页访问该服务的唯一凭证 +2. 网易云账号信息: `account` 和 `password` 可以后续在网页修改 +3. 该 JSON 是个数组,支持配置多个账号 + +Q: 更新了 accounts.json 如何生效? + +A: 两种方式。1: 重启服务。2: 网页端 `我的音乐账号` tab 下,随便修改点账号,密码,然后点击 `更新账号密码`,这样会从 accounts.json 更新信息到内存(我后面优化下这块) + +### 浏览器访问 + +最后,在浏览器访问 http://127.0.0.1:5566 就可以使用啦~ + +## 功能介绍 + +### 关键词搜索歌曲 + +如果试听后是你想要的,点击上传按钮会将该歌曲上传到你的网易云音乐云盘 + + +### 链接搜索 + +有时候我们在 b 站 听到好听的歌,也可以上传到云盘 + + +### 一键解锁歌单 + +点击 `解锁全部`(实验性功能) 后,服务会自动匹配每首歌,并把歌曲上传到云盘,最后做个 match,以保证你还能看到歌词、评论 + + +### 手动搜索匹配 + +当某首歌自动解锁失败后,还可以手动点击搜索按钮,找到符合的歌曲后,手动点击上传按钮 + + +### 移动端适配 +
+ + +
+
+ + +
+ +## Roadmap + +计划在后面支持以下功能 + +- [x] 页面适配移动端 +- [ ] 浏览器油猴脚本 +- [ ] 云盘歌曲 match 手动纠错 +- [ ] 支持播放列表 +- [x] 支持播放云盘的歌曲 +- [x] 支持 docker 部署 +- [ ] 支持 youtube-dl,you-dl 等工具作为输入源 +- [ ] 支持 酷狗、qq 音乐等音乐平台的云盘作为输出 +- [ ] 偏好设置 +- [ ] 版本更新提示 + +## Q & A +1. Q:移动端版本,为什么点击下载歌曲,会跳新的页面? + + A:有的浏览器不支持嗅探的,会有这个问题。因为外部资源文件都不允许跨域,无法用常规下载方式 save as。考虑后续 hack +2. Q:移动端版本,为什么在数据网络无法播放歌曲? + + A:发现某些网络下,没有触发 `canplaythrough` 事件,wifi 环境下一般是没有问题的。 +3. Q:为什么移动端 PWA,点击跳转到其他页面时,无法返回到原来页面? + + A:PWA 在移动端不支持使用外部浏览器打开外链,只能在应用内打开,因此会有各种奇怪问题。此时,只能先杀死应用。 + +4. Q:为什么我部署的服务,PWA 始终出不了? + + A:PWA 要求服务必须是 HTTPS。 + +5. Q: 为什么更新 media-get 组件后,搜索报错 + + A: 目前存在 bug,更新完 media-get 组件之后,请务必重启 docker 容器或服务,否则将无法继续使用 + +## Change log +见 [Release](https://github.com/foamzou/melody/releases) + +## 致谢 + +- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 的网易云 API +- [MakeGirlsMoe](https://make.girls.moe/) 生成的 Melody 虚拟形象图片 +- [Media Get](https://github.com/foamzou/media-get) 我的开源项目 diff --git a/api.md b/api.md new file mode 100644 index 0000000..281108a --- /dev/null +++ b/api.md @@ -0,0 +1,226 @@ +# TuneHub API 文档 + +TuneHub 是一个统一的音乐信息解析服务。它打破了不同音乐平台之间的壁垒,提供了一套标准化的 API 接口。 + +**Base URL:** `https://music-dl.sayqz.com` +**Version:** 1.0.0 + +--- + +## 核心 API + +### 1. 获取歌曲基本信息 + +获取歌曲的名称、歌手、专辑等基本元数据信息。 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `source`: `string` - **必需**. 平台标识 (例如: `netease`, `kuwo`, `qq`). + * `id`: `string` - **必需**. 歌曲 ID. + * `type`: `string` - **必需**. 固定为 `info`. +* **Response Example (200 OK):** + ```json + { + "code": 200, + "message": "success", + "data": { + "name": "歌曲名称", + "artist": "歌手名称", + "album": "专辑名称", + "url": "https://music-dl.sayqz.com/api/?source=netease&id=123456&type=url", + "pic": "https://music-dl.sayqz.com/api/?source=netease&id=123456&type=pic", + "lrc": "https://music-dl.sayqz.com/api/?source=netease&id=123456&type=lrc" + }, + "timestamp": "2025-11-23T12:00:00.000+08:00" + } + ``` + +### 2. 获取音乐文件链接 + +获取音乐文件链接。成功时返回 302 Redirect 到实际的音乐文件 URL。 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `source`: `string` - **必需**. 平台标识. + * `id`: `string` - **必需**. 歌曲 ID. + * `type`: `string` - **必需**. 固定为 `url`. + * `br`: `string` - *可选*. 音质 (默认: `320k`). 可选值: `128k`, `320k`, `flac`, `flac24bit`. + +### 3. 获取专辑封面 + +获取歌曲的专辑封面图片。成功时返回 302 Redirect 到图片 URL。 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `source`: `string` - **必需**. 平台标识. + * `id`: `string` - **必需**. 歌曲 ID. + * `type`: `string` - **必需**. 固定为 `pic`. + +### 4. 获取歌词 + +获取LRC格式的歌词。 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `source`: `string` - **必需**. 平台标识. + * `id`: `string` - **必需**. 歌曲 ID. + * `type`: `string` - **必需**. 固定为 `lrc`. +* **Response Example (200 OK, text/plain):** + ``` + [00:00.00]歌词第一行 + [00:05.50]歌词第二行 + ``` + +### 5. 搜索歌曲 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `source`: `string` - **必需**. 平台标识. + * `type`: `string` - **必需**. 固定为 `search`. + * `keyword`: `string` - **必需**. 搜索关键词. + * `limit`: `integer` - *可选*. 返回数量 (默认: 20). +* **Response Example (200 OK):** + ```json + { + "code": 200, + "message": "success", + "data": { + "keyword": "周杰伦", + "total": 10, + "results": [ + { + "id": "123456", + "name": "歌曲名称", + "artist": "周杰伦", + "album": "专辑名称", + "url": "https://music-dl.sayqz.com/api/?...", + "platform": "netease" + } + ] + } + } + ``` + +### 6. 聚合搜索 + +一次性并发请求所有启用的平台,并对结果进行智能混合排列。 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `type`: `string` - **必需**. 固定为 `aggregateSearch`. + * `keyword`: `string` - **必需**. 搜索关键词. +* **Response Example (200 OK):** + ```json + { + "code": 200, + "message": "success", + "data": { + "keyword": "周杰伦", + "results": [ + { + "id": "123456", + "name": "歌曲名称", + "artist": "周杰伦", + "platform": "netease" + }, + { + "id": "789012", + "name": "另一首歌", + "artist": "周杰伦", + "platform": "kuwo" + } + ] + } + } + ``` + +### 7. 获取歌单详情 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `source`: `string` - **必需**. 平台标识. + * `id`: `string` - **必需**. 歌单 ID. + * `type`: `string` - **必需**. 固定为 `playlist`. + +### 8. 获取排行榜列表 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `source`: `string` - **必需**. 平台标识. + * `type`: `string` - **必需**. 固定为 `toplists`. + +### 9. 获取排行榜歌曲 + +* **Method:** `GET` +* **Endpoint:** `/api/` +* **Query Parameters:** + * `source`: `string` - **必需**. 平台标识. + * `id`: `string` - **必需**. 排行榜 ID. + * `type`: `string` - **必需**. 固定为 `toplist`. + +--- + +## 系统 API + +### 10. 系统状态 + +* **Method:** `GET` +* **Endpoint:** `/status` + +### 11. 健康检查 + +* **Method:** `GET` +* **Endpoint:** `/health` + +--- + +## 统计 API + +### 12. 获取统计数据 + +* **Method:** `GET` +* **Endpoint:** `/stats` +* **Query Parameters:** + * `period`: `string` - *可选*. 时间段 (默认: `today`). + * `groupBy`: `string` - *可选*. 分组依据 (默认: `platform`). + +### 13. 获取统计摘要 + +* **Method:** `GET` +* **Endpoint:** `/stats/summary` + +### 14. 平台统计概览 + +* **Method:** `GET` +* **Endpoint:** `/stats/platforms` +* **Query Parameters:** + * `period`: `string` - *可选*. 时间段 (默认: `today`). + +### 15. QPS 统计 + +* **Method:** `GET` +* **Endpoint:** `/stats/qps` +* **Query Parameters:** + * `period`: `string` - *可选*. 时间段 (默认: `today`). + +### 16. 趋势数据 + +* **Method:** `GET` +* **Endpoint:** `/stats/trends` +* **Query Parameters:** + * `period`: `string` - *可选*. 时间段 (默认: `week`). + +### 17. 请求类型统计 + +* **Method:** `GET` +* **Endpoint:** `/stats/types` +* **Query Parameters:** + * `period`: `string` - *可选*. 时间段 (默认: `today`). \ No newline at end of file diff --git a/backend/.nvmrc b/backend/.nvmrc new file mode 100644 index 0000000..5dbac1e --- /dev/null +++ b/backend/.nvmrc @@ -0,0 +1 @@ +v16.13.0 \ No newline at end of file diff --git a/backend/accounts.sample.json b/backend/accounts.sample.json new file mode 100644 index 0000000..633e7c2 --- /dev/null +++ b/backend/accounts.sample.json @@ -0,0 +1,14 @@ +{ + "Melody Key,建议随机生成 UUID": { + "loginType": "固定为:phone,目前仅支持手机号+密码登录。下面为示例", + "account": "填写手机号。如:18888888888", + "password": "填写密码", + "platform": "固定为:wy,目前仅支持网易云。" + }, + "melody": { + "loginType": "phone", + "account": "", + "password": "", + "platform": "wy" + } +} diff --git a/backend/bin/.gitkeep b/backend/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..a8c4792 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,33 @@ +{ + "name": "melody-backend", + "version": "0.1.0", + "main": "index.js", + "scripts": { + "dev": "nodemon node src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/foamzou/personal-music-assistant.git" + }, + "author": "foamzou", + "license": "ISC", + "bugs": { + "url": "https://github.com/foamzou/personal-music-assistant/issues" + }, + "homepage": "https://github.com/foamzou/personal-music-assistant#readme", + "dependencies": { + "NeteaseCloudMusicApi": "4.6.7", + "body-parser": "^1.19.1", + "consola": "^2.15.3", + "cors": "^2.8.5", + "express": "^4.17.2", + "got": "11", + "md5": "^2.3.0", + "node-schedule": "^2.1.1", + "uuid": "^8.3.2" + }, + "devDependencies": { + "nodemon": "^2.0.15" + } +} diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml new file mode 100644 index 0000000..3f0cb82 --- /dev/null +++ b/backend/pnpm-lock.yaml @@ -0,0 +1,1699 @@ +lockfileVersion: 5.4 + +specifiers: + NeteaseCloudMusicApi: 4.6.7 + body-parser: ^1.19.1 + consola: ^2.15.3 + cors: ^2.8.5 + express: ^4.17.2 + got: '11' + md5: ^2.3.0 + node-schedule: ^2.1.1 + nodemon: ^2.0.15 + uuid: ^8.3.2 + +dependencies: + NeteaseCloudMusicApi: 4.6.7 + body-parser: 1.20.3 + consola: 2.15.3 + cors: 2.8.5 + express: 4.21.2 + got: 11.8.6 + md5: 2.3.0 + node-schedule: 2.1.1 + uuid: 8.3.2 + +devDependencies: + nodemon: 2.0.22 + +packages: + + /@sindresorhus/is/4.6.0: + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + dev: false + + /@szmarczak/http-timer/4.0.6: + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + + /@tokenizer/token/0.3.0: + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + dev: false + + /@tootallnate/once/1.1.2: + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + dev: false + + /@types/cacheable-request/6.0.3: + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 22.10.7 + '@types/responselike': 1.0.3 + dev: false + + /@types/http-cache-semantics/4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: false + + /@types/keyv/3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 22.10.7 + dev: false + + /@types/node/22.10.7: + resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==} + dependencies: + undici-types: 6.20.0 + dev: false + + /@types/responselike/1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + dependencies: + '@types/node': 22.10.7 + dev: false + + /NeteaseCloudMusicApi/4.6.7: + resolution: {integrity: sha512-Qw+r3ti2O5vgrqeOxOvlu+DNsTdzfxJ+nC6C5rjbwU2d4VMZZynqm8Sb+692WGQvZAjH5aTbXLnGAR8hBFJdEQ==} + engines: {node: '>=12'} + hasBin: true + dependencies: + axios: 0.24.0 + express: 4.21.2 + express-fileupload: 1.5.1 + md5: 2.3.0 + music-metadata: 7.14.0 + pac-proxy-agent: 5.0.0 + qrcode: 1.5.4 + safe-decode-uri-component: 1.2.1 + tunnel: 0.0.6 + yargs: 17.7.2 + transitivePeerDependencies: + - debug + - supports-color + dev: false + + /accepts/1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-walk/8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.14.0 + dev: false + + /acorn/8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /agent-base/6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + dev: false + + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: false + + /anymatch/3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /array-flatten/1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /ast-types/0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + dependencies: + tslib: 2.8.1 + dev: false + + /axios/0.24.0: + resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} + dependencies: + follow-redirects: 1.15.9 + transitivePeerDependencies: + - debug + dev: false + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions/2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: true + + /body-parser/1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces/3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /busboy/1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + + /bytes/3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /cacheable-lookup/5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + dev: false + + /cacheable-request/7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + dev: false + + /call-bind-apply-helpers/1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + dev: false + + /call-bound/1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.7 + dev: false + + /camelcase/5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: false + + /charenc/0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + + /chokidar/3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /cliui/6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + + /cliui/8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + + /clone-response/1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: false + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: false + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: false + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /consola/2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + dev: false + + /content-disposition/0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type/1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature/1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie/0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + dev: false + + /core-util-is/1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + + /cors/2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + + /cron-parser/4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + dependencies: + luxon: 3.5.0 + dev: false + + /crypt/0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + + /data-uri-to-buffer/3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + dev: false + + /debug/2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug/3.2.7_supports-color@5.5.0: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + dev: true + + /debug/4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: false + + /decamelize/1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: false + + /decompress-response/6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + + /deep-is/0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: false + + /defer-to-connect/2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: false + + /degenerator/3.0.4: + resolution: {integrity: sha512-Z66uPeBfHZAHVmue3HPfyKu2Q0rC2cRxbTOsvmU/po5fvvcx27W4mIu9n0PUlQih4oUYvcG1BsbtVv8x7KDOSw==} + engines: {node: '>= 6'} + dependencies: + ast-types: 0.13.4 + escodegen: 1.14.3 + esprima: 4.0.1 + vm2: 3.9.19 + dev: false + + /depd/2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy/1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /dijkstrajs/1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dev: false + + /dunder-proto/1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + dev: false + + /ee-first/1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /emoji-regex/8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /encodeurl/1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /encodeurl/2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + dev: false + + /end-of-stream/1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + + /es-define-property/1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + dev: false + + /es-errors/1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + + /es-object-atoms/1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: false + + /escalade/3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + dev: false + + /escape-html/1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /escodegen/1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 4.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: false + + /esprima/4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /estraverse/4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: false + + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: false + + /etag/1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express-fileupload/1.5.1: + resolution: {integrity: sha512-LsYG1ALXEB7vlmjuSw8ABeOctMp8a31aUC5ZF55zuz7O2jLFnmJYrCv10py357ky48aEoBQ/9bVXgFynjvaPmA==} + engines: {node: '>=12.0.0'} + dependencies: + busboy: 1.6.0 + dev: false + + /express/4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /fast-levenshtein/2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: false + + /file-type/16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 6.3.0 + token-types: 4.2.1 + dev: false + + /file-uri-to-path/2.0.0: + resolution: {integrity: sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==} + engines: {node: '>= 6'} + dev: false + + /fill-range/7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /finalhandler/1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /find-up/4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: false + + /follow-redirects/1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /forwarded/0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh/0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fs-extra/8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: false + + /fsevents/2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /ftp/0.3.10: + resolution: {integrity: sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==} + engines: {node: '>=0.8.0'} + dependencies: + readable-stream: 1.1.14 + xregexp: 2.0.0 + dev: false + + /function-bind/1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /get-caller-file/2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false + + /get-intrinsic/1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + dev: false + + /get-proto/1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + dev: false + + /get-stream/5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.2 + dev: false + + /get-uri/3.0.2: + resolution: {integrity: sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 1.1.2 + data-uri-to-buffer: 3.0.1 + debug: 4.4.0 + file-uri-to-path: 2.0.0 + fs-extra: 8.1.0 + ftp: 0.3.10 + transitivePeerDependencies: + - supports-color + dev: false + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /gopd/1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + dev: false + + /got/11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + dev: false + + /graceful-fs/4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + + /has-flag/3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-symbols/1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + dev: false + + /hasown/2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /http-cache-semantics/4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: false + + /http-errors/2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /http-proxy-agent/4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + dev: false + + /http2-wrapper/1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: false + + /https-proxy-agent/5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + dev: false + + /iconv-lite/0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /ignore-by-default/1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ip-address/9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + dev: false + + /ip/1.1.9: + resolution: {integrity: sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==} + dev: false + + /ipaddr.js/1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + dev: true + + /is-buffer/1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point/3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /isarray/0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: false + + /jsbn/1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + dev: false + + /json-buffer/3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: false + + /jsonfile/4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /keyv/4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: false + + /levn/0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: false + + /locate-path/5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: false + + /long-timeout/0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + dev: false + + /lowercase-keys/2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: false + + /luxon/3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + dev: false + + /math-intrinsics/1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + dev: false + + /md5/2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + + /media-typer/0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /media-typer/1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + dev: false + + /merge-descriptors/1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + dev: false + + /methods/1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db/1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types/2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime/1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /mimic-response/1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: false + + /mimic-response/3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /ms/2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms/2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /music-metadata/7.14.0: + resolution: {integrity: sha512-xrm3w7SV0Wk+OythZcSbaI8mcr/KHd0knJieu8bVpaPfMv/Agz5EooCAPz3OR5hbYMiUG6dgAPKZKnMzV+3amA==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + content-type: 1.0.5 + debug: 4.4.0 + file-type: 16.5.4 + media-typer: 1.1.0 + strtok3: 6.3.0 + token-types: 4.2.1 + transitivePeerDependencies: + - supports-color + dev: false + + /negotiator/0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /netmask/2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + dev: false + + /node-schedule/2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} + engines: {node: '>=6'} + dependencies: + cron-parser: 4.9.0 + long-timeout: 0.1.1 + sorted-array-functions: 1.3.0 + dev: false + + /nodemon/2.0.22: + resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} + engines: {node: '>=8.10.0'} + hasBin: true + dependencies: + chokidar: 3.6.0 + debug: 3.2.7_supports-color@5.5.0 + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 5.7.2 + simple-update-notifier: 1.1.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /normalize-url/6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: false + + /object-assign/4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + + /object-inspect/1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + dev: false + + /on-finished/2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: false + + /optionator/0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + dev: false + + /p-cancelable/2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + dev: false + + /p-limit/2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: false + + /p-locate/4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: false + + /p-try/2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: false + + /pac-proxy-agent/5.0.0: + resolution: {integrity: sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==} + engines: {node: '>= 8'} + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.0 + get-uri: 3.0.2 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + pac-resolver: 5.0.1 + raw-body: 2.5.2 + socks-proxy-agent: 5.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /pac-resolver/5.0.1: + resolution: {integrity: sha512-cy7u00ko2KVgBAjuhevqpPeHIkCIqPe1v24cydhWjmeuzaBfmUWFCZJ1iAh5TuVzVZoUzXIW7K8sMYOZ84uZ9Q==} + engines: {node: '>= 8'} + dependencies: + degenerator: 3.0.4 + ip: 1.1.9 + netmask: 2.0.2 + dev: false + + /parseurl/1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-exists/4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: false + + /path-to-regexp/0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + dev: false + + /peek-readable/4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + dev: false + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pngjs/5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + dev: false + + /prelude-ls/1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: false + + /proxy-addr/2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /pstree.remy/1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + + /pump/3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + + /qrcode/1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + dev: false + + /qs/6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.1.0 + dev: false + + /quick-lru/5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: false + + /range-parser/1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body/2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /readable-stream/1.1.14: + resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + dev: false + + /readable-stream/3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readable-web-to-node-stream/3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + dependencies: + readable-stream: 3.6.2 + dev: false + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /require-directory/2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: false + + /require-main-filename/2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: false + + /resolve-alpn/1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: false + + /responselike/2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + dependencies: + lowercase-keys: 2.0.0 + dev: false + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safe-decode-uri-component/1.2.1: + resolution: {integrity: sha512-j0PKk0v8qPOD0goqRAvoI7GKy+HwLHXEpXaQlA1IqJ65jjiILRK1InlfNdIKUUUYuLtERADaJumgYKNfyQBO+w==} + dev: false + + /safer-buffer/2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /semver/5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + dev: true + + /semver/7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true + dev: true + + /send/0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static/1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-blocking/2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false + + /setprototypeof/1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel-list/1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + dev: false + + /side-channel-map/1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + dev: false + + /side-channel-weakmap/1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + side-channel-map: 1.0.1 + dev: false + + /side-channel/1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + dev: false + + /simple-update-notifier/1.1.0: + resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} + engines: {node: '>=8.10.0'} + dependencies: + semver: 7.0.0 + dev: true + + /smart-buffer/4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: false + + /socks-proxy-agent/5.0.1: + resolution: {integrity: sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.4.0 + socks: 2.8.3 + transitivePeerDependencies: + - supports-color + dev: false + + /socks/2.8.3: + resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + dev: false + + /sorted-array-functions/1.3.0: + resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==} + dev: false + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dev: false + optional: true + + /sprintf-js/1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + dev: false + + /statuses/2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /streamsearch/1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /string_decoder/0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + dev: false + + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /strtok3/6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + dev: false + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /toidentifier/1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /token-types/4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: false + + /touch/3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + dev: true + + /tslib/2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + dev: false + + /tunnel/0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + dev: false + + /type-check/0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: false + + /type-is/1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /undefsafe/2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + + /undici-types/6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + dev: false + + /universalify/0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: false + + /unpipe/1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /util-deprecate/1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /utils-merge/1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /uuid/8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /vary/1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /vm2/3.9.19: + resolution: {integrity: sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==} + engines: {node: '>=6.0'} + deprecated: The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm. + hasBin: true + dependencies: + acorn: 8.14.0 + acorn-walk: 8.3.4 + dev: false + + /which-module/2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: false + + /word-wrap/1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: false + + /wrap-ansi/6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrap-ansi/7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: false + + /xregexp/2.0.0: + resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==} + dev: false + + /y18n/4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: false + + /y18n/5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + + /yargs-parser/18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + + /yargs-parser/21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: false + + /yargs/15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: false + + /yargs/17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + 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 + dev: false diff --git a/backend/src/consts/business_code.js b/backend/src/consts/business_code.js new file mode 100644 index 0000000..a8a1ff3 --- /dev/null +++ b/backend/src/consts/business_code.js @@ -0,0 +1,5 @@ +module.exports = { + StatusJobAlreadyExisted: 40010, + StatusJobNoNeedToCreate: 40011, + StatusNoNeedToSync: 40012, +} diff --git a/backend/src/consts/job_status.js b/backend/src/consts/job_status.js new file mode 100644 index 0000000..a43e2fe --- /dev/null +++ b/backend/src/consts/job_status.js @@ -0,0 +1,6 @@ +module.exports = { + Pending: "待开始", + InProgress: "进行中", + Failed: "失败", + Finished: "已完成", +} \ No newline at end of file diff --git a/backend/src/consts/job_type.js b/backend/src/consts/job_type.js new file mode 100644 index 0000000..9eabaa8 --- /dev/null +++ b/backend/src/consts/job_type.js @@ -0,0 +1,7 @@ +module.exports = { + UnblockedPlaylist: "UnblockedPlaylist", + UnblockedSong: "UnblockedSong", + SyncSongFromUrl: "SyncSongFromUrl", + DownloadSongFromUrl: "DownloadSongFromUrl", + SyncThePlaylistToLocalService: "SyncThePlaylistToLocalService", +} \ No newline at end of file diff --git a/backend/src/consts/sound_quality.js b/backend/src/consts/sound_quality.js new file mode 100644 index 0000000..199e1a9 --- /dev/null +++ b/backend/src/consts/sound_quality.js @@ -0,0 +1,4 @@ +module.exports = { + High: "high", + Lossless: "lossless", +} \ No newline at end of file diff --git a/backend/src/consts/source.js b/backend/src/consts/source.js new file mode 100644 index 0000000..4d0c5b3 --- /dev/null +++ b/backend/src/consts/source.js @@ -0,0 +1,40 @@ +module.exports = { + consts: { + Netease : { + code: 'netease', + label: '网易云', + }, + Bilibili : { + code: 'bilibili', + label: '哔哩哔哩', + }, + Douyin : { + code: 'douyin', + label: '抖音', + }, + Kugou : { + code: 'kugou', + label: '酷狗', + }, + Kuwo : { + code: 'kuwo', + label: '酷我', + }, + Migu : { + code: 'migu', + label: '咪咕', + }, + QQ : { + code: 'qq', + label: 'QQ', + }, + Youtube : { + code: 'youtube', + label: 'Youtube', + }, + Qmkg : { + code: 'qmkg', + label: '全民K歌', + }, + }, +} \ No newline at end of file diff --git a/backend/src/errors/account_not_existed.js b/backend/src/errors/account_not_existed.js new file mode 100644 index 0000000..29d6287 --- /dev/null +++ b/backend/src/errors/account_not_existed.js @@ -0,0 +1,6 @@ +module.exports = class AccountNotExisted extends Error { + constructor(message) { + super(message); + this.name = 'AccountNotExisted'; + } +} \ No newline at end of file diff --git a/backend/src/handler/account.js b/backend/src/handler/account.js new file mode 100644 index 0000000..50762f2 --- /dev/null +++ b/backend/src/handler/account.js @@ -0,0 +1,108 @@ +const AccountService = require('../service/account'); +const WYAPI = require('../service/music_platform/wycloud'); +const { storeCookie } = require('../service/music_platform/wycloud/transport.js'); + +async function get(req, res) { + res.send({ + status: 0, + data: { + account: await getWyAccountInfo(req.account.uid) + } + }); +} + +async function set(req, res) { + const loginType = req.body.loginType; + const accountName = req.body.account; + const password = req.body.password; + const countryCode = req.body.countryCode; + const config = req.body.config; + const name = req.body.name; + + if (name) { + // check if the name is already used by other accounts + const allAccounts = await AccountService.getAllAccountsWithoutSensitiveInfo(); + for (const account of Object.values(allAccounts)) { + if (account.name === name && account.uid !== req.account.uid) { + res.status(412).send({ status: 1, message: '昵称已被占用啦,请换一个试试吧', data: {} }); + return; + } + } + } + + const ret = await AccountService.setAccount(req.account.uid, loginType, accountName, password, countryCode, config, name); + res.send({ + status: ret ? 0 : 1, + data: { + account: await getWyAccountInfo(req.account.uid) + } + }); +} + +async function getWyAccountInfo(uid) { + const account = AccountService.getAccount(uid) + const wyInfo = await WYAPI.getMyAccount(uid); + account.wyAccount = wyInfo; + return account; +} + +async function qrLoginCreate(req, res) { + const qrData = await WYAPI.qrLoginCreate(req.account.uid); + if (qrData === false) { + res.status(500).send({ + status: 1, + message: 'qr login create failed', + data: {} + }); + return; + } + res.send({ + status: 0, + data: { + qrKey: qrData.qrKey, + qrCode: qrData.qrCode, + } + }); +} +async function qrLoginCheck(req, res) { + // 800 为二维码过期; 801 为等待扫码; 802 为待确认; 803 为授权登录成功 + const loginCheckRet = await WYAPI.qrLoginCheck(req.account.uid, req.query.qrKey); + let account = false; + if (loginCheckRet.code == 803) { + // it's a bad design to export the transport function here. Let's refactor it at a good time. + // should be put the cookie method to a cookie manager service + req.account.loginType = 'qrcode'; + req.account.account = 'temp'; + storeCookie(req.account.uid, req.account, loginCheckRet.cookie); + + account = await getWyAccountInfo(req.account.uid); + req.account.account = account.wyAccount.userId; + storeCookie(req.account.uid, req.account, loginCheckRet.cookie); + + AccountService.setAccount(req.account.uid, 'qrcode', account.wyAccount.userId, '', null); + account = await getWyAccountInfo(req.account.uid); + } + res.send({ + status: loginCheckRet ? 0 : 1, + data: { + wyQrStatus: loginCheckRet.code, + account + } + }); +} + +async function getAllAccounts(req, res) { + const data = await AccountService.getAllAccountsWithoutSensitiveInfo(); + res.send({ + status: 0, + data: data + }); +} + +module.exports = { + get: get, + set: set, + qrLoginCreate, + qrLoginCheck, + getAllAccounts, +} \ No newline at end of file diff --git a/backend/src/handler/config.js b/backend/src/handler/config.js new file mode 100644 index 0000000..dd0dfff --- /dev/null +++ b/backend/src/handler/config.js @@ -0,0 +1,23 @@ +const ConfigService = require('../service/config_manager'); + +async function getGlobalConfig(req, res) { + const config = await ConfigService.getGlobalConfig(); + res.send({ + status: 0, + data: config + }); +} + +async function setGlobalConfig(req, res) { + const config = req.body; + await ConfigService.setGlobalConfig(config); + res.send({ + status: 0, + data: config + }); +} + +module.exports = { + getGlobalConfig, + setGlobalConfig, +} \ No newline at end of file diff --git a/backend/src/handler/media_fetcher_lib.js b/backend/src/handler/media_fetcher_lib.js new file mode 100644 index 0000000..745468a --- /dev/null +++ b/backend/src/handler/media_fetcher_lib.js @@ -0,0 +1,41 @@ +const logger = require('consola'); +const { getMediaGetInfo, getLatestMediaGetVersion, downloadTheLatestMediaGet } = require('../service/media_fetcher/media_get'); + +async function checkLibVersion(req, res) { + const query = req.query; + + if (!['mediaGet'].includes(query.lib)) { + res.send({ + status: 1, + message: "lib name is invalid", + }); + return; + } + + const latestVersion = await getLatestMediaGetVersion(); + const mediaGetInfo = await getMediaGetInfo(); + + res.send({ + status: 0, + data: { + mediaGetInfo, + latestVersion, + } + }); +} + +async function downloadTheLatestLib(req, res) { + const {version} = req.body; + + const succeed = await downloadTheLatestMediaGet(version); + + res.send({ + status: succeed ? 0 : 1, + data: {} + }); +} + +module.exports = { + checkLibVersion: checkLibVersion, + downloadTheLatestLib: downloadTheLatestLib, +} \ No newline at end of file diff --git a/backend/src/handler/playlists.js b/backend/src/handler/playlists.js new file mode 100644 index 0000000..0b8e018 --- /dev/null +++ b/backend/src/handler/playlists.js @@ -0,0 +1,92 @@ +const logger = require('consola'); +const { getUserAllPlaylist } = require('../service/music_platform/wycloud'); +const { getPlaylistDetail } = require('../service/music_platform/tunehub'); +const Source = require('../consts/source').consts; + +function splitArtists(artist) { + if (!artist) { + return []; + } + return artist + .split(/[、/]/) + .map(item => item.trim()) + .filter(Boolean); +} + +function normalizeTunehubPlaylist(playlistId, detail) { + const info = detail && detail.info ? detail.info : {}; + const list = detail && Array.isArray(detail.list) ? detail.list : []; + + const songs = list.map(item => { + const artists = splitArtists(item.artist); + const primaryArtist = artists[0] || item.artist || ''; + const cover = item.pic || info.pic || ''; + const isBlocked = !item.types || item.types.length === 0; + return { + songId: item.id, + songName: item.name || '', + artists, + artist: primaryArtist, + duration: 0, + album: item.album || '', + cover, + pageUrl: `https://music.163.com/song?id=${item.id}`, + playUrl: item.url || '', + isBlocked, + isCloud: false, + }; + }); + + return { + id: playlistId, + name: info.name || '', + cover: info.pic || '', + songs, + }; +} + +async function listAllPlaylists(req, res) { + const uid = req.account.uid; + const playlists = await getUserAllPlaylist(uid); + if (playlists === false) { + logger.error(`get user all playlist failed, uid: ${uid}`); + } + + res.send({ + status: playlists ? 0 : 1, + data: { + playlists, + } + }); +} + +async function listSongsFromPlaylist(req, res) { + const uid = req.account.uid; + const source = req.params.source; + const playlistId = req.params.id; + + if (source !== Source.Netease.code || !playlistId) { + res.send({ + status: 1, + message: "source or id is invalid", + }); + return; + } + const detail = await getPlaylistDetail(source, playlistId); + if (detail === false) { + logger.error(`get playlist detail failed, uid: ${uid}`); + } + const playlists = detail ? normalizeTunehubPlaylist(playlistId, detail) : false; + + res.send({ + status: playlists ? 0 : 1, + data: { + playlists: playlists ? playlists : [], + } + }); +} + +module.exports = { + listAllPlaylists: listAllPlaylists, + listSongsFromPlaylist: listSongsFromPlaylist, +} diff --git a/backend/src/handler/proxy.js b/backend/src/handler/proxy.js new file mode 100644 index 0000000..e51288e --- /dev/null +++ b/backend/src/handler/proxy.js @@ -0,0 +1,46 @@ +const logger = require('consola'); +const got = require('got'); + +async function proxyAudio(req, res) { + const url = req.query.url; + const source = req.query.source; + const referer = req.query.referer; + + if (!url || !source) { + res.status(400).send({ + status: 1, + message: "url and source are required" + }); + return; + } + + // 只允许 bilibili 源 + if (source !== 'bilibili') { + res.status(403).send({ + status: 1, + message: "only bilibili source is allowed" + }); + return; + } + + try { + const stream = got.stream(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)', + 'Referer': referer || 'https://www.bilibili.com' + } + }); + + stream.pipe(res); + } catch (err) { + logger.error('proxy audio error:', err); + res.status(500).send({ + status: 1, + message: "proxy failed" + }); + } +} + +module.exports = { + proxyAudio +}; \ No newline at end of file diff --git a/backend/src/handler/scheduler.js b/backend/src/handler/scheduler.js new file mode 100644 index 0000000..d9dafda --- /dev/null +++ b/backend/src/handler/scheduler.js @@ -0,0 +1,27 @@ +const schedulerService = require('../service/scheduler'); +const AccountService = require('../service/account'); + +async function getNextRun(req, res) { + const localNextRun = schedulerService.getLocalSyncNextRun(); + const accounts = await AccountService.getAllAccounts(); + + const cloudNextRuns = {}; + for (const uid in accounts) { + const nextRun = schedulerService.getCloudSyncNextRun(uid); + if (nextRun) { + cloudNextRuns[uid] = nextRun; + } + } + + res.send({ + status: 0, + data: { + localNextRun, + cloudNextRuns + } + }); +} + +module.exports = { + getNextRun +}; diff --git a/backend/src/handler/song_meta.js b/backend/src/handler/song_meta.js new file mode 100644 index 0000000..4b3f756 --- /dev/null +++ b/backend/src/handler/song_meta.js @@ -0,0 +1,31 @@ +const logger = require('consola'); +const { getMetaWithUrl } = require('../service/media_fetcher'); +const { matchUrlFromStr } = require('../utils/regex'); + +async function getMeta(req, res) { + const query = req.query; + + const url = matchUrlFromStr(query.url); + + if (!url) { + res.send({ + status: 1, + message: "url is invalid", + }); + return; + } + + const songMeta = await getMetaWithUrl(url); + songMeta && (songMeta.pageUrl = url); + + res.send({ + status: songMeta ? 0 : 1, + data: { + songMeta, + } + }); +} + +module.exports = { + getMeta: getMeta +} \ No newline at end of file diff --git a/backend/src/handler/songs.js b/backend/src/handler/songs.js new file mode 100644 index 0000000..b168f90 --- /dev/null +++ b/backend/src/handler/songs.js @@ -0,0 +1,76 @@ +const logger = require('consola'); +const { searchSongsWithKeyword, searchSongsWithSongMeta } = require('../service/search_songs'); +const { getPlayUrlWithOptions } = require('../service/songs_info'); +const { getMetaWithUrl } = require('../service/media_fetcher'); +const { matchUrlFromStr } = require('../utils/regex'); + +async function search(req, res) { + const query = req.query; + + const keywordOrUrl = query.keyword; + + if (!keywordOrUrl) { + res.send({ + status: 1, + message: "keyword is required", + }); + return; + } + let songs = []; + const url = matchUrlFromStr(keywordOrUrl); + if (!url) { + songs = await searchSongsWithKeyword(keywordOrUrl); + } else { + const songMeta = await getMetaWithUrl(url); + if (!songMeta) { + res.send({ + status: 2, + message: "can not get song meta with this url", + }); + return; + } + songs = await searchSongsWithSongMeta({ + songName: songMeta.songName, + artist: songMeta.artist, + album: songMeta.album, + duration: songMeta.duration, + }, { + expectArtistAkas: [], + allowSongsJustMatchDuration: true, + allowSongsNotMatchMeta: true, + }); + } + + res.send({ + status: 0, + data: { + songs: songs ? songs : [], + } + }); +} + +async function getPlayUrl(req, res) { + const source = req.params.source; + const songId = req.params.id; + + if (!source || !songId) { + res.send({ + status: 1, + message: "source and songId is required", + }); + return; + } + const playUrl = await getPlayUrlWithOptions(req.account.uid, source, songId); + + res.send({ + status: 0, + data: { + playUrl, + } + }); +} + +module.exports = { + search: search, + getPlayUrl: getPlayUrl +} \ No newline at end of file diff --git a/backend/src/handler/sync_jobs.js b/backend/src/handler/sync_jobs.js new file mode 100644 index 0000000..55f07dd --- /dev/null +++ b/backend/src/handler/sync_jobs.js @@ -0,0 +1,183 @@ +const logger = require('consola'); +const { unblockMusicInPlaylist, unblockMusicWithSongId } = require('../service/sync_music'); +const JobType = require('../consts/job_type'); +const Source = require('../consts/source').consts; +const { matchUrlFromStr } = require('../utils/regex'); +const { syncSingleSongWithUrl, syncPlaylist } = require('../service/sync_music'); +const findTheBestMatchFromWyCloud = require('../service/search_songs/find_the_best_match_from_wycloud'); +const JobManager = require('../service/job_manager'); +const JobStatus = require('../consts/job_status'); +const BusinessCode = require('../consts/business_code'); + + +async function createJob(req, res) { + const uid = req.account.uid; + const request = req.body; + + const jobType = request.jobType; + const options = request.options; + let jobId = 0; + + if (jobType === JobType.UnblockedPlaylist || jobType === JobType.SyncThePlaylistToLocalService) { + const source = request.playlist && request.playlist.source; + const playlistId = request.playlist && request.playlist.id; + + if (source !== Source.Netease.code || !playlistId) { + res.status(412).send({ + status: 1, + message: "source or id is invalid", + }); + return; + } + if (jobType === JobType.UnblockedPlaylist) { + jobId = await unblockMusicInPlaylist(uid, source, playlistId, { + syncWySong: options.syncWySong, + syncNotWySong: options.syncNotWySong, + asyncExecute: true, + }); + } else { + jobId = await syncPlaylist(uid, source, playlistId) + } + } else if (jobType === JobType.UnblockedSong) { + const source = request.source; + const songId = request.songId; + + if (source !== Source.Netease.code || !songId) { + res.status(412).send({ + status: 1, + message: "source or id is invalid", + }); + return; + } + jobId = await unblockMusicWithSongId(uid, source, songId) + } else if (jobType === JobType.SyncSongFromUrl || jobType === JobType.DownloadSongFromUrl) { + const request = req.body; + const url = request.urlJob && matchUrlFromStr(request.urlJob.url); + + if (!url) { + res.status(412).send({ + status: 1, + message: "url is invalid", + }); + return; + } + + let meta = {}; + const songId = request.urlJob && request.urlJob.meta.songId ? request.urlJob.meta.songId : ""; + + if (request.urlJob.meta && (request.urlJob.meta.songName !== "" && request.urlJob.meta.artist !== "")) { + meta = { + songName: request.urlJob.meta.songName, + artist: request.urlJob.meta.artist, + album : request.urlJob.meta.album ? request.urlJob.meta.album : "", + }; + } + + if (songId) { + const songFromWyCloud = await findTheBestMatchFromWyCloud(req.account.uid, { + songName: meta.songName, + artist: meta.artist, + album: meta.album, + musicPlatformSongId: songId, + }); + if (!songFromWyCloud) { + logger.error(`song not found in wycloud`); + res.status(412).send({ + status: 1, + message: "can not find song in wycloud with your songId", + }); + return; + } + meta.songFromWyCloud = songFromWyCloud; + } + + // create job + const args = `${jobType}: {"url":${url}}`; + if (await JobManager.findActiveJobByArgs(uid, args)) { + logger.info(`${jobType} job is already running.`); + jobId = BusinessCode.StatusJobAlreadyExisted; + } else { + const operation = jobType === JobType.SyncSongFromUrl ? "上传" : "下载"; + jobId = await JobManager.createJob(uid, { + name: `${operation}歌曲:${meta.songName ? meta.songName : url}`, + args, + type: jobType, + status: JobStatus.Pending, + desc: `歌曲:${meta.songName ? meta.songName : url}`, + progress: 0, + tip: `等待${operation}`, + createdAt: Date.now() + }); + + // async job + syncSingleSongWithUrl(req.account.uid, url, meta, jobId, jobType).then(async ret => { + await JobManager.updateJob(uid, jobId, { + status: ret === true ? JobStatus.Finished : JobStatus.Failed, + progress: 1, + tip: ret === true ? `${operation}成功` : `${operation}失败`, + }); + }) + } + } else { + res.status(412).send({ + status: 1, + message: "jobType is not supported", + }); + return; + } + + if (jobId === false) { + logger.error(`create job failed, uid: ${uid}`); + res.status(412).send({ + status: 1, + message: "create job failed", + }); + return; + } + + if (jobId === BusinessCode.StatusJobAlreadyExisted) { + res.status(412).send({ + status: BusinessCode.StatusJobAlreadyExisted, + message: "你的任务已经在跑啦,等等吧", + }); + return; + } + if (jobId === BusinessCode.StatusJobNoNeedToCreate) { + res.status(412).send({ + status: BusinessCode.StatusJobAlreadyExisted, + message: "你的任务无需被创建,可能是因为没有需要 sync 的歌曲", + }); + return; + } + + res.status(201).send({ + status: jobId ? 0 : 1, + data: { + jobId, + } + }); +} + +async function listAllJobs(req, res) { + res.send({ + status: 0, + data: { + jobs: await JobManager.listJobs(req.account.uid), + } + }); +} + +async function getJob(req, res) { + res.send({ + status: 0, + data: { + jobs: await JobManager.getJob(req.account.uid, req.params.id), + } + }); +} + +module.exports = { + createJob: createJob, + listAllJobs: listAllJobs, + getJob: getJob, +} \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000..0b6b51c --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,39 @@ +const path = require('path'); +const logger = require('consola'); +const cors = require('cors'); +const express = require('express'); +const bodyParser = require('body-parser'); +const app = express(); +const port = 5566; + + +require('./init_app')().then(() => { + const middlewareHandleError = require('./middleware/handle_error'); + const middlewareAuth = require('./middleware/auth'); + const proxy = require('./handler/proxy'); + + app.use(bodyParser.json()); + app.use(cors({ + origin: true, + credentials: true, + })); + + // 先注册代理路由,跳过 auth 验证 + app.get('/api/proxy/audio', proxy.proxyAudio); + + // 其他 API 路由需要 auth + app.use('/api', middlewareAuth); + app.use('/', require('./router')); + app.use(middlewareHandleError); + + app.use(express.static(path.resolve(__dirname, '../public'))); + + const server = app.listen(port, () => { + const host = server.address().address + const port = server.address().port + logger.info(`Express server is listening on ${host}:${port}!`) + }) + + const schedulerService = require('./service/scheduler'); + schedulerService.start(); +}); diff --git a/backend/src/init_app.js b/backend/src/init_app.js new file mode 100644 index 0000000..77438dd --- /dev/null +++ b/backend/src/init_app.js @@ -0,0 +1,48 @@ +const logger = require('consola'); +const fs = require('fs'); +const process = require('process'); + +initDir(); +initAccountFileIfNotExisted(); + +const mediaGet = require('./service/media_fetcher/media_get'); + +function initDir() { + // make sure all dir has been created + const dirList = [ + __dirname + '/../.profile', + __dirname + '/../.profile/cookie', + __dirname + '/../.profile/data', + __dirname + '/../.profile/data/jobs', + ]; + + dirList.forEach(dir => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + }); +} + +function initAccountFileIfNotExisted() { + const targetFile = __dirname + '/../.profile/accounts.json'; + const sampleFile = __dirname + '/../accounts.sample.json'; + if (!fs.existsSync(targetFile)) { + fs.copyFileSync(sampleFile, targetFile); + logger.info('初始化 accounts.json 文件成功, 默认的 melody key 为: melody'); + } +} + +module.exports = async function() { + // check if media-get is installed + const mediaGetInfo = await mediaGet.getMediaGetInfo(); + if (mediaGetInfo === false) { + process.exit(-1); + } + if (!mediaGetInfo.hasInstallFFmpeg) { + logger.error('please install FFmpeg and FFprobe first'); + process.exit(-1); + } + logger.info(`[media-get] Version: ${mediaGetInfo.versionInfo}`); + + // TODO check media-get latest version +} \ No newline at end of file diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js new file mode 100644 index 0000000..adbe888 --- /dev/null +++ b/backend/src/middleware/auth.js @@ -0,0 +1,19 @@ +const AccountService = require('../service/account'); +const AccountNotExisted = require('../errors/account_not_existed'); +const logger = require('consola'); + +module.exports = (req, res, next) => { + if (req.method === 'OPTIONS') { + return next(); + } + if (!req.headers['mk']) { + throw new AccountNotExisted; + } + const account = AccountService.getAccount(req.headers['mk']) + if (!account) { + throw new AccountNotExisted; + } + //logger.info('user access', account); + req.account = account + next() +} \ No newline at end of file diff --git a/backend/src/middleware/handle_error.js b/backend/src/middleware/handle_error.js new file mode 100644 index 0000000..d5f76b6 --- /dev/null +++ b/backend/src/middleware/handle_error.js @@ -0,0 +1,19 @@ +const logger = require('consola'); +const AccountNotExisted = require('../errors/account_not_existed'); + +module.exports = async (error, req, res, next) => { + logger.error('catch error', error); + + if (error instanceof AccountNotExisted) { + res.status(403).send({ + status: 1, + message: "account not existed", + }); + return; + } + + res.status(500).send({ + status: 1, + message: "Internal Server Error", + }); +} \ No newline at end of file diff --git a/backend/src/router.js b/backend/src/router.js new file mode 100644 index 0000000..2ff3740 --- /dev/null +++ b/backend/src/router.js @@ -0,0 +1,42 @@ +const router = require('express').Router(); + +const SyncJob = require('./handler/sync_jobs'); +const Songs = require('./handler/songs'); +const SongMeta = require('./handler/song_meta'); +const Playlists = require('./handler/playlists'); +const Account = require('./handler/account'); +const MediaFetcherLib = require('./handler/media_fetcher_lib'); +const Config = require('./handler/config'); +const Scheduler = require('./handler/scheduler'); +const asyncWrapper = (cb) => { + return (req, res, next) => cb(req, res, next).catch(next); + }; + +router.post('/api/sync-jobs', asyncWrapper(SyncJob.createJob)); +router.get('/api/sync-jobs', asyncWrapper(SyncJob.listAllJobs)); +router.get('/api/sync-jobs/:id', asyncWrapper(SyncJob.getJob)); + +router.get('/api/songs', asyncWrapper(Songs.search)); +router.get('/api/songs/:source/:id/playUrl', asyncWrapper(Songs.getPlayUrl)); + +router.get('/api/songs-meta', asyncWrapper(SongMeta.getMeta)); + +router.get('/api/playlists', asyncWrapper(Playlists.listAllPlaylists)); +router.get('/api/playlists/:source/:id/songs', asyncWrapper(Playlists.listSongsFromPlaylist)); + +router.get('/api/account', asyncWrapper(Account.get)); +router.post('/api/account', asyncWrapper(Account.set)); +router.get('/api/accounts', asyncWrapper(Account.getAllAccounts)); +router.get('/api/account/qrlogin-create', asyncWrapper(Account.qrLoginCreate)); +router.get('/api/account/qrlogin-check', asyncWrapper(Account.qrLoginCheck)); + +router.get('/api/media-fetcher-lib/version-check', asyncWrapper(MediaFetcherLib.checkLibVersion)); +router.post('/api/media-fetcher-lib/update', asyncWrapper(MediaFetcherLib.downloadTheLatestLib)); + +router.get('/api/config/global', asyncWrapper(Config.getGlobalConfig)); +router.post('/api/config/global', asyncWrapper(Config.setGlobalConfig)); + +router.get('/api/scheduler/next-run', asyncWrapper(Scheduler.getNextRun)); + +module.exports = router; + \ No newline at end of file diff --git a/backend/src/service/account.js b/backend/src/service/account.js new file mode 100644 index 0000000..7b7e0e0 --- /dev/null +++ b/backend/src/service/account.js @@ -0,0 +1,117 @@ +const AccountPath = __dirname + '/../../.profile/accounts.json'; +const CookiePath = __dirname + '/../../.profile/cookie/'; +let AccountMap = require(AccountPath); +const logger = require('consola'); +const locker = require('../utils/simple_locker'); +const fs = require('fs'); +const SoundQuality = require('../consts/sound_quality'); + +module.exports = { + getAccount: getAccount, + setAccount: setAccount, + getAllAccounts: getAllAccounts, + getAllAccountsWithoutSensitiveInfo: getAllAccountsWithoutSensitiveInfo, +} + +const defaultConfig = { + playlistSyncToWyCloudDisk: { + autoSync: { + enable: false, + frequency: 1, + frequencyUnit: "day", + onlyCreatedPlaylists: true, + }, + syncWySong: true, + syncNotWySong: false, + soundQualityPreference: SoundQuality.High, + }, +}; + +function getAccount(uid) { + const account = AccountMap[uid]; + if (!account) { + logger.error(`the uid(${uid}) does not existed`); + return false; + } + if (!account.config) { + account.config = defaultConfig; + } + if (!account.config.playlistSyncToWyCloudDisk) { + account.config.playlistSyncToWyCloudDisk = defaultConfig.playlistSyncToWyCloudDisk; + } + account.uid = uid; + return account; +} + +async function setAccount(uid, loginType, account, password, countryCode = '', config, name) { + const lockKey = 'setAccount'; + await locker.lock(lockKey, 5); + + refreshAccountFromFile(); + + const userAccount = getAccount(uid); + if (!userAccount) { + locker.unlock(lockKey); + return false; + } + + const oldAccount = userAccount; + + userAccount.loginType = loginType; + userAccount.account = account; + userAccount.password = password; + userAccount.countryCode = countryCode; + if (name) { + userAccount.name = name; + } + + if (config) { + userAccount.config = config; + } + + AccountMap[uid] = userAccount; + + storeAccount(AccountMap); + locker.unlock(lockKey); + + // clear cookie + try { + fs.unlinkSync(CookiePath + uid); + } catch(_){} + + // 重启调度器以应用新的账号配置 + if (config && JSON.stringify(oldAccount?.config?.playlistSyncToWyCloudDisk) !== + JSON.stringify(config.playlistSyncToWyCloudDisk)) { + const schedulerService = require('../service/scheduler'); + await schedulerService.updateCloudSyncJob(uid); + } + + return true; +} + +function refreshAccountFromFile() { + AccountMap = JSON.parse(fs.readFileSync(AccountPath).toString()); +} + +function storeAccount(account) { + fs.writeFileSync(AccountPath, JSON.stringify(account, null, 4)); +} + +async function getAllAccounts() { + refreshAccountFromFile(); + return AccountMap; +} + +async function getAllAccountsWithoutSensitiveInfo() { + refreshAccountFromFile(); + const filteredAccounts = {}; + for (const [uid, account] of Object.entries(AccountMap)) { + filteredAccounts[uid] = { + name: account.name || uid, + uid: account.uid + }; + } + return filteredAccounts; +} + + diff --git a/backend/src/service/config_manager/index.js b/backend/src/service/config_manager/index.js new file mode 100644 index 0000000..ed9a670 --- /dev/null +++ b/backend/src/service/config_manager/index.js @@ -0,0 +1,85 @@ +const sound_quality = require('../../consts/sound_quality'); +const asyncFs = require('../../utils/fs'); + +const DataPath = `${__dirname}/../../../.profile/data`; +const ConfigPath = `${DataPath}/config`; +const GlobalConfig = `${ConfigPath}/global.json`; +const sourceConsts = require('../../consts/source').consts; +const libPath = require('path'); + +async function init() { + if (!await asyncFs.asyncFileExisted(ConfigPath)) { + await asyncFs.asyncMkdir(ConfigPath, { recursive: true }); + } +} +init(); + +const GlobalDefaultConfig = { + downloadPath: '', + filenameFormat: '{songName}-{artist}', + downloadPathExisted: false, + // don't search youtube by default + sources: Object.values(sourceConsts).map(i => i.code).filter(s => s !== sourceConsts.Youtube.code), + sourceConsts, + playlistSyncToLocal: { + autoSync: { + enable: false, + frequency: 1, + frequencyUnit: "day", + }, + deleteLocalFile: false, + filenameFormat: `{playlistName}${libPath.sep}{songName}-{artist}`, + soundQualityPreference: sound_quality.High, + syncAccounts: [], + }, +}; + +async function setGlobalConfig(config) { + const oldConfig = await getGlobalConfig(); + await asyncFs.asyncWriteFile(GlobalConfig, JSON.stringify(config)); + + // 只在本地同步配置发生变化时更新调度器 + if (JSON.stringify(oldConfig.playlistSyncToLocal) !== JSON.stringify(config.playlistSyncToLocal)) { + const schedulerService = require('../scheduler'); + await schedulerService.updateLocalSyncJob(); + } +} + +async function getGlobalConfig() { + if (!await asyncFs.asyncFileExisted(GlobalConfig)) { + return GlobalDefaultConfig; + } + const config = JSON.parse(await asyncFs.asyncReadFile(GlobalConfig)); + if (!config.sources) { + config.sources = GlobalDefaultConfig.sources; + } + config.sourceConsts = GlobalDefaultConfig.sourceConsts; + config.downloadPathExisted = false; + if (config.downloadPath) { + config.downloadPathExisted = await asyncFs.asyncFileExisted(config.downloadPath); + } + + if (!config.filenameFormat) { + config.filenameFormat = GlobalDefaultConfig.filenameFormat; + } + + if (!config.playlistSyncToLocal) { + config.playlistSyncToLocal = GlobalDefaultConfig.playlistSyncToLocal; + } + if (!config.playlistSyncToLocal.filenameFormat) { + config.playlistSyncToLocal.filenameFormat = GlobalDefaultConfig.playlistSyncToLocal.filenameFormat; + } + if (!config.playlistSyncToLocal.soundQualityPreference) { + config.playlistSyncToLocal.soundQualityPreference = GlobalDefaultConfig.playlistSyncToLocal.soundQualityPreference; + } + if (!config.playlistSyncToLocal.syncAccounts) { + config.playlistSyncToLocal.syncAccounts = GlobalDefaultConfig.playlistSyncToLocal.syncAccounts; + } + return config; +} + + +module.exports = { + setGlobalConfig, + getGlobalConfig, +} \ No newline at end of file diff --git a/backend/src/service/cronjob/index.js b/backend/src/service/cronjob/index.js new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/service/job_manager/index.js b/backend/src/service/job_manager/index.js new file mode 100644 index 0000000..68859eb --- /dev/null +++ b/backend/src/service/job_manager/index.js @@ -0,0 +1,189 @@ +const logger = require('consola'); +const asyncFs = require('../../utils/fs'); +const genUUID = require('../../utils/uuid'); +const { lock, unlock } = require('../../utils/simple_locker'); +const JobStatus = require('../../consts/job_status'); + +const DataPath = `${__dirname}/../../../.profile/data`; +const JobDataPath = `${DataPath}/jobs`; + +const JobManagerInitTime = Date.now(); + +async function listJobs(uid) { + const list = []; + const jobs = await getUserJobs(uid); + for (const jobId in jobs) { + const job = await getJob(uid, jobId); + if (!job) { + continue; + } + job.id = jobId; + list.push(job); + } + return list.sort((a, b) => b.createdAt - a.createdAt); +} + +async function getJob(uid, jobId) { + const jobFile = await getJobFilePath(uid, jobId, false); + if (!await asyncFs.asyncFileExisted(jobFile)) { + return null; + } + const fileText = await asyncFs.asyncReadFile(jobFile); + if (fileText == "") { + return null; + } + return JSON.parse(fileText); +} + +async function updateJob(uid, jobId, info) { + const lockKey = getJobLockKey(jobId); + if (!await lock(lockKey, 5)) { + logger.error(`get job locker failed, uid: ${uid}, job: ${jobId}`); + return false; + } + const job = await getJob(uid, jobId); + if (info.desc) { + job.desc = info.desc; + } + if (info.progress) { + job.progress = info.progress; + } + if (info.status) { + job.status = info.status; + } + if (info.tip) { + job.tip = info.tip; + if (!info.log) { + info.log = info.tip + } + } + if (info.log) { + if (!job.logs) { + job.logs = []; + } + job.logs.push({ + time: Date.now(), + info: info.log + }); + } + if (info.data) { + job.data = info.data; + } + const jobFile = await getJobFilePath(uid, jobId); + await asyncFs.asyncWriteFile(jobFile, JSON.stringify(job)); + + unlock(lockKey); +} + +async function createJob(uid, job = { + name: '', + type: '', + desc: '', + progress: 0, + tip: '', + status: '', + logs: [], + data: {}, + createdAt: Date.now(), +}) { + const jobId = genUUID(); + const jobFile = await getJobFilePath(uid, jobId); + await asyncFs.asyncWriteFile(jobFile, JSON.stringify(job)); + + await addJobIdToUserJobList(uid, jobId); + return jobId; +} + +async function deleteJob(uid, jobId) { + await removeJobIdFromUserJobList(uid, jobId); + await asyncFs.asyncUnlinkFile(await getJobFilePath(uid, jobId, false)); +} + +async function addJobIdToUserJobList(uid, jobId) { + const lockKey = getJobListLockKey(uid); + if (!await lock(lockKey, 5)) { + logger.error(`get job_list locker failed, uid: ${uid}`); + return false; + } + const jobs = await getUserJobs(uid); + jobs[jobId] = { + createdAt: Date.now(), + }; + await asyncFs.asyncWriteFile(await getJobListFilePath(uid), JSON.stringify(jobs)); + unlock(lockKey); +} + +async function removeJobIdFromUserJobList(uid, jobId) { + const lockKey = getJobListLockKey(uid); + if (!await lock(lockKey, 5)) { + logger.error(`get job_list locker failed, uid: ${uid}`); + return false; + } + const jobs = await getUserJobs(uid); + delete jobs[jobId]; + await asyncFs.asyncWriteFile(await getJobListFilePath(uid), JSON.stringify(jobs)); + unlock(lockKey); +} + +function getJobListLockKey(uid) { + return `job_list_${uid}`; +} + +function getJobLockKey(jobId) { + return `job_${jobId}`; +} + +async function getUserJobs(uid) { + const jobListFile = await getJobListFilePath(uid); + return JSON.parse(await asyncFs.asyncReadFile(jobListFile)); +} + +async function getJobFilePath(uid, jobId, createIfNotExist = true) { + const path = `${await getUserJobPath(uid)}/${jobId}`; + if (createIfNotExist && !await asyncFs.asyncFileExisted(path)) { + await asyncFs.asyncWriteFile(path, '{}'); + } + return path; +} + +async function getJobListFilePath(uid, createIfNotExist = true) { + const path = `${await getUserJobPath(uid)}/list`; + if (createIfNotExist && !await asyncFs.asyncFileExisted(path)) { + await asyncFs.asyncWriteFile(path, '{}'); + } + return path; +} + +async function getUserJobPath(uid, createIfNotExist = true) { + const path = `${JobDataPath}/${uid}`; + if (createIfNotExist && !await asyncFs.asyncFileExisted(path)) { + await asyncFs.asyncMkdir(path, { recursive: true }); + } + return path; +} + +async function findActiveJobByArgs(uid, args) { + const jobs = await listJobs(uid); + return jobs.find(job => { + if (job['args'] === args && job['status'] !== JobStatus.Failed && job['status'] !== JobStatus.Finished) { + // 如果创建时间 早于 组件 init 时间,那么认为是无效的 job(意味着服务重启了,而目前 job 不支持重启服务后继续 run) + if (job['createdAt'] < JobManagerInitTime) { + return false; + } + // 超过 1 小时也认为超时 + if (Date.now() - job['createdAt'] > 1000 * 60 * 60) { + return false; + } + return job; + } + }); +} + +module.exports = { + listJobs: listJobs, + createJob: createJob, + findActiveJobByArgs: findActiveJobByArgs, + deleteJob: deleteJob, + getJob: getJob, + updateJob: updateJob, +} \ No newline at end of file diff --git a/backend/src/service/kv/index.js b/backend/src/service/kv/index.js new file mode 100644 index 0000000..85a2711 --- /dev/null +++ b/backend/src/service/kv/index.js @@ -0,0 +1,88 @@ +const { lock, unlock } = require('../../utils/simple_locker'); +const asyncFs = require('../../utils/fs'); +const logger = require('consola'); + +const DbPath = `${__dirname}/../../../.profile/data/kv-db`; + +async function init() { + if (!await asyncFs.asyncFileExisted(DbPath)) { + await asyncFs.asyncMkdir(DbPath); + } +} +init(); + +async function set(table, key, value) { + if (!await lock(table, 5)) { + logger.error(`get table locker failed, table: ${table}, key: ${key}, value: ${value}`); + return false; + } + const filePath = `${DbPath}/${table}.json`; + let data = {}; + if (await asyncFs.asyncFileExisted(filePath)) { + try { + data = JSON.parse(await asyncFs.asyncReadFile(filePath)); + } catch (err) { + logger.error(`parse ${filePath} failed`, err); + return false; + } + } + data[key] = value; + try { + await asyncFs.asyncWriteFile(filePath, JSON.stringify(data)); + } catch (err) { + logger.error(`write ${filePath} failed`, err); + return false; + } + unlock(table); + return true; +} + +async function get(table, key) { + const filePath = `${DbPath}/${table}.json`; + let data = {}; + if (await asyncFs.asyncFileExisted(filePath)) { + try { + data = JSON.parse(await asyncFs.asyncReadFile(filePath)); + } catch (err) { + logger.error(`parse ${filePath} failed`, err); + return false; + } + } + return data[key]; +} + +module.exports = { + set, + get, + fileSyncMeta: { + set: async function (source, sourceID, value) { + const key = `${source}-${sourceID}`; + return await set('fileSyncMeta', key, JSON.stringify(value)); + }, + get: async function (source, sourceID) { + const key = `${source}-${sourceID}`; + const ret = await get('fileSyncMeta', key); + if (!ret) { + return false; + } + return JSON.parse(ret); + }, + setPlaylistMeta: async function(playlistID, meta) { + const key = `playlist-${playlistID}`; + return await set('fileSyncMeta', key, JSON.stringify({ + songIDs: meta.songIDs || [], + // 预留其他字段 + })); + }, + getPlaylistMeta: async function(playlistID) { + const key = `playlist-${playlistID}`; + const ret = await get('fileSyncMeta', key); + if (!ret) { + return { + songIDs: [] + }; + } + return JSON.parse(ret); + } + } +}; \ No newline at end of file diff --git a/backend/src/service/media_fetcher/index.js b/backend/src/service/media_fetcher/index.js new file mode 100644 index 0000000..afa4cff --- /dev/null +++ b/backend/src/service/media_fetcher/index.js @@ -0,0 +1,180 @@ +const logger = require('consola'); +const os = require('os'); +const md5 = require('md5'); +const path = require('path'); +const cmd = require('../../utils/cmd'); +const fs = require('fs'); +const configManager = require('../config_manager') +const downloadFile = require('../../utils/download'); + +const { getBinPath } = require('./media_get'); + +const basePath = path.join(os.tmpdir(), 'melody-tmp-songs'); +// create path if not exists +if (!fs.existsSync(basePath)) { + fs.mkdirSync(basePath); +} +logger.info(`[tmp path] use ${basePath}`) + + +async function downloadViaSourceUrl(url) { + logger.info(`downloadViaSourceUrl params: url: ${url}`); + + const requestHash = md5(url); + const downloadPath = `${basePath}/${requestHash}.mp3`; + logger.info(`start download from ${url}`); + + + const isSucceed = await downloadFile(url, downloadPath); + if (!isSucceed) { + logger.error(`download failed with ${url}`); + return false; + } + + if (!fs.existsSync(downloadPath)) { + logger.error(`download failed with ${url}, the file not exists ${downloadPath}`); + return false; + } + logger.info(`download success, path: ${downloadPath}`); + return downloadPath; +} + +async function fetchWithUrl(url, { + songName = "", + addMediaTag = false, +}) { + logger.info(`fetchWithUrl params: ${JSON.stringify(arguments)}`); + if (songName) { + songName = songName.replace(/ /g, '').replace(/\./g, '').replace(/\//g, '').replace(/"/g, ''); + } + const requestHash = md5(`${url}${songName}${addMediaTag}`); + const fileBasePath = `${basePath}/${requestHash}`; + try { + fs.mkdirSync(fileBasePath, { recursive: true }); + } catch (err) { + logger.error('create dir failed', err); + return false; + } + + addMediaTag = false; // todo: 等到 media-get fix 偶现的 添加 addMediaTag 后 panic 的问题,再移除这行代码 + const downloadPath = `${fileBasePath}/${songName ? songName : requestHash}.mp3`; + logger.info(`start parse and download from ${url}`); + + let args = ['-u', `"${url}"`, '--out', `${downloadPath}`, '-t', 'audio', `${addMediaTag ? '--addMediaTag' : ''}`]; + + logger.info(`${getBinPath()} ${args.join(' ')}`); + + const {code, message} = await cmd(getBinPath(), args); + logger.info('-------') + logger.info(code); + logger.info(message); + logger.info('-------') + if (code != 0) { + return false; + } + + if (!fs.existsSync(downloadPath)) { + return false; + } + return downloadPath; +} + +async function getMetaWithUrl(url) { + logger.info(`getMetaWithUrl from ${url}`); + + let args = ['-u', `"${url}"`, '-m', '--infoFormat=json', '-l=silence']; + + const {code, message} = await cmd(getBinPath(), args); + logger.info('-------') + logger.info(code); + // logger.info(message); + logger.info('-------') + if (code != 0) { + logger.error(`getMetaWithUrl failed with ${url}, err: ${message}`); + return false; + } + + let meta; + try { + meta = JSON.parse(message); + } catch (e) { + logger.error(e, message) + return false; + } + + return { + songName: meta.title, + artist: meta.artist, + album: meta.album, + duration: meta.duration, + coverUrl: meta.cover_url, + publicTime: meta.public_time, + isTrial: meta.is_trial, + resourceType: meta.resource_type, + audios: meta.audios, + fromMusicPlatform: meta.from_music_platform, + resourceForbidden: meta.resource_forbidden, + source: meta.source + } +} + +async function searchSongFromAllPlatform({ + keyword, + songName, artist, album +}) { + logger.info(`searchSong with ${JSON.stringify(arguments)}`); + + const globalConfig = await configManager.getGlobalConfig(); + + let searchParams = keyword + ? ['-k', `"${keyword}"`] + : ['--searchSongName', `"${songName}"`, '--searchArtist', `"${artist}"`, '--searchAlbum', `"${album}"`]; + searchParams = searchParams.concat([ + '--searchType="song"', + '-m', + `--sources=${globalConfig.sources.join(',')}`, + '--infoFormat=json', + '-l', 'silence' + ]); + + logger.info(`cmdStr: ${getBinPath()} ${searchParams.join(' ')}`); + + const {code, message} = await cmd(getBinPath(), searchParams); + logger.info('-------') + logger.info(code); + // logger.info(message); + logger.info('-------') + if (code != 0) { + logger.error(`searchSong failed with ${arguments}, err: ${message}`); + return false; + } + + let jsonResponse; + try { + jsonResponse = JSON.parse(message); + } catch (e) { + logger.error(e, message) + return false; + } + + return jsonResponse.map(searchItem => { + return { + songName: searchItem.Name, + artist: searchItem.Artist, + album: searchItem.Album, + duration: searchItem.Duration, + url: searchItem.Url, + resourceForbidden: searchItem.ResourceForbidden, + source: searchItem.Source, + fromMusicPlatform: searchItem.FromMusicPlatform, + score: searchItem.Score, + } + }) +} + +module.exports = { + downloadViaSourceUrl: downloadViaSourceUrl, + fetchWithUrl: fetchWithUrl, + getMetaWithUrl: getMetaWithUrl, + searchSongFromAllPlatform: searchSongFromAllPlatform, +} \ No newline at end of file diff --git a/backend/src/service/media_fetcher/media_get.js b/backend/src/service/media_fetcher/media_get.js new file mode 100644 index 0000000..14385fd --- /dev/null +++ b/backend/src/service/media_fetcher/media_get.js @@ -0,0 +1,228 @@ +const logger = require('consola'); +const https = require('https'); +const cmd = require('../../utils/cmd'); +var isWin = require('os').platform().indexOf('win32') > -1; +const isLinux = require('os').platform().indexOf('linux') > -1; +const isDarwin = require('os').platform().indexOf('darwin') > -1; +const httpsGet = require('../../utils/network').asyncHttpsGet; +const RemoteConfig = require('../remote_config'); +const fs = require('fs'); + +function getBinPath(isTemp = false) { + return `${__dirname}/../../../bin/media-get` + (isTemp ? '-tmp-' : '') + (isWin ? '.exe' : ''); +} + +async function getMediaGetInfo(isTempBin = false) { + try { + const {code, message, error} = await cmd(getBinPath(isTempBin), ['-h']); + logger.info('Command execution result:', { + code, + error, + binPath: getBinPath(isTempBin) + }); + + if (code != 0) { + logger.error(`Failed to execute media-get:`, { + code, + error, + message + }); + return false; + } + + const hasInstallFFmpeg = message.indexOf('FFmpeg,FFprobe: installed') > -1; + const versionInfo = message.match(/Version:(.+?)\n/); + + return { + hasInstallFFmpeg, + versionInfo: versionInfo ? versionInfo[1].trim() : '', + fullMessage: message, + } + } catch (err) { + logger.error('Exception while executing media-get:', err); + return false; + } +} + +async function getLatestMediaGetVersion() { + const remoteConfig = await RemoteConfig.getRemoteConfig(); + const latestVerisonUrl = `${remoteConfig.bestGithubProxy}https://raw.githubusercontent.com/foamzou/media-get/main/LATEST_VERSION`; + console.log('start to get latest version from: ' + latestVerisonUrl); + + const latestVersion = await httpsGet(latestVerisonUrl); + console.log('latest version: ' + latestVersion); + if (latestVersion === null || (latestVersion || "").split('.').length !== 3) { + logger.error('获取 media-get 最新版本号失败, got: ' + latestVersion); + return false; + } + return latestVersion; +} + +async function downloadFile(url, filename) { + return new Promise((resolve) => { + let fileStream = fs.createWriteStream(filename); + let receivedBytes = 0; + + const handleResponse = (res) => { + // Handle redirects + if (res.statusCode === 301 || res.statusCode === 302) { + logger.info('Following redirect'); + fileStream.end(); + fileStream = fs.createWriteStream(filename); + if (res.headers.location) { + https.get(res.headers.location, handleResponse) + .on('error', handleError); + } + return; + } + + // Check for successful status code + if (res.statusCode !== 200) { + handleError(new Error(`HTTP Error: ${res.statusCode}`)); + return; + } + + const totalBytes = parseInt(res.headers['content-length'], 10); + + res.on('error', handleError); + fileStream.on('error', handleError); + + res.pipe(fileStream); + + res.on('data', (chunk) => { + receivedBytes += chunk.length; + }); + + fileStream.on('finish', () => { + fileStream.close(() => { + if (receivedBytes === 0) { + fs.unlink(filename, () => { + logger.error('Download failed: Empty file received'); + resolve(false); + }); + } else if (totalBytes && receivedBytes < totalBytes) { + fs.unlink(filename, () => { + logger.error(`Download incomplete: ${receivedBytes}/${totalBytes} bytes`); + resolve(false); + }); + } else { + resolve(true); + } + }); + }); + }; + + const handleError = (error) => { + fileStream.destroy(); + fs.unlink(filename, () => { + logger.error('Download error:', error); + resolve(false); + }); + }; + + const req = https.get(url, handleResponse) + .on('error', handleError) + .setTimeout(60000, () => { + handleError(new Error('Download timeout')); + }); + + req.on('error', handleError); + }); +} + +async function getMediaGetRemoteFilename(latestVersion) { + let suffix = 'win.exe'; + if (isLinux) { + suffix = 'linux'; + } + if (isDarwin) { + suffix = 'darwin'; + } + if (process.arch === 'arm64') { + suffix += '-arm64'; + } + const remoteConfig = await RemoteConfig.getRemoteConfig(); + return `${remoteConfig.bestGithubProxy}https://github.com/foamzou/media-get/releases/download/v${latestVersion}/media-get-${latestVersion}-${suffix}`; +} + +const renameFile = (oldName, newName) => { + return new Promise((resolve, reject) => { + fs.rename(oldName, newName, (err) => { + if (err) { + logger.error(err) + resolve(false); + } else { + resolve(true); + } + }); + }); + }; + +async function downloadTheLatestMediaGet(version) { + const remoteFile = await getMediaGetRemoteFilename(version); + logger.info('start to download media-get: ' + remoteFile); + const ret = await downloadFile(remoteFile, getBinPath(true)); + if (ret === false) { + logger.error('download failed'); + return false; + } + fs.chmodSync(getBinPath(true), '755'); + logger.info('download finished'); + + // Add debug logs for binary file and validate + try { + const stats = fs.statSync(getBinPath(true)); + logger.info(`Binary file stats: size=${stats.size}, mode=${stats.mode.toString(8)}`); + + // Check minimum file size (should be at least 2MB) + const minSize = 2 * 1024 * 1024; // 2MB + if (stats.size < minSize) { + logger.error(`Invalid binary file size: ${stats.size} bytes. Expected at least ${minSize} bytes`); + return false; + } + + // Check file permissions (should be executable) + const executableMode = 0o755; + if ((stats.mode & 0o777) !== executableMode) { + logger.error(`Invalid binary file permissions: ${stats.mode.toString(8)}. Expected: ${executableMode.toString(8)}`); + return false; + } + + // Skip validation when cross compiling + if (!process.env.CROSS_COMPILING) { + const temBinInfo = await getMediaGetInfo(true); + logger.info('Execution result:', { + binPath: getBinPath(true), + arch: process.arch, + platform: process.platform, + temBinInfo + }); + + if (!temBinInfo || temBinInfo.versionInfo === "") { + logger.error('testing new bin failed. Details:', { + binExists: fs.existsSync(getBinPath(true)), + binPath: getBinPath(true), + error: temBinInfo === false ? 'Execution failed' : 'No version info' + }); + return false; + } + } + + const renameRet = await renameFile(getBinPath(true), getBinPath()); + if (!renameRet) { + logger.error('rename failed'); + return false; + } + return true; + } catch (err) { + logger.error('Failed to get binary stats:', err); + return false; + } +} + +module.exports = { + getBinPath: getBinPath, + getMediaGetInfo: getMediaGetInfo, + getLatestMediaGetVersion: getLatestMediaGetVersion, + downloadTheLatestMediaGet: downloadTheLatestMediaGet, +} \ No newline at end of file diff --git a/backend/src/service/music_platform/tunehub.js b/backend/src/service/music_platform/tunehub.js new file mode 100644 index 0000000..0c23658 --- /dev/null +++ b/backend/src/service/music_platform/tunehub.js @@ -0,0 +1,52 @@ +const logger = require('consola'); +const { asyncHttpsGet } = require('../../utils/network'); + +const BaseUrl = 'https://music-dl.sayqz.com/api/'; + +function buildApiUrl(params = {}) { + const searchParams = new URLSearchParams(params); + return `${BaseUrl}?${searchParams.toString()}`; +} + +async function fetchJson(params = {}) { + const url = buildApiUrl(params); + const raw = await asyncHttpsGet(url); + if (!raw) { + return null; + } + try { + return JSON.parse(raw); + } catch (err) { + logger.error(`TuneHub 返回非 JSON: ${url}`); + return null; + } +} + +async function getPlaylistDetail(source, playlistId) { + const response = await fetchJson({ + source, + id: playlistId, + type: 'playlist', + }); + if (!response || response.code !== 200 || !response.data) { + return false; + } + return response.data; +} + +function buildSongUrl(source, songId, br) { + const params = { + source, + id: songId, + type: 'url', + }; + if (br) { + params.br = br; + } + return buildApiUrl(params); +} + +module.exports = { + getPlaylistDetail, + buildSongUrl, +}; diff --git a/backend/src/service/music_platform/wycloud/index.js b/backend/src/service/music_platform/wycloud/index.js new file mode 100644 index 0000000..d093dcd --- /dev/null +++ b/backend/src/service/music_platform/wycloud/index.js @@ -0,0 +1,312 @@ +const logger = require('consola'); +const { + cloud, cloudsearch, cloud_match, song_detail, + user_playlist, playlist_detail, user_account, playlist_track_all, + login_qr_check, login_qr_create, login_qr_key, +} = require('NeteaseCloudMusicApi'); +const fs = require('fs'); +const path = require('path'); +const {requestApi} = require('./transport'); +const { buildSongUrl } = require('../tunehub'); + +async function uploadSong(uid, filePath) { + const response = await safeRequest(uid, cloud, { + songFile: { + name: path.basename(filePath), + data: fs.readFileSync(filePath), + }, + }); + if (response === false) { + return false; + } + logger.debug('uploadSong\'s resonse: ', response) + if (!response.privateCloud) { + return false; + } + const songInfo = response.privateCloud.simpleSong; + + return { + songId: songInfo.id, + matched: songInfo.ar[0].id !== 0 && songInfo.al.id !== 0, // It's matched the song on wyMusic if singer and album has info + }; +} + + +async function searchSong(uid, songName, artist) { + const response = await safeRequest(uid, cloudsearch, { + keywords: `${songName} ${artist}`, + type: 1, + }); + if (response === false) { + return false; + } + if (!response.result || response.result.songs.length === 0) { + return false; + } + + return response.result.songs.map(song => { + let artists = []; + + if (song.ar.length !== 0) { + song.ar.map(artist => { + artists.push(artist.name); + artist.alias && artists.push(...artist.alias); + artist.alia && artists.push(...artist.alia); + }); + } + return { + songId: song.id, + songName: song.name, + album: song.al.name, + artists: artists.filter(a => a !== '' && a !== undefined), + }; + }) +} + +async function matchAndFixCloudSong(uid, cloudSongId, wySongId) { + const response = await safeRequest(uid, cloud_match, { + sid: cloudSongId, + asid: wySongId, + }); + if (response === false) { + return false; + } + if (response.code > 399) { + logger.warn(response); + return false; + } + return true; +} + +async function getMyAccount(uid) { + const response = await safeRequest(uid, user_account, { + uid, + }); + if (response === false) { + return false; + } + if (!response.profile) { + return false; + } + return { + userId: response.profile.userId, + nickname: response.profile.nickname, + avatarUrl: response.profile.avatarUrl, + }; +} + +async function getSongInfo(uid, id) { + const response = await safeRequest(uid, song_detail, { + ids: `"${id}"`, + }); + if (response === false) { + return false; + } + if (!response.songs || response.songs.length === 0) { + return false; + } + const songInfo = response['songs'][0]; + return { + songId: songInfo.id, + songName: songInfo.name, + artists: songInfo.ar.map(artist => artist.name), + duration: songInfo.dt / 1000, + album: songInfo.al.name, + cover: songInfo.al.picUrl, + }; +} + +async function getPlayUrl(uid, id, isLossless = false) { + const br = isLossless ? 'flac' : '320k'; + return buildSongUrl('netease', id, br); +} + +async function getUserAllPlaylist(uid) { + const wyAccount = await getMyAccount(uid); + if (wyAccount === false) { + logger.error(`uid(${uid}) get user's wycloud account failed.`); + return false; + } + const response = await safeRequest(uid, user_playlist, { + uid: wyAccount.userId, + }); + if (response === false) { + return false; + } + if (!response.playlist || response.playlist.length === 0) { + return false; + } + return response.playlist.map(playlist => { + return { + id: playlist.id, + name: playlist.name, + cover: playlist.coverImgUrl, + trackCount: playlist.trackCount, + isCreatedByMe: playlist.creator.userId === wyAccount.userId, + }; + }); +} + +async function getSongsFromPlaylist(uid, source, playlistId) { + const [detailResponse, songsResponse] = await Promise.all([ + safeRequest(uid, playlist_detail, { + id: playlistId, + }), + safeRequest(uid, playlist_track_all, { + id: playlistId, + offset: 0, + limit: 1000, + }), + ]); + if (detailResponse === false || songsResponse === false) { + return false; + } + if (!detailResponse.playlist || !songsResponse.songs || songsResponse.songs.length === 0) { + logger.error(`uid(${uid}) playlist(${playlistId}) has no songs.`, detailResponse, songsResponse); + return false; + } + // console.log(JSON.stringify(songsResponse, null, 4)); + // ddd + if (songsResponse.songs.length >= 1000) { + const songsPage2Response = await safeRequest(uid, playlist_track_all, { + id: playlistId, + offset: 1000, + limit: 1000, + }); + if (songsPage2Response !== false && songsPage2Response.songs) { + songsResponse.songs = songsResponse.songs.concat(songsPage2Response.songs); + songsResponse.privileges = songsResponse.privileges.concat(songsPage2Response.privileges); + } + } + + let info = { + id: playlistId, + name: detailResponse.playlist.name, + cover: detailResponse.playlist.coverImgUrl, + songs: [], + }; + const songsMap = {}; + songsResponse.songs.map(song => { + songsMap[song.id] = song; + }); + + const isBlockedSong = (song, songInfo) => { + // the song has been added to cloud if the pc field is present + if (songInfo.pc) { + return false; + } + + // 收费歌曲 + if (song.fee === 1) { + if (song.realPayed === 1 || song.payed === 1) { + return false; + } + return true; + } + // subp 或 cp === 1 可能都表示有版权 + // 免费歌曲 + if (song.subp === 1) { + return false; + } + + return true; + }; + + songsResponse.privileges.forEach(song => { + const songInfo = songsMap[song.id]; + if (!songInfo) { + return; + } + + const isBlocked = isBlockedSong(song, songInfo); + const isCloud = !!songInfo.pc; + info.songs.push({ + songId: songInfo.id, + songName: songInfo.name, + artists: songInfo.ar.map(artist => artist.name), + artist: songInfo.ar.length > 0 ? songInfo.ar[0].name : '', + duration: songInfo.dt / 1000, + album: songInfo.al.name, + cover: songInfo.al.picUrl, + pageUrl: `https://music.163.com/song?id=${songInfo.id}`, + playUrl: !isBlocked && !isCloud ? `http://music.163.com/song/media/outer/url?id=${songInfo.id}.mp3` : '', // 不再建议使用这个 url,建议每次都 Call API 获取 + isBlocked, + isCloud, + }); + }); + + return info; +} + +async function getBlockedSongsFromPlaylist(uid, source, playlistId) { + const info = await getSongsFromPlaylist(uid, source, playlistId); + if (info === false) { + return false; + } + info.blockedSongs = info.songs.filter(song => song.isBlocked); + return info; +} + +async function qrLoginCreate(uid) { + const keyResponse = await safeRequest(uid, login_qr_key, {}, false); + if (keyResponse === false || !keyResponse.data.unikey) { + logger.warn(`uid(${uid}) get qr login key failed.`); + return false; + } + const qrKey = keyResponse.data.unikey; + const qrCodeResponse = await safeRequest(uid, login_qr_create, {key: qrKey, qrimg: true}, false); + if (qrCodeResponse === false || !qrCodeResponse.data.qrimg) { + return false; + } + return { + qrKey, + qrCode: qrCodeResponse.data.qrimg, + }; +} + +async function qrLoginCheck(uid, qrKey) { + const response = await safeRequest(uid, login_qr_check, {key: qrKey, cookie: { + os: 'pc', + }}, false); + if (response === false) { + return false; + } + return { + code: response.code, + cookie: response.cookie, + }; +} + +async function verifyAccountStatus(uid) { + const account = await getMyAccount(uid); + return account !== false; +} + +async function safeRequest(uid, moduleFunc, params, cookieRequired = true) { + try { + const response = await requestApi(uid, moduleFunc, params, cookieRequired); + if (response == false) { + logger.error(`request failed.`, response); + return false; + } + return response; + } catch (error) { + logger.error(`uid(${uid}) request failed.`, error); + return false; + } +} + +module.exports = { + getMyAccount: getMyAccount, + uploadSong: uploadSong, + matchAndFixCloudSong: matchAndFixCloudSong, + searchSong: searchSong, + getBlockedSongsFromPlaylist: getBlockedSongsFromPlaylist, + getSongsFromPlaylist: getSongsFromPlaylist, + getUserAllPlaylist: getUserAllPlaylist, + getSongInfo: getSongInfo, + getPlayUrl: getPlayUrl, + qrLoginCreate: qrLoginCreate, + qrLoginCheck: qrLoginCheck, + verifyAccountStatus: verifyAccountStatus, +} diff --git a/backend/src/service/music_platform/wycloud/transport.js b/backend/src/service/music_platform/wycloud/transport.js new file mode 100644 index 0000000..0b9a403 --- /dev/null +++ b/backend/src/service/music_platform/wycloud/transport.js @@ -0,0 +1,151 @@ +const logger = require('consola'); +const AccountService = require('../../account'); +const { login_cellphone, login_refresh, login } = require('NeteaseCloudMusicApi'); +const CookiePath = `${__dirname}/../../../../.profile/cookie/`; +const fs = require('fs'); + + +const LoginTypePhone = 'phone'; +const LoginTypeEmail = 'email'; + +const CookieMap = {}; + +async function requestApi(uid, moduleFunc, request = {}, cookieRequired = true) { + if (cookieRequired) { + let cookie = await getCookie(uid); + if (!cookie) { + logger.error(`uid(${uid}) get cookie failed`); + return false; + } + request.cookie = cookie; + } + + let response = await requestWyyApi(moduleFunc, request); + // need refresh + if (response && response.status == 301) { + cookie = await getCookie(uid, true); + if (!cookie) { + logger.error(`uid(${uid}) refresh cookie failed. request api abort`); + return false; + } + + // retry request + request.cookie = cookie; + response = await requestWyyApi(moduleFunc, request); + } + + if (response && response.status == 200) { + return response.body; + } + + logger.error(`requestWyyApi respond non 200, response: `, response); + return false; +} + +async function requestWyyApi(moduleFunc, request) { + return moduleFunc(request).then(response => { + return response; + }).catch(err => { + console.log(err) + + logger.error(`requestWyyApi failed: `, err); + if (typeof err == 'object' && err.status == '301') { + return err; + } + return false; + }); +} + + +async function getCookie(uid, refresh = false) { + const account = AccountService.getAccount(uid); + if (!account) { + return false; + } + + // fetch from cache + const cookieFromCache = fetchCookieFromCache(uid, account); + if (cookieFromCache) { + if (!refresh) { + return cookieFromCache; + } + + logger.info('refresh cookie...', cookieFromCache); + const response = await requestWyyApi(login_refresh, {cookie: cookieFromCache}); + if (response && response.status == 200 && response.cookie && response.cookie.length > 1) { + const cookie = response.cookie.map(line => line.replace('HTTPOnly', '')).join(';'); + logger.info('refresh cookie succeed, ', cookie); + storeCookie(uid, account, cookie); + return cookie; + } + logger.info(`refresh failed, try login again`); + } + + // login + logger.info(`uid(${uid}) login with ${account.countryCode} ${account.account} via ${account.loginType}`); + + let result; + if (account.loginType === LoginTypePhone) { + result = await requestWyyApi(login_cellphone, { + countrycode: account.countrycode, + phone: account.account, + password: account.password, + }); + } else if (account.loginType === LoginTypeEmail) { + result = await requestWyyApi(login, { + email: account.account, + password: account.password, + }); + } else { + if (account.loginType === 'qrcode') { + logger.error(`uid(${uid})'s loginType(${account.loginType}) does not support auto login, please login in the browser page first`); + } else { + logger.error(`uid(${uid})'s loginType(${account.loginType}) does not support now`); + } + return false; + } + + if (result && result.status == 200 && result.body && result.body.code == 200 && result.body.cookie) { + logger.info(`uid(${uid}) login succeed`) + storeCookie(uid, account, result.body.cookie); + return result.body.cookie; + } + logger.error(`fetch cookie from response failed, uid(${uid}) login failed`, result); + return false; +} + +function storeCookie(uid, account, cookie) { + fs.writeFileSync(getCookieFilePath(uid, account), cookie); + CookieMap[getCookieMapKey(uid, account)] = cookie; +} + +function fetchCookieFromCache(uid, account) { + const cacheKey = getCookieMapKey(uid, account); + if (CookieMap[cacheKey]) { + return CookieMap[cacheKey]; + } + const CookieFile = getCookieFilePath(uid, account); + + if (!fs.existsSync(CookieFile)) { + logger.info(`uid(${uid})'s cookie not found from .profile`); + return null; + } + + const cookie = fs.readFileSync(CookieFile).toString(); + CookieMap[cacheKey] = cookie; + + return cookie; +} + +function getCookieMapKey(uid, account) { + return `${uid}-${account.platform}-${account.account}`; +} + +function getCookieFilePath(uid, account) { + return `${CookiePath}${uid}-${account.platform}-${account.account}`; +} + +module.exports = { + requestApi, + storeCookie, +} \ No newline at end of file diff --git a/backend/src/service/remote_config/index.js b/backend/src/service/remote_config/index.js new file mode 100644 index 0000000..19786dd --- /dev/null +++ b/backend/src/service/remote_config/index.js @@ -0,0 +1,69 @@ +const httpsGet = require('../../utils/network').asyncHttpsGet; +const logger = require('consola'); +const configManager = require('../config_manager'); + +// Store best proxy in memory for performance +let cachedBestProxy = ''; + +async function validateGithubAccess(proxy = '') { + try { + const testUrl = proxy ? `${proxy}https://api.github.com/zen` : 'https://api.github.com/zen'; + const response = await httpsGet(testUrl); + return response !== null; + } catch (err) { + return false; + } +} + +async function findBestProxy(proxyList) { + // Always try direct access first + if (await validateGithubAccess()) { + cachedBestProxy = ''; + return ''; + } + + // Try cached proxy if available + if (cachedBestProxy && await validateGithubAccess(cachedBestProxy)) { + return cachedBestProxy; + } + + // Test each proxy in the list + for (const proxy of proxyList) { + if (proxy && await validateGithubAccess(proxy)) { + cachedBestProxy = proxy; + return proxy; + } + } + + logger.warn('No working GitHub access found, either direct or via proxy'); + return ''; // Return empty string if no working access found +} + +async function getRemoteConfig() { + const fallbackConfig = { + githubProxy: ['', 'https://ghp.ci/'], + } + + const remoteConfigUrl = 'https://foamzou.com/tools/melody-config.php?v=2'; + const remoteConfig = await httpsGet(remoteConfigUrl); + + let config = {}; + if (remoteConfig === null) { + config = fallbackConfig; + } else { + config = JSON.parse(remoteConfig); + } + + let bestGithubProxy = await findBestProxy(config.githubProxy); + if (bestGithubProxy !== '' && !bestGithubProxy.endsWith('/')) { + bestGithubProxy = bestGithubProxy + '/'; + } + + return { + bestGithubProxy, + } +} + +module.exports = { + getRemoteConfig, +} \ No newline at end of file diff --git a/backend/src/service/scheduler/index.js b/backend/src/service/scheduler/index.js new file mode 100644 index 0000000..ab8d376 --- /dev/null +++ b/backend/src/service/scheduler/index.js @@ -0,0 +1,221 @@ +const schedule = require('node-schedule'); +const logger = require('consola'); +const configManager = require('../config_manager'); +const AccountService = require('../account'); +const syncPlaylist = require('../sync_music/sync_playlist'); +const unblockMusicInPlaylist = require('../sync_music/unblock_music_in_playlist'); +const { consts: sourceConsts } = require('../../consts/source'); +const { getUserAllPlaylist, verifyAccountStatus } = require('../music_platform/wycloud'); + +class SchedulerService { + constructor() { + this.jobs = new Map(); + } + + async start() { + await this.scheduleLocalSyncJobs(); + await this.scheduleCloudSyncJobs(); + + // Log initial schedule info + const localNextRun = this.getLocalSyncNextRun(); + if (localNextRun) { + logger.info(`Next local sync scheduled at: ${localNextRun.nextRunTime}, in ${Math.round(localNextRun.remainingMs / 1000 / 60)} minutes`); + } + + const accounts = await AccountService.getAllAccounts(); + for (const uid in accounts) { + const cloudNextRun = this.getCloudSyncNextRun(uid); + if (cloudNextRun) { + logger.info(`Next cloud sync for account ${uid} scheduled at: ${cloudNextRun.nextRunTime}, in ${Math.round(cloudNextRun.remainingMs / 1000 / 60)} minutes`); + } + } + + logger.info('Scheduler service started'); + } + + async scheduleLocalSyncJobs() { + // 系统级别的本地同步任务 + const config = await configManager.getGlobalConfig(); + const syncAccounts = config.playlistSyncToLocal.syncAccounts || []; + if (!config.playlistSyncToLocal.autoSync.enable || syncAccounts.length === 0) { + return; + } + + const frequency = config.playlistSyncToLocal.autoSync.frequency; + const unit = config.playlistSyncToLocal.autoSync.frequencyUnit; + + const rule = this.buildScheduleRule(frequency, unit); + const jobKey = 'localSync'; + + const job = schedule.scheduleJob(rule, async () => { + logger.info('Start auto sync playlist to local'); + for (const uid of syncAccounts) { + const isActive = await verifyAccountStatus(uid); + if (!isActive) { + logger.warn(`Account ${uid} is not active, skip local sync`); + continue; + } + const playlists = await getUserAllPlaylist(uid); + for (const playlist of playlists) { + logger.info(`Start sync playlist ${playlist.id} to local for account ${uid}`); + await syncPlaylist(uid, sourceConsts.Netease.code, playlist.id); + } + } + }); + + this.jobs.set(jobKey, job); + + logger.info(`Schedule local sync job success, rule: ${this.formatScheduleRule(rule)}`); + } + + async scheduleCloudSyncJobs() { + // 账号级别的云盘同步任务 + const accounts = await AccountService.getAllAccounts(); + for (const uid in accounts) { + const account = accounts[uid]; + await this.scheduleCloudSyncJob(uid, account); + } + } + + async scheduleCloudSyncJob(uid, account) { + if (!account.config?.playlistSyncToWyCloudDisk?.autoSync?.enable) { + return; + } + + const isActive = await verifyAccountStatus(uid); + if (!isActive) { + logger.warn(`Account ${uid} is not active, skip cloud sync`); + return; + } + + const frequency = account.config.playlistSyncToWyCloudDisk.autoSync.frequency; + const unit = account.config.playlistSyncToWyCloudDisk.autoSync.frequencyUnit; + const jobKey = `cloudSync_${uid}`; + + const rule = this.buildScheduleRule(frequency, unit); + + this.jobs.set(jobKey, schedule.scheduleJob(rule, async () => { + logger.info(`Start cloud sync for account ${uid}`); + const playlists = await getUserAllPlaylist(uid); + // Filter playlists based on user preference + const playlistsToSync = account.config.playlistSyncToWyCloudDisk.autoSync.onlyCreatedPlaylists + ? playlists.filter(p => p.isCreatedByMe) + : playlists; + for (const playlist of playlistsToSync) { + logger.info(`Start sync playlist ${playlist.id} to cloud for account ${uid}`); + await unblockMusicInPlaylist(uid, sourceConsts.Netease.code, playlist.id, { + syncWySong: account.config.playlistSyncToWyCloudDisk.syncWySong, + syncNotWySong: account.config.playlistSyncToWyCloudDisk.syncNotWySong + }); + } + })); + logger.info(`Schedule cloud sync job for account ${uid} success, rule: ${this.formatScheduleRule(rule)}`); + } + + buildScheduleRule(frequency, unit) { + if (unit === 'minute') { + return `0 */${frequency} * * * *`; + } else if (unit === 'hour') { + return `0 0 */${frequency} * * *`; + } else { + return `0 0 0 */${frequency} * *`; + } + } + + formatScheduleRule(rule) { + if (typeof rule === 'string') { + return rule; + } + return `每天 ${rule.hour.map(h => h.toString().padStart(2, '0') + ':00').join(', ')} 执行`; + } + + async updateLocalSyncJob() { + logger.info('Update local sync job'); + // 添加调试日志 + logger.info('Before update:'); + logger.info(`- Jobs map size: ${this.jobs.size}`); + logger.info(`- Jobs keys: ${Array.from(this.jobs.keys()).join(', ')}`); + + const localJob = this.jobs.get('localSync'); + if (localJob) { + localJob.cancel(); + this.jobs.delete('localSync'); + } + + // 添加调试日志 + logger.info('After cancel:'); + logger.info(`- Jobs map size: ${this.jobs.size}`); + logger.info(`- Jobs keys: ${Array.from(this.jobs.keys()).join(', ')}`); + + await this.scheduleLocalSyncJobs(); + + // 添加调试日志 + logger.info('After reschedule:'); + logger.info(`- Jobs map size: ${this.jobs.size}`); + logger.info(`- Jobs keys: ${Array.from(this.jobs.keys()).join(', ')}`); + const newJob = this.jobs.get('localSync'); + logger.info(`- New job created: ${!!newJob}`); + if (newJob) { + logger.info(`- New job next run: ${newJob.nextInvocation()}`); + } + + logger.info('Update local sync job success'); + } + + async updateCloudSyncJob(uid) { + logger.info(`Update cloud sync job for account ${uid}`); + // 取消指定账号的云盘同步任务 + const cloudJobKey = `cloudSync_${uid}`; + const cloudJob = this.jobs.get(cloudJobKey); + if (cloudJob) { + cloudJob.cancel(); + this.jobs.delete(cloudJobKey); + logger.info(`Cancel cloud sync job for account ${uid}`); + } + // 重新调度指定账号的云盘同步任务 + const account = (await AccountService.getAllAccounts())[uid]; + if (account) { + await this.scheduleCloudSyncJob(uid, account); + } + logger.info(`Update cloud sync job for account ${uid} success`); + } + + getNextRunInfo(job) { + if (!job) return null; + + const nextRun = job.nextInvocation(); + if (!nextRun) return null; + + const now = new Date(); + const remainingMs = nextRun.getTime() - now.getTime(); + + return { + nextRunTime: nextRun, + remainingMs: remainingMs + }; + } + + getLocalSyncNextRun() { + const job = this.jobs.get('localSync'); + + // 添加调试日志 + logger.info('Debug getLocalSyncNextRun:'); + logger.info(`- Has job: ${!!job}`); + logger.info(`- Jobs map size: ${this.jobs.size}`); + logger.info(`- Jobs keys: ${Array.from(this.jobs.keys()).join(', ')}`); + if (job) { + logger.info(`- Job next invocation: ${job.nextInvocation()}`); + logger.info(`- Job scheduling info:`, job.scheduledJobs); + } + + return this.getNextRunInfo(job); + } + + getCloudSyncNextRun(uid) { + const job = this.jobs.get(`cloudSync_${uid}`); + return this.getNextRunInfo(job); + } +} + +const schedulerService = new SchedulerService(); +module.exports = schedulerService; \ No newline at end of file diff --git a/backend/src/service/search_songs/find_the_best_match_from_wycloud.js b/backend/src/service/search_songs/find_the_best_match_from_wycloud.js new file mode 100644 index 0000000..638aa97 --- /dev/null +++ b/backend/src/service/search_songs/find_the_best_match_from_wycloud.js @@ -0,0 +1,57 @@ +const { searchSong, getSongInfo } = require('../music_platform/wycloud'); +const logger = require('consola'); + +module.exports = async function findTheBestMatchFromWyCloud(uid, {songName, artist, album, musicPlatformSongId} = {}) { + if (musicPlatformSongId) { + const songInfo = await getSongInfo(uid, musicPlatformSongId); + + if (songInfo) { + return songInfo; + } + + if (songName && artist) { + return { + songId: musicPlatformSongId, + songName, + artists: [artist], + album, + }; + } + + return null; + } + + if (songName === "" || artist === "") { + return null; + } + const searchLists = await searchSong(uid, songName, artist); + logger.info('searchLists', searchLists); + if (searchLists === false) { + logger.warn(`search song failed, no matter, go on`); + return null; + } + + let matchSongAndArtist = null; + for (const searchItem of searchLists) { + let hitArtist = false; + for (const searchArtist of searchItem.artists) { + if (artist === searchArtist) { + hitArtist = true; + } + } + if (!hitArtist) { + continue; + } + + if (searchItem.songName === songName) { + if (searchItem.album === album) { + logger.info('matched the best') + return searchItem; + } + if (!matchSongAndArtist) { + matchSongAndArtist = searchItem; + } + } + } + return matchSongAndArtist; +} diff --git a/backend/src/service/search_songs/index.js b/backend/src/service/search_songs/index.js new file mode 100644 index 0000000..2eaf606 --- /dev/null +++ b/backend/src/service/search_songs/index.js @@ -0,0 +1,19 @@ +const { searchSongFromAllPlatform } = require('../media_fetcher'); +const searchSongsWithSongMeta = require('./search_songs_with_song_meta'); +const findTheBestMatchFromWyCloud = require('./find_the_best_match_from_wycloud'); + +async function searchSongsWithKeyword(keyword) { + const searchList = await searchSongFromAllPlatform({keyword}); + if (searchList === false || searchList.length === 0) { + return []; + } + + return searchList; +} + + +module.exports = { + searchSongsWithSongMeta: searchSongsWithSongMeta, + searchSongsWithKeyword: searchSongsWithKeyword, + findTheBestMatchFromWyCloud: findTheBestMatchFromWyCloud, +} \ No newline at end of file diff --git a/backend/src/service/search_songs/search_songs_with_song_meta.js b/backend/src/service/search_songs/search_songs_with_song_meta.js new file mode 100644 index 0000000..00c9d8a --- /dev/null +++ b/backend/src/service/search_songs/search_songs_with_song_meta.js @@ -0,0 +1,131 @@ +const logger = require('consola'); +const { searchSongFromAllPlatform } = require('../media_fetcher'); + +module.exports = async function searchSongsWithSongMeta(songMeta = { + songName: '', + artist: '', + album: '', + duration: 0, +}, options = { + expectArtistAkas: [], // 歌手名字,有的歌手有很多别名的,给出这些信息能够更好地排序 + allowSongsJustMatchDuration: false, // 关键信息不对的情况下,但 duration 很接近的歌曲,是否希望返回 + allowSongsNotMatchMeta: false, // 关键的 meta 信息不匹配的歌曲,是否希望返回 +}) { + // search song with the meta + const searchList = await searchSongFromAllPlatform({ + songName:songMeta.songName, + artist: songMeta.artist, + album: songMeta.album + }); + if (searchList === false || searchList.length === 0) { + logger.error(`search song failed, songMeta: ${JSON.stringify(songMeta)}`); + return false; + } + + return sortOutTheSearchList(searchList, options.expectArtistAkas, { + songName: songMeta.songName, + duration: songMeta.duration, + }, { + allowSongsJustMatchDuration: options.allowSongsJustMatchDuration, + allowSongsNotMatchMeta: options.allowSongsNotMatchMeta, + }); +} + + +function sortOutTheSearchList(searchList, expectArtistAkas, songMeta = { + songName: '', + duration: 0, +}, options = { + allowSongsJustMatchDuration: false, + allowSongsNotMatchMeta: false, +}) { + let searchListfilttered = []; + let searchListfiltterJustWithDuration = []; + let searchListNotMatchMeta = []; + + // filter with song name, artist first + for (const searchItem of searchList) { + if (searchItem.cannotDownload) { + logger.info(`song cannot download, continue. searchItem: ${JSON.stringify(searchItem)}`); + searchListNotMatchMeta.push(searchItem); + continue; + } + + const hasDuration = songMeta.duration > 0 && searchItem.duration > 0; + const durationDiff = hasDuration ? Math.abs(searchItem.duration - songMeta.duration) : 0; + searchItem.durationDiff = durationDiff; + + if (hasDuration && durationDiff > 10) { + searchListNotMatchMeta.push(searchItem); + continue; + } + + if (thereAreWordNotExistFromInputButInSearchResult(['cover', '伴奏', '翻唱', 'instrumental'], searchItem.songName, songMeta.songName)) { + logger.info(`there are word not exist from input but in search result, continue. searchItem.songName: ${searchItem.songName}, songMeta.songName: ${songMeta.songName}`); + searchListNotMatchMeta.push(searchItem); + continue; + } + + if (hasDuration && durationDiff <= 5) { + searchListfiltterJustWithDuration.push(searchItem); + searchListNotMatchMeta.push(searchItem); + } + + if (searchItem.songName.replace(' ', '').indexOf(songMeta.songName.replace(' ', '')) === -1) { + logger.info(`songName not matched, continue. ${searchItem.songName} vs ${songMeta.songName}`); + searchListNotMatchMeta.push(searchItem); + continue; + } + + if (searchItem.fromMusicPlatform && expectArtistAkas.length > 0) { + logger.info(`should find the artist:${searchItem.artist} from ${expectArtistAkas.join(',')}`); + if (!expectArtistAkas.find(artist => artist === searchItem.artist)) { + logger.info(`artist not matched, continue.`); + searchListNotMatchMeta.push(searchItem); + continue; + } + } + + searchListfilttered.push(searchItem); + } + if (options.allowSongsJustMatchDuration) { + searchListfiltterJustWithDuration = searchListfiltterJustWithDuration.sort((a, b) => a.durationDiff - b.durationDiff); + searchListfilttered.push(...searchListfiltterJustWithDuration); + } + if (options.allowSongsNotMatchMeta) { + searchListfilttered.push(...searchListNotMatchMeta); + } + + // uniq with song url + const uniqedSearchList = []; + for (const searchItem of searchListfilttered) { + if (uniqedSearchList.find(item => item.url === searchItem.url)) { + continue; + } + uniqedSearchList.push(searchItem); + } + + // stable sort。 resourceForbidden 排在后面 + return uniqedSearchList.map((data, i) => { + return {i, data} + }).sort((a,b)=>{ + if (a.data.resourceForbidden == b.data.resourceForbidden) { + return a.i-b.i; + } if (a.data.resourceForbidden) { + return 1; + } + return -1 + }).map(d=> d.data) +} + + +function thereAreWordNotExistFromInputButInSearchResult(words, searchResultWord, inputWord) { + searchResultWord = searchResultWord.toLowerCase(); + inputWord = inputWord.toLowerCase(); + for (const word of words) { + if (searchResultWord.indexOf(word) !== -1 && inputWord.indexOf(word) === -1) { + return true; + } + } + return false; +} diff --git a/backend/src/service/songs_info/index.js b/backend/src/service/songs_info/index.js new file mode 100644 index 0000000..b8c6627 --- /dev/null +++ b/backend/src/service/songs_info/index.js @@ -0,0 +1,14 @@ +const { getPlayUrl } = require('../music_platform/wycloud'); + +async function getPlayUrlWithOptions(uid, source, songId) { + // Only support netease now + if (source !== 'netease') { + return ''; + } + return await getPlayUrl(uid, songId); +} + + +module.exports = { + getPlayUrlWithOptions: getPlayUrlWithOptions, +} \ No newline at end of file diff --git a/backend/src/service/sync_music/download_to_local.js b/backend/src/service/sync_music/download_to_local.js new file mode 100644 index 0000000..9cdc2ad --- /dev/null +++ b/backend/src/service/sync_music/download_to_local.js @@ -0,0 +1,64 @@ +const { fetchWithUrl, getMetaWithUrl } = require('../media_fetcher'); +const { uploadSong, searchSong, matchAndFixCloudSong } = require('../music_platform/wycloud'); +const logger = require('consola'); +const sleep = require('../../utils/sleep'); +const configManager = require('../config_manager'); +const fs = require('fs'); +const libPath = require('path'); +const utilFs = require('../../utils/fs'); + + +module.exports = { + downloadFromLocalTmpPath: downloadFromLocalTmpPath, + buildDestFilename: buildDestFilename, +} + +async function downloadFromLocalTmpPath(tmpPath, songInfo = { + songName: "", + artist: "", + album: "", +}, playlistName = '', collectResponse) { + const globalConfig = (await configManager.getGlobalConfig()); + const downloadPath = globalConfig.downloadPath; + if (!downloadPath) { + logger.error(`download path not set`); + return "IOFailed"; + } + const destPathAndFilename = buildDestFilename(globalConfig, songInfo, playlistName); + const destPath = libPath.dirname(destPathAndFilename); + // make sure the path is exist + await utilFs.asyncMkdir(destPath, {recursive: true}); + try { + if (await utilFs.asyncFileExisted(destPathAndFilename)) { + logger.info(`file already exists, remove it: ${destPathAndFilename}`); + await utilFs.asyncUnlinkFile(destPathAndFilename) + } + await utilFs.asyncMoveFile(tmpPath, destPathAndFilename); + } catch (err) { + logger.error(`move file failed, ${tmpPath} -> ${destPathAndFilename}`, err); + return "IOFailed"; + } + if (collectResponse !== undefined) { + try { + const md5Value = await utilFs.asyncMd5(destPathAndFilename); + collectResponse['md5Value'] = md5Value; + } catch (err) { + logger.error(`md5 failed, ${destPathAndFilename}`, err); + // don't return false, just log it + } + } + logger.info(`download song success, path: ${destPathAndFilename}`); + return true; +} + +function buildDestFilename(globalConfig, songInfo, playlistName) { + const downloadPath = globalConfig.downloadPath; + let filename = (playlistName ? globalConfig.playlistSyncToLocal?.filenameFormat : globalConfig.filenameFormat) + .replace(/{artist}/g, songInfo.artist ? songInfo.artist : 'Unknown') + .replace(/{songName}/g, songInfo.songName ? songInfo.songName : 'Unknown') + .replace(/{playlistName}/g, playlistName ? playlistName : 'UnknownPlayList') + .replace(/{album}/g, songInfo.album ? songInfo.album : 'Unknown'); + // remove the head / and \ in filename + filename = filename.replace(/^[\/\\]+/, '') + '.mp3'; + return `${downloadPath}${libPath.sep}${filename}` +} \ No newline at end of file diff --git a/backend/src/service/sync_music/index.js b/backend/src/service/sync_music/index.js new file mode 100644 index 0000000..c7c3f5a --- /dev/null +++ b/backend/src/service/sync_music/index.js @@ -0,0 +1,11 @@ +const syncSingleSongWithUrl = require('./sync_single_song_with_url'); +const unblockMusicInPlaylist = require('./unblock_music_in_playlist'); +const unblockMusicWithSongId = require('./unblock_music_with_song_id'); +const syncPlaylist = require('./sync_playlist'); + +module.exports = { + syncSingleSongWithUrl: syncSingleSongWithUrl, + unblockMusicInPlaylist: unblockMusicInPlaylist, + unblockMusicWithSongId: unblockMusicWithSongId, + syncPlaylist: syncPlaylist, +}; \ No newline at end of file diff --git a/backend/src/service/sync_music/sync_playlist.js b/backend/src/service/sync_music/sync_playlist.js new file mode 100644 index 0000000..82fde10 --- /dev/null +++ b/backend/src/service/sync_music/sync_playlist.js @@ -0,0 +1,382 @@ +const { + getSongsFromPlaylist, + getPlayUrl, +} = require("../music_platform/wycloud"); +const syncSingleSongWithUrl = require("./sync_single_song_with_url"); +const logger = require("consola"); +const { + findTheBestMatchFromWyCloud, + searchSongsWithSongMeta, +} = require("../search_songs"); +const JobManager = require("../job_manager"); +const JobType = require("../../consts/job_type"); +const JobStatus = require("../../consts/job_status"); +const BusinessCode = require("../../consts/business_code"); +const { downloadViaSourceUrl } = require("../media_fetcher/index"); +const { + downloadFromLocalTmpPath, + buildDestFilename, +} = require("./download_to_local"); +const KV = require("../kv"); +const utilFs = require("../../utils/fs"); +const configManager = require("../config_manager"); +const path = require("path"); +const { consts } = require("../../consts/source"); +const soundQualityConst = require("../../consts/sound_quality"); + +module.exports = async function syncPlaylist(uid, source, playlistId) { + // step 1. get all the songs + const playlistInfo = await getSongsFromPlaylist(uid, source, playlistId); + if (playlistInfo === false) { + return false; + } + + // calc the songs need to be synced + const songsNeedToSync = []; + const cacheForCalcSongsNeedToSync = {}; + + // 如果开启了删除功能,先执行删除 + const globalConfig = await configManager.getGlobalConfig(); + if (globalConfig.playlistSyncToLocal.deleteLocalFile) { + const currentSongIDs = playlistInfo.songs.map(song => song.songId); + await syncDeleteFiles(playlistId, currentSongIDs); + } + + for (const song of playlistInfo.songs) { + const needToSync = await isNeedToSyncFile(playlistId, song.songId, cacheForCalcSongsNeedToSync); + if (!needToSync) { + continue; + } + songsNeedToSync.push(song); + } + + if (songsNeedToSync.length === 0) { + logger.info(`[No need] all the songs in the playlist are already downloaded.`); + return BusinessCode.StatusJobNoNeedToCreate; + } + + // create job + const args = `syncPlaylist: {"source":${source},"playlistId":${playlistId}}`; + if (await JobManager.findActiveJobByArgs(uid, args)) { + logger.info(`syncPlaylist job is already running.`); + return BusinessCode.StatusJobAlreadyExisted; + } + const jobId = await JobManager.createJob(uid, { + name: `下载歌单到本地服务器:${playlistInfo.name}`, + args, + type: JobType.SyncThePlaylistToLocalService, + status: JobStatus.Pending, + desc: `有${songsNeedToSync.length}首歌曲需要下载`, + progress: 0, + tip: "等待下载", + createdAt: Date.now(), + }); + + // async do the job + (async () => { + const songs = songsNeedToSync; + logger.info(`${jobId}: try to sync songs: ${playlistInfo.name}`); + await JobManager.updateJob(uid, jobId, { + status: JobStatus.InProgress, + }); + const succeedList = []; + const failedList = []; + // step 2. download the songs + for (const song of songs) { + let tip = `[${succeedList.length + failedList.length + 1}/${ + songs.length + }] 正在下载歌曲:${song.songName}`; + await JobManager.updateJob(uid, jobId, { + tip, + }); + const syncSucceed = await syncSingleSong(uid, song, playlistInfo); + if (syncSucceed) { + await JobManager.updateJob(uid, jobId, { + log: song.songName + ": 下载成功", + }); + succeedList.push({ songName: song.songName, artist: song.artists[0] }); + } else { + await JobManager.updateJob(uid, jobId, { + log: song.songName + ": 下载失败", + }); + failedList.push({ songName: song.songName, artist: song.artists[0] }); + } + await JobManager.updateJob(uid, jobId, { + progress: (succeedList.length + failedList.length) / songs.length, + }); + } + + let tip = `任务完成,成功${succeedList.length}首,失败${failedList.length}首`; + await JobManager.updateJob(uid, jobId, { + progress: 1, + status: succeedList.length > 0 ? JobStatus.Finished : JobStatus.Failed, + tip, + data: { + succeedList, + failedList, + }, + }); + })().catch(async (e) => { + logger.error(`${jobId}: ${e}`); + let tip = "遇到不可思议的错误了哦,任务终止"; + await JobManager.updateJob(uid, jobId, { + status: JobStatus.Failed, + tip, + }); + }); + + return jobId; +}; + +async function syncSingleSong(uid, wySongMeta, playlistInfo) { + const playlistName = playlistInfo.name; + const playlistID = playlistInfo.id; + logger.info(`sync single song with meta: ${JSON.stringify(wySongMeta)}`); + const globalConfig = await configManager.getGlobalConfig(); + let isLossless = false; + if (globalConfig.playlistSyncToLocal.soundQualityPreference === soundQualityConst.Lossless) { + isLossless = true; + } + // 优先使用官方资源下载 + const playUrl = await getPlayUrl(uid, wySongMeta.songId, isLossless); + if (playUrl) { + const tmpPath = await downloadViaSourceUrl(playUrl); + if (tmpPath) { + const collectRet = {}; + const ret = await downloadFromLocalTmpPath( + tmpPath, + wySongMeta, + playlistName, + collectRet + ); + if (ret === true) { + logger.info(`download from official succeed`, wySongMeta); + if (collectRet.md5Value) { + await recordSongIndex(playlistID, wySongMeta.songId, wySongMeta, playlistName, collectRet.md5Value); + } + return true; + } + } + } + + // 从公开资源获取 + logger.info( + `download from official failed, try to download from public resources`, + wySongMeta + ); + + const songFromWyCloud = await findTheBestMatchFromWyCloud(uid, { + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + musicPlatformSongId: wySongMeta.songId, + }); + // search songs with the meta + const searchListfilttered = await searchSongsWithSongMeta( + { + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + duration: wySongMeta.duration, + }, + { + expectArtistAkas: songFromWyCloud.artists ? songFromWyCloud.artists : [], + allowSongsJustMatchDuration: false, + allowSongsNotMatchMeta: false, + } + ); + + logger.info( + `use the searchListfilttered: ${JSON.stringify(searchListfilttered)}` + ); + if (searchListfilttered === false) { + return false; + } + + // find the best match song + for (const searchItem of searchListfilttered) { + logger.info(`try to the search item: ${JSON.stringify(searchItem)}`); + + const collectRet = {}; + const isSucceed = await syncSingleSongWithUrl( + uid, + searchItem.url, + { + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + songFromWyCloud, + }, + 0, + JobType.SyncThePlaylistToLocalService, + playlistName, + collectRet + ); + if (isSucceed === "IOFailed") { + logger.error(`not try others due to upload failed.`); + return false; + } + if (isSucceed) { + if (collectRet.md5Value) { + await recordSongIndex(playlistID, wySongMeta.songId, wySongMeta, playlistName, collectRet.md5Value); + } + return true; + } + } + return false; +} + +const SourceWYPlaylist = "wycloudPlaylist"; +function recordFileIndex(playlistID, songID, songInfo, playlistName, md5Value) { + const sourceID = `${playlistID}_${songID}`; + // no need to await to save time + KV.fileSyncMeta.set(SourceWYPlaylist, sourceID, { + songInfo, + playlistName, + md5Value, + createTime: Date.now(), + }); +} + +async function getRecordFileIndex(playlistID, songID) { + const sourceID = `${playlistID}_${songID}`; + return await KV.fileSyncMeta.get(SourceWYPlaylist, sourceID); +} + +async function isNeedToSyncFile(playlistID, songID, cache) { + const record = await getRecordFileIndex(playlistID, songID); + if (!record) { + // logger.info(`no record for ${playlistID}_${songID}, need to sync`); + return true; + } + + // use the latest setting to rebuild the dest filename + // then check: + // 1. if the file exists(don't check the md5), skip + // 2. if the file not exists, check if there is a same file under the destFile's dir with the same md5, skip + + const globalConfig = await configManager.getGlobalConfig(); + const destFilename = buildDestFilename( + globalConfig, + record.songInfo, + record.playlistName + ); + + if (await utilFs.asyncFileExisted(destFilename)) { + // logger.info(`file already exists, skip: ${destFilename}`); + return false; + } + + try { + const dir = path.dirname(destFilename); + const files = await (async () => { + if (cache[`dir_files_${dir}`]) { + return cache[`dir_files_${dir}`]; + } + const files = await utilFs.asyncReadDir(dir); + cache[`dir_files_${dir}`] = files; + return files; + })(); + for (const file of files) { + const filename = path.join(dir, file); + const md5Value = await (async () => { + if (cache[`md5_${filename}`]) { + return cache[`md5_${filename}`]; + } + const md5Value = await utilFs.asyncMd5(filename); + cache[`md5_${filename}`] = md5Value; + return md5Value; + })() + if (md5Value === record.md5Value) { + // logger.info(`file already exists with the same md5, skip: ${filename}`); + return false; + } + } + // logger.info(`no file with the same md5, need to sync: ${destFilename}`); + return true; + } catch (e) { + logger.error(e); + // logger.info(`error when check the file, need to sync: ${destFilename}`); + return true; + } +} + +async function recordSongIndex(playlistID, songID, songInfo, playlistName, md5Value) { + try { + // 记录单曲信息 + recordFileIndex(playlistID, songID, songInfo, playlistName, md5Value); + + // 更新歌单元数据 + const playlistMeta = await KV.fileSyncMeta.getPlaylistMeta(playlistID); + const songIDs = new Set(playlistMeta.songIDs || []); + songIDs.add(songID); + await KV.fileSyncMeta.setPlaylistMeta(playlistID, { + songIDs: Array.from(songIDs) + }); + + logger.info(`Updated playlist meta for song: ${songInfo.songName}`); + } catch (err) { + logger.error(`Failed to record song index: ${songInfo.songName}`, err); + // 不抛出错误,避免影响主流程 + } +} + +async function syncDeleteFiles(playlistId, currentSongIDs) { + try { + // 获取本地已下载的歌曲记录 + const playlistMeta = await KV.fileSyncMeta.getPlaylistMeta(playlistId); + const localSongIDs = playlistMeta.songIDs || []; + + // 找出需要删除的歌曲(在本地但不在云端的) + const needDeleteSongIDs = localSongIDs.filter(id => !currentSongIDs.includes(id)); + + if (needDeleteSongIDs.length === 0) { + logger.info(`No songs need to be deleted for playlist: ${playlistId}`); + return; + } + + logger.info(`Found ${needDeleteSongIDs.length} songs to delete for playlist: ${playlistId}`); + + // 记录删除结果 + const deletedSongIDs = new Set(); + + for (const songID of needDeleteSongIDs) { + const record = await getRecordFileIndex(playlistId, songID); + if (!record) { + logger.warn(`No record found for song: ${songID}`); + continue; + } + + const globalConfig = await configManager.getGlobalConfig(); + const destFilename = buildDestFilename(globalConfig, record.songInfo, record.playlistName); + + try { + if (await utilFs.asyncFileExisted(destFilename)) { + await utilFs.asyncUnlinkFile(destFilename); + logger.info(`Deleted file: ${destFilename}`); + } else { + logger.warn(`File not found: ${destFilename}`); + } + + // 删除单曲记录 + const sourceID = `${playlistId}_${songID}`; + await KV.fileSyncMeta.set(SourceWYPlaylist, sourceID, null); + + deletedSongIDs.add(songID); + logger.info(`Deleted record for song: ${record.songInfo.songName}`); + } catch (err) { + logger.error(`Failed to delete song: ${record.songInfo.songName}`, err); + } + } + + // 更新歌单元数据 + const remainingSongIDs = localSongIDs.filter(id => !deletedSongIDs.has(id)); + await KV.fileSyncMeta.setPlaylistMeta(playlistId, { + songIDs: remainingSongIDs + }); + + logger.info(`Successfully deleted ${deletedSongIDs.size} songs from playlist: ${playlistId}`); + } catch (err) { + logger.error(`Failed to sync delete files for playlist: ${playlistId}`, err); + } +} diff --git a/backend/src/service/sync_music/sync_single_song_with_url.js b/backend/src/service/sync_music/sync_single_song_with_url.js new file mode 100644 index 0000000..60d7d2f --- /dev/null +++ b/backend/src/service/sync_music/sync_single_song_with_url.js @@ -0,0 +1,83 @@ +const { fetchWithUrl, getMetaWithUrl } = require('../media_fetcher'); +const logger = require('consola'); +const sleep = require('../../utils/sleep'); +const findTheBestMatchFromWyCloud = require('../search_songs/find_the_best_match_from_wycloud'); +const JobManager = require('../job_manager'); +const JobStatus = require('../../consts/job_status'); +const JobType = require('../../consts/job_type'); +const configManager = require('../config_manager'); +const fs = require('fs'); +const libPath = require('path'); +const utilFs = require('../../utils/fs'); +const { downloadFromLocalTmpPath } = require('./download_to_local'); +const uploadWithRetryThenMatch = require('./upload_to_wycloud_disk_with_retry_then_match'); + +module.exports = async function syncSingleSongWithUrl(uid, url, { + songName = "", + artist = "", + album = "", + songFromWyCloud = null +} = {}, jobId = 0, jobType = JobType.SyncSongFromUrl, playlistName = "", collectRet) { + // step 1. fetch song info + const songInfo = await getMetaWithUrl(url); + logger.info(songInfo); + if (songInfo === false || songInfo.isTrial) { + logger.error(`fetch song info failed or it's a trial song. ${JSON.stringify(songInfo)}`); + return false; + } + + await updateJobIfNeed(uid, jobId, songInfo, jobType); + + // step 2. find the best match from wycloud + if (songFromWyCloud === null) { + let findSongName, findArtist, findAlbum; + if (songName !== "" && artist !== "") { + logger.info(`use the user input song name and artist, ${songName}, ${artist}, ${album}`); + findSongName = songName; + findArtist = artist; + findAlbum = album; + } else if (songInfo.fromMusicPlatform) { + findSongName = songInfo.songName; + findArtist = songInfo.artist; + findAlbum = songInfo.album; + } + songFromWyCloud = await findTheBestMatchFromWyCloud(uid, { + songName: findSongName, + artist: findArtist, + album: findAlbum, + }); + } else { + logger.info(`use the songFromWyCloud by params`); + } + + logger.info('songFromWyCloud:', songFromWyCloud); + + // step 3. download + // should add meta tag if not matched song on wycloud + const path = await fetchWithUrl(url, {songName: songInfo.songName, addMediaTag: songFromWyCloud ? false : true}); + if (path === false) { + return false; + } + + // step 4. upload or download + logger.info(`handle song start: ${path}`); + + if (jobType === JobType.DownloadSongFromUrl || jobType === JobType.SyncThePlaylistToLocalService) { + return await downloadFromLocalTmpPath(path, songInfo, playlistName, collectRet); + } else { + return await uploadWithRetryThenMatch(uid, path, songInfo, songFromWyCloud); + } +} + +async function updateJobIfNeed(uid, jobId, songInfo, jobType) { + if (!jobId) { + return; + } + const operation = jobType === JobType.SyncSongFromUrl ? "上传" : "下载"; + await JobManager.updateJob(uid, jobId, { + name: `${operation}歌曲:${songInfo.songName}`, + status: JobStatus.InProgress, + desc: `歌曲: ${songInfo.songName}`, + tip: "任务开始", + }); +} \ No newline at end of file diff --git a/backend/src/service/sync_music/unblock_music_in_playlist.js b/backend/src/service/sync_music/unblock_music_in_playlist.js new file mode 100644 index 0000000..3acccf0 --- /dev/null +++ b/backend/src/service/sync_music/unblock_music_in_playlist.js @@ -0,0 +1,205 @@ +const { getSongsFromPlaylist, getPlayUrl } = require('../music_platform/wycloud'); +const syncSingleSongWithUrl = require('./sync_single_song_with_url'); +const logger = require('consola'); +const { findTheBestMatchFromWyCloud, searchSongsWithSongMeta } = require('../search_songs'); +const JobManager = require('../job_manager'); +const JobType = require('../../consts/job_type'); +const JobStatus = require('../../consts/job_status'); +const SoundQuality = require('../../consts/sound_quality'); +const BusinessCode = require('../../consts/business_code'); +const AccountService = require('../account'); +const { downloadViaSourceUrl } = require("../media_fetcher/index"); +const uploadWithRetryThenMatch = require('./upload_to_wycloud_disk_with_retry_then_match'); +const asyncFS = require('../../utils/fs'); + +// scope: +// 1. for not wy song: download from network then upload to cloud disk +// 2. for wy song: download from wy then upload to cloud disk. (i.e. backup wy song to cloud disk) +module.exports = async function unblockMusicInPlaylist(uid, source, playlistId, options = { + syncWySong: false, + syncNotWySong: false, + asyncExecute: true, +}) { + // step 1. get songs + const songsInfo = await getSongsFromPlaylist(uid, source, playlistId); + if (songsInfo === false) { + return false; + } + + if (songsInfo.songs.length === 0) { + return false; + } + + const songsNeedToSync = []; + songsInfo.songs.forEach(song => { + if (song.isCloud) { + return + } + // block song + if (song.isBlocked) { + if (options.syncNotWySong) { + songsNeedToSync.push(song); + } + } else { + // wy song + if (options.syncWySong) { + songsNeedToSync.push(song); + } + } + }); + if (songsNeedToSync.length === 0) { + return BusinessCode.StatusNoNeedToSync; + } + + // create job + const args = `unblockMusicInPlaylist: {"source":${source},"playlistId":${playlistId}}`; + if (await JobManager.findActiveJobByArgs(uid, args)) { + logger.info(`unblock music in playlist job is already running.`); + return BusinessCode.StatusJobAlreadyExisted; + } + const jobId = await JobManager.createJob(uid, { + name: `解锁歌单:${songsInfo.name}`, + args, + type: JobType.UnblockedPlaylist, + status: JobStatus.Pending, + desc: `有${songsNeedToSync.length}首歌曲需要解锁`, + progress: 0, + tip: "等待解锁", + createdAt: Date.now() + }); + + // async do the job + const job = (async () => { + const songs = songsNeedToSync; + logger.info(`${jobId}: try to unblock songs: ${JSON.stringify(songs)}`); + await JobManager.updateJob(uid, jobId, { + status: JobStatus.InProgress, + }); + const succeedList = []; + const failedList = []; + // step 2. download the songs and upload to cloud + for (const song of songs) { + let tip = `[${(succeedList.length + failedList.length + 1)}/${songs.length}] 正在解锁歌曲:${song.songName}`; + await JobManager.updateJob(uid, jobId, { + tip, + }); + const syncSucceed = await syncSingleSongWithMeta(uid, song); + if (syncSucceed) { + await JobManager.updateJob(uid, jobId, { + log: song.songName + ": 解锁成功", + }); + succeedList.push({songName: song.songName, artist: song.artists[0]}); + } else { + await JobManager.updateJob(uid, jobId, { + log: song.songName + ": 解锁失败", + }); + failedList.push({songName: song.songName, artist: song.artists[0]}); + } + await JobManager.updateJob(uid, jobId, { + progress: (succeedList.length + failedList.length) / songs.length, + }); + } + + let tip = `任务完成,成功${succeedList.length}首,失败${failedList.length}首`; + await JobManager.updateJob(uid, jobId, { + progress: 1, + status: succeedList.length > 0 ? JobStatus.Finished : JobStatus.Failed, + tip, + data: { + succeedList, + failedList, + } + }); + })().catch(async e => { + logger.error(`${jobId}: ${e}`); + let tip = '遇到不可思议的错误了哦,任务终止'; + await JobManager.updateJob(uid, jobId, { + status: JobStatus.Failed, + tip, + }); + }); + + // For sync execution, wait for job completion + if (!options.asyncExecute) { + await job; + } + + return jobId; +} + +async function syncSingleSongWithMeta(uid, wySongMeta) { + logger.info(`sync single song with meta: ${JSON.stringify(wySongMeta)}`); + // 获取 wycloud 的歌曲信息,有 id 就直接 get,没有就 search meta 选一个最匹配的 + const songFromWyCloud = await findTheBestMatchFromWyCloud(uid, { + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + musicPlatformSongId: wySongMeta.songId, + }); + + // Case 1: download the song from wy + if (!wySongMeta.isBlocked) { + const account = AccountService.getAccount(uid); + const playUrl = await getPlayUrl(uid, wySongMeta.songId, account.config.playlistSyncToWyCloudDisk.soundQualityPreference === SoundQuality.Lossless); + // if the playUrl is empty, we think the song is block as well. go through the search process + if (playUrl) { + const tmpPath = await downloadViaSourceUrl(playUrl); + // if download failed, we think due to network issue, just return false. It will retry in the next time + if (tmpPath === false) { + return false; + } + + // add some magic + try { + await asyncFS.asyncAppendFile(tmpPath, '00000'); + } catch (e) { + logger.error(`append file failed: ${tmpPath}`); + // 追加失败,可以继续 + } + + const isSucceed = await uploadWithRetryThenMatch(uid, tmpPath, null, songFromWyCloud); + + if (isSucceed === true) { + return true; + } + return false; + } + } + + // Case 2: search songs with the meta in the internet then upload to cloud + const searchListfilttered = await searchSongsWithSongMeta({ + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + duration: wySongMeta.duration, + }, { + expectArtistAkas: songFromWyCloud.artists ? songFromWyCloud.artists : [], + allowSongsJustMatchDuration: false, + allowSongsNotMatchMeta: false, + }); + + logger.info(`use the searchListfilttered: ${JSON.stringify(searchListfilttered)}`); + if (searchListfilttered === false) { + return false; + } + + // find the best match song + for (const searchItem of searchListfilttered) { + logger.info(`try to the search item: ${JSON.stringify(searchItem)}`); + + const isUploadSucceed = await syncSingleSongWithUrl(uid, searchItem.url, { + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + songFromWyCloud, + }); + if (isUploadSucceed === "IOFailed") { + logger.error(`not try others due to upload failed.`); + return false; + } + if (isUploadSucceed) { + return true; + } + } + return false; +} diff --git a/backend/src/service/sync_music/unblock_music_with_song_id.js b/backend/src/service/sync_music/unblock_music_with_song_id.js new file mode 100644 index 0000000..887b03c --- /dev/null +++ b/backend/src/service/sync_music/unblock_music_with_song_id.js @@ -0,0 +1,102 @@ +const { getSongInfo } = require('../music_platform/wycloud'); +const syncSingleSongWithUrl = require('./sync_single_song_with_url'); +const logger = require('consola'); +const { findTheBestMatchFromWyCloud, searchSongsWithSongMeta } = require('../search_songs'); +const JobManager = require('../job_manager'); +const JobType = require('../../consts/job_type'); +const JobStatus = require('../../consts/job_status'); +const BusinessCode = require('../../consts/business_code'); + +module.exports = async function unblockMusiWithSongId(uid, source, songId) { + const songInfo = await getSongInfo(uid, songId); + if (songInfo === false) { + return false; + } + + // create job + const args = `unblockMusicWithSongId: {"source":${source},"songId":${songId}}`; + if (await JobManager.findActiveJobByArgs(uid, args)) { + logger.info(`unblock music with songID job is already running.`); + return BusinessCode.StatusJobAlreadyExisted; + } + const jobId = await JobManager.createJob(uid, { + name: `解锁歌曲:${songInfo.songName}`, + args, + type: JobType.UnblockedSong, + status: JobStatus.Pending, + desc: `${songInfo.songName} - ${songInfo.artists.join(',')}`, + progress: 0, + tip: "等待解锁", + createdAt: Date.now() + }); + + // async do the job + (async () => { + logger.info(`${jobId}: try to unblock song: ${JSON.stringify(songInfo)}`); + // download the songs and upload to cloud + const syncSucceed = await syncSingleSongWithMeta(uid, songInfo); + + let tip = songInfo.songName + (syncSucceed ? ": 解锁成功" : ": 解锁失败"); + await JobManager.updateJob(uid, jobId, { + progress: 1, + status: syncSucceed ? JobStatus.Finished : JobStatus.Failed, + tip, + data: {} + }); + })().catch(async e => { + logger.error(`${jobId}: ${e}`); + let tip = '遇到不可思议的错误了哦,任务终止'; + await JobManager.updateJob(uid, jobId, { + status: JobStatus.Failed, + tip, + }); + }) + + return jobId; +} + +async function syncSingleSongWithMeta(uid, wySongMeta) { + logger.info(`sync single song with meta: ${JSON.stringify(wySongMeta)}`); + const songFromWyCloud = await findTheBestMatchFromWyCloud(uid, { + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + musicPlatformSongId: wySongMeta.songId, + }); + // search songs with the meta + const searchListfilttered = await searchSongsWithSongMeta({ + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + duration: wySongMeta.duration, + }, { + expectArtistAkas: songFromWyCloud.artists ? songFromWyCloud.artists : [], + allowSongsJustMatchDuration: false, + allowSongsNotMatchMeta: false, + }); + + logger.info(`use the searchListfilttered: ${JSON.stringify(searchListfilttered)}`); + if (searchListfilttered === false) { + return false; + } + + // find the best match song + for (const searchItem of searchListfilttered) { + logger.info(`try to the search item: ${JSON.stringify(searchItem)}`); + + const isUploadSucceed = await syncSingleSongWithUrl(uid, searchItem.url, { + songName: wySongMeta.songName, + artist: wySongMeta.artists[0], + album: wySongMeta.album, + songFromWyCloud, + }); + if (isUploadSucceed === "IOFailed") { + logger.error(`not try others due to upload failed.`); + return false; + } + if (isUploadSucceed) { + return true; + } + } + return false; +} diff --git a/backend/src/service/sync_music/upload_to_wycloud_disk_with_retry_then_match.js b/backend/src/service/sync_music/upload_to_wycloud_disk_with_retry_then_match.js new file mode 100644 index 0000000..700aab7 --- /dev/null +++ b/backend/src/service/sync_music/upload_to_wycloud_disk_with_retry_then_match.js @@ -0,0 +1,50 @@ +const { uploadSong, matchAndFixCloudSong } = require('../music_platform/wycloud'); +const logger = require('consola'); +const fs = require('fs'); +const sleep = require('../../utils/sleep'); + +module.exports = async function uploadWithRetryThenMatch(uid, path, songInfo, songFromWyCloud) { + const startTime = new Date(); + let isHandleSucceed = false; + let uploadResult; + + for (let tryCount = 0; tryCount < 5; tryCount++) { + if (tryCount !== 0) { + logger.info(`upload song failed, try again: ${path}`); + } + uploadResult = await uploadSong(uid, path); + if (uploadResult === false) { + logger.error(`upload song failed, uid: ${uid}, path: ${path}`); + await sleep(3000); + continue; + } else { + isHandleSucceed = true; + break; + } + } + + // del file async + fs.unlink(path, () => {}); + + if (!isHandleSucceed) { + logger.error(`upload song failed, uid: ${uid}, path: ${path}`); + return "IOFailed"; + } + + const costSeconds = (new Date() - startTime) / 1000; + logger.info(`upload song success, uid: ${uid}, path: ${path}, cost: ${costSeconds}s`); + + if (uploadResult.matched) { + logger.info(`matched song already, uid: ${uid}, songId: ${uploadResult.songId}. ignore.`); + return true; + } + + // fix match manually IF not matched in music platform + if (!songFromWyCloud) { + logger.info(`would not try to match from wycloud!!! uid: ${uid}, ${JSON.stringify(songInfo)}`); + return true; + } + const matchResult = await matchAndFixCloudSong(uid, uploadResult.songId, songFromWyCloud.songId); + logger.info(`match song ${matchResult ? 'success' : 'failed'}, uid: ${uid}, songId: ${uploadResult.songId}, wySongId: ${songFromWyCloud.songId}`); + return true; +} \ No newline at end of file diff --git a/backend/src/utils/cmd.js b/backend/src/utils/cmd.js new file mode 100644 index 0000000..5f6812d --- /dev/null +++ b/backend/src/utils/cmd.js @@ -0,0 +1,32 @@ +const logger = require("consola"); +const spawnAsync = require("child_process").spawn; + +module.exports = function exec(exe, args) { + return new Promise(function (resolve, reject) { + console.log(exe, args); + const process = spawnAsync(exe, args); + let stdout = ""; + let stderr = ""; + + process.stdout.on("data", function (data) { + stdout += data; + }); + process.stderr.on("data", function (data) { + stderr += data; + }); + + process.on("close", function (code) { + resolve({ + code: stderr ? code : 0, + message: stderr ? stderr: stdout, + }); + }); + process.on("error", function (error) { + logger.error('exec error: ', error); + resolve({ + code: -1, + message: error.message, + }); + }); + }); +} \ No newline at end of file diff --git a/backend/src/utils/download.js b/backend/src/utils/download.js new file mode 100644 index 0000000..36245e2 --- /dev/null +++ b/backend/src/utils/download.js @@ -0,0 +1,20 @@ +const got = require('got'); +const fs = require('fs'); +const pipeline = require('stream').pipeline; +const { promisify } = require('util'); +const streamPipeline = promisify(pipeline); + + +module.exports = async function downloadFile(url, destination) { + try { + await streamPipeline( + got.stream(url), + fs.createWriteStream(destination) + ); + return true; + } catch (error) { + console.error('download failed:', error); + return false; + } + +} \ No newline at end of file diff --git a/backend/src/utils/fs.js b/backend/src/utils/fs.js new file mode 100644 index 0000000..bc2e8e8 --- /dev/null +++ b/backend/src/utils/fs.js @@ -0,0 +1,124 @@ +const fs = require('fs'); +const crypto = require('crypto'); + +function asyncReadFile(filePath) { + return new Promise((resolve, reject) => { + fs.readFile(filePath, (err, data) => { + if (err) { + reject(err); + return; + } + resolve(data); + }); + }); +} + +function asyncWriteFile(filePath, data) { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, data, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); +} + +function asyncFileExisted(filePath) { + return new Promise((resolve, reject) => { + fs.access(filePath, fs.constants.F_OK, (err) => { + if (err) { + resolve(false); + return; + } + resolve(true); + }); + }); +} + +function asyncMkdir(dirPath, options) { + return new Promise((resolve, reject) => { + fs.mkdir(dirPath, options, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); +} + +function asyncUnlinkFile(filePath) { + return new Promise((resolve, reject) => { + fs.unlink(filePath, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); +} + +const fsPromise = fs.promises; +async function asyncMoveFile(oldPath, newPath) { + await fsPromise.copyFile(oldPath, newPath) + await fsPromise.unlink(oldPath); +} + +function asyncReadDir(dirPath) { + return new Promise((resolve, reject) => { + fs.readdir(dirPath, (err, files) => { + if (err) { + reject(err); + return; + } + resolve(files); + } + )}); +} + +async function asyncMd5(filePath) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('md5'); + const stream = fs.createReadStream(filePath); + + stream.on('data', (data) => { + hash.update(data); + }); + + stream.on('end', () => { + resolve(hash.digest('hex')); + }); + + stream.on('error', (error) => { + reject(error); + }); + }); +} + +async function asyncAppendFile(filePath, str) { + return new Promise((resolve, reject) => { + fs.appendFile(filePath, str, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); +} + + +module.exports = { + asyncReadFile, + asyncWriteFile, + asyncFileExisted, + asyncMkdir, + asyncUnlinkFile, + asyncMoveFile, + asyncReadDir, + asyncMd5, + asyncAppendFile, +}; \ No newline at end of file diff --git a/backend/src/utils/network.js b/backend/src/utils/network.js new file mode 100644 index 0000000..481fad6 --- /dev/null +++ b/backend/src/utils/network.js @@ -0,0 +1,25 @@ +const https = require('https'); + +function asyncHttpsGet(url) { + return new Promise((resolve) => { + https.get(url, res => { + let data = ''; + + res.on('data', chunk => { + data += chunk; + }); + + res.on('end', () => { + resolve(data.toString()); + }); + + }).on('error', err => { + console.error(err); + resolve(null); + }); + }); +} + +module.exports = { + asyncHttpsGet +} \ No newline at end of file diff --git a/backend/src/utils/regex.js b/backend/src/utils/regex.js new file mode 100644 index 0000000..19010ca --- /dev/null +++ b/backend/src/utils/regex.js @@ -0,0 +1,9 @@ +module.exports = { + matchUrlFromStr: (str) => { + const matched = str.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/); + if (!matched) { + return false; + } + return matched[0]; + } +} \ No newline at end of file diff --git a/backend/src/utils/simple_locker.js b/backend/src/utils/simple_locker.js new file mode 100644 index 0000000..e634a94 --- /dev/null +++ b/backend/src/utils/simple_locker.js @@ -0,0 +1,30 @@ +const lockMap = {}; +const sleep = require('./sleep'); + +async function lock(key, expireSeconds = 20) { + let retryCount = 20; + while (--retryCount >= 0) { + if (!lockMap[key]) { + lockMap[key] = true; + + setTimeout(() => { + delete lockMap[key]; + }, expireSeconds * 1000); + + return true; + } + await sleep(200); + } + + return false; +} + +function unlock(key) { + delete lockMap[key]; +} + + +module.exports = { + lock: lock, + unlock: unlock, +}; \ No newline at end of file diff --git a/backend/src/utils/sleep.js b/backend/src/utils/sleep.js new file mode 100644 index 0000000..d6a3f27 --- /dev/null +++ b/backend/src/utils/sleep.js @@ -0,0 +1,3 @@ +module.exports = function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} \ No newline at end of file diff --git a/backend/src/utils/uuid.js b/backend/src/utils/uuid.js new file mode 100644 index 0000000..740b535 --- /dev/null +++ b/backend/src/utils/uuid.js @@ -0,0 +1,5 @@ +const { v4: uuidv4 } = require('uuid'); + +module.exports = function () { + return uuidv4().replace(/-/g, ''); +} \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..777c83e --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,5 @@ +NODE_ENV = 'development' +VITE_APP_MODE = 'development' +VITE_APP_API_URL = 'http://172.16.252.1:5566/api' +VITE_APP_API_URL = 'http://10.0.0.2:5566/api' +VITE_APP_API_URL = 'http://127.0.0.1:5566/api' diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..28fefc6 --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,3 @@ +NODE_ENV = 'production' +VITE_APP_MODE = 'production' +VITE_APP_API_URL = '/api' \ No newline at end of file diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 0000000..5dbac1e --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +v16.13.0 \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..4daa311 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + Melody - 我的音乐精灵 + + + +
+ + + + + \ No newline at end of file diff --git a/frontend/mobile.html b/frontend/mobile.html new file mode 100644 index 0000000..d481045 --- /dev/null +++ b/frontend/mobile.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + Melody - 我的音乐精灵 + + + +
+ + + + + + + \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f57464f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "melody-frontend", + "private": true, + "version": "0.1.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^1.1.4", + "axios": "0.26.1", + "element-plus": "2.1.9", + "howler": "github:foamzou/howler.js#0.0.1-foam", + "vant": "^3.4.9", + "vue": "3.2.33", + "vue-router": "4.0.12", + "vue-virtual-scroller": "2.0.0-alpha.1", + "vuex": "4.0.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^2.3.1", + "rollup": "^2.70.2", + "vite": "^2.9.14", + "vite-plugin-cdn-import": "^0.3.5", + "vite-plugin-pwa": "^0.12.3", + "workbox-window": "^6.5.4" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..661a6cb --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,3690 @@ +lockfileVersion: 5.4 + +specifiers: + '@vitejs/plugin-vue': ^2.3.1 + axios: 0.26.1 + element-plus: 2.1.9 + howler: github:foamzou/howler.js#0.0.1-foam + rollup: ^2.70.2 + vant: ^3.4.9 + vite: ^2.9.14 + vite-plugin-cdn-import: ^0.3.5 + vite-plugin-pwa: ^0.12.3 + vue: 3.2.33 + vue-router: 4.0.12 + vue-virtual-scroller: 2.0.0-alpha.1 + vuex: 4.0.2 + workbox-window: ^6.5.4 + +dependencies: + axios: 0.26.1 + element-plus: 2.1.9_vue@3.2.33 + howler: github.com/foamzou/howler.js/b8764072a5e9e48adec50ed57691f04c49c0500d + vant: 3.6.16_vue@3.2.33 + vue: 3.2.33 + vue-router: 4.0.12_vue@3.2.33 + vue-virtual-scroller: 2.0.0-alpha.1_vue@3.2.33 + vuex: 4.0.2_vue@3.2.33 + +devDependencies: + '@vitejs/plugin-vue': 2.3.4_vite@2.9.18+vue@3.2.33 + rollup: 2.79.2 + vite: 2.9.18 + vite-plugin-cdn-import: 0.3.5_rollup@2.79.2 + vite-plugin-pwa: 0.12.8_vite@2.9.18 + workbox-window: 6.6.0 + +packages: + + /@ampproject/remapping/2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@apideck/better-ajv-errors/0.3.6_ajv@8.17.1: + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + dependencies: + ajv: 8.17.1 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + dev: true + + /@babel/code-frame/7.26.2: + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + dev: true + + /@babel/compat-data/7.26.5: + resolution: {integrity: sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core/7.26.0: + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.5 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-module-transforms': 7.26.0_@babel+core@7.26.0 + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.5 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator/7.26.5: + resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/parser': 7.26.5 + '@babel/types': 7.26.5 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + dev: true + + /@babel/helper-annotate-as-pure/7.25.9: + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.26.5 + dev: true + + /@babel/helper-compilation-targets/7.26.5: + resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.26.5 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.26.5_@babel+core@7.26.0 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.26.5 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-create-regexp-features-plugin/7.26.3_@babel+core@7.26.0: + resolution: {integrity: sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + dev: true + + /@babel/helper-define-polyfill-provider/0.6.3_@babel+core@7.26.0: + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-plugin-utils': 7.26.5 + debug: 4.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-member-expression-to-functions/7.25.9: + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-imports/7.25.9: + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms/7.26.0_@babel+core@7.26.0: + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-optimise-call-expression/7.25.9: + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.26.5 + dev: true + + /@babel/helper-plugin-utils/7.26.5: + resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-replace-supers/7.26.5_@babel+core@7.26.0: + resolution: {integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-skip-transparent-expression-wrappers/7.25.9: + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@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/helper-validator-option/7.25.9: + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function/7.25.9: + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helpers/7.26.0: + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.5 + dev: true + + /@babel/parser/7.26.5: + resolution: {integrity: sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.26.5 + + /@babel/plugin-bugfix-firefox-class-in-computed-class-key/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-bugfix-safari-class-field-initializer-scope/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9_@babel+core@7.26.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-proposal-private-property-in-object/7.21.0-placeholder-for-preset-env.2_@babel+core@7.26.0: + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + dev: true + + /@babel/plugin-syntax-import-assertions/7.26.0_@babel+core@7.26.0: + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-syntax-import-attributes/7.26.0_@babel+core@7.26.0: + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-syntax-unicode-sets-regex/7.18.6_@babel+core@7.26.0: + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-arrow-functions/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-async-generator-functions/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-remap-async-to-generator': 7.25.9_@babel+core@7.26.0 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-async-to-generator/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-remap-async-to-generator': 7.25.9_@babel+core@7.26.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-block-scoped-functions/7.26.5_@babel+core@7.26.0: + resolution: {integrity: sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-block-scoping/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-class-properties/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-class-static-block/7.26.0_@babel+core@7.26.0: + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-classes/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-replace-supers': 7.26.5_@babel+core@7.26.0 + '@babel/traverse': 7.26.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-computed-properties/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/template': 7.25.9 + dev: true + + /@babel/plugin-transform-destructuring/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-dotall-regex/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-duplicate-keys/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-duplicate-named-capturing-groups-regex/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-dynamic-import/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-exponentiation-operator/7.26.3_@babel+core@7.26.0: + resolution: {integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-export-namespace-from/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-for-of/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-function-name/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-json-strings/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-literals/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-logical-assignment-operators/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-member-expression-literals/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-modules-amd/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs/7.26.3_@babel+core@7.26.0: + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-new-target/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-nullish-coalescing-operator/7.26.6_@babel+core@7.26.0: + resolution: {integrity: sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-numeric-separator/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-object-rest-spread/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-transform-parameters': 7.25.9_@babel+core@7.26.0 + dev: true + + /@babel/plugin-transform-object-super/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-replace-supers': 7.26.5_@babel+core@7.26.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-optional-catch-binding/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-optional-chaining/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-parameters/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-private-methods/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-private-property-in-object/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-property-literals/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-regenerator/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + regenerator-transform: 0.15.2 + dev: true + + /@babel/plugin-transform-regexp-modifiers/7.26.0_@babel+core@7.26.0: + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-reserved-words/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-shorthand-properties/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-spread/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-sticky-regex/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-template-literals/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-typeof-symbol/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-unicode-escapes/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-unicode-property-regex/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-unicode-regex/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/plugin-transform-unicode-sets-regex/7.25.9_@babel+core@7.26.0: + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3_@babel+core@7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + dev: true + + /@babel/preset-env/7.26.0_@babel+core@7.26.0: + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.26.5 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.26.5 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2_@babel+core@7.26.0 + '@babel/plugin-syntax-import-assertions': 7.26.0_@babel+core@7.26.0 + '@babel/plugin-syntax-import-attributes': 7.26.0_@babel+core@7.26.0 + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6_@babel+core@7.26.0 + '@babel/plugin-transform-arrow-functions': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-async-generator-functions': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-async-to-generator': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-block-scoped-functions': 7.26.5_@babel+core@7.26.0 + '@babel/plugin-transform-block-scoping': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-class-properties': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-class-static-block': 7.26.0_@babel+core@7.26.0 + '@babel/plugin-transform-classes': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-computed-properties': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-destructuring': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-dotall-regex': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-duplicate-keys': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-dynamic-import': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-exponentiation-operator': 7.26.3_@babel+core@7.26.0 + '@babel/plugin-transform-export-namespace-from': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-for-of': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-function-name': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-json-strings': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-literals': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-logical-assignment-operators': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-member-expression-literals': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-modules-amd': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-modules-commonjs': 7.26.3_@babel+core@7.26.0 + '@babel/plugin-transform-modules-systemjs': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-modules-umd': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-new-target': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-nullish-coalescing-operator': 7.26.6_@babel+core@7.26.0 + '@babel/plugin-transform-numeric-separator': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-object-rest-spread': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-object-super': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-optional-catch-binding': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-optional-chaining': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-parameters': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-private-methods': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-private-property-in-object': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-property-literals': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-regenerator': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-regexp-modifiers': 7.26.0_@babel+core@7.26.0 + '@babel/plugin-transform-reserved-words': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-shorthand-properties': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-spread': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-sticky-regex': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-template-literals': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-typeof-symbol': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-unicode-escapes': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-unicode-property-regex': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-unicode-regex': 7.25.9_@babel+core@7.26.0 + '@babel/plugin-transform-unicode-sets-regex': 7.25.9_@babel+core@7.26.0 + '@babel/preset-modules': 0.1.6-no-external-plugins_@babel+core@7.26.0 + babel-plugin-polyfill-corejs2: 0.4.12_@babel+core@7.26.0 + babel-plugin-polyfill-corejs3: 0.10.6_@babel+core@7.26.0 + babel-plugin-polyfill-regenerator: 0.6.3_@babel+core@7.26.0 + core-js-compat: 3.40.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules/0.1.6-no-external-plugins_@babel+core@7.26.0: + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/types': 7.26.5 + esutils: 2.0.3 + dev: true + + /@babel/runtime/7.26.0: + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: true + + /@babel/template/7.25.9: + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.5 + '@babel/types': 7.26.5 + dev: true + + /@babel/traverse/7.26.5: + resolution: {integrity: sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.5 + '@babel/parser': 7.26.5 + '@babel/template': 7.25.9 + '@babel/types': 7.26.5 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types/7.26.5: + resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + /@ctrl/tinycolor/3.6.1: + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + dev: false + + /@element-plus/icons-vue/1.1.4_vue@3.2.33: + resolution: {integrity: sha512-Iz/nHqdp1sFPmdzRwHkEQQA3lKvoObk8azgABZ81QUOpW9s/lUyQVUSh0tNtEPZXQlKwlSh7SPgoVxzrE0uuVQ==} + peerDependencies: + vue: ^3.2.0 + dependencies: + vue: 3.2.33 + dev: false + + /@esbuild/linux-loong64/0.14.54: + resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@floating-ui/core/0.6.2: + resolution: {integrity: sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg==} + dev: false + + /@floating-ui/dom/0.4.5: + resolution: {integrity: sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==} + dependencies: + '@floating-ui/core': 0.6.2 + dev: false + + /@jridgewell/gen-mapping/0.3.8: + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/resolve-uri/3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array/1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map/0.3.6: + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/sourcemap-codec/1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + dev: true + + /@jridgewell/trace-mapping/0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.18.0 + dev: true + + /@popperjs/core/2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: false + + /@rollup/plugin-babel/5.3.1_jv6xoz77vs757ecddpkzporema: + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@rollup/pluginutils': 3.1.0_rollup@2.79.2 + rollup: 2.79.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@rollup/plugin-node-resolve/11.2.1_rollup@2.79.2: + resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.2 + '@types/resolve': 1.17.1 + builtin-modules: 3.3.0 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.10 + rollup: 2.79.2 + dev: true + + /@rollup/plugin-replace/2.4.2_rollup@2.79.2: + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0_rollup@2.79.2 + magic-string: 0.25.9 + rollup: 2.79.2 + dev: true + + /@rollup/pluginutils/3.1.0_rollup@2.79.2: + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.2 + dev: true + + /@rollup/pluginutils/4.2.1: + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@surma/rollup-plugin-off-main-thread/2.2.3: + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.12 + dev: true + + /@types/estree/0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + + /@types/estree/1.0.6: + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + dev: true + + /@types/lodash-es/4.17.12: + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + dependencies: + '@types/lodash': 4.17.14 + dev: false + + /@types/lodash/4.17.14: + resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==} + dev: false + + /@types/node/22.10.7: + resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==} + dependencies: + undici-types: 6.20.0 + dev: true + + /@types/resolve/1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 22.10.7 + dev: true + + /@types/trusted-types/2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + dev: true + + /@types/web-bluetooth/0.0.14: + resolution: {integrity: sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==} + dev: false + + /@vant/icons/1.8.0: + resolution: {integrity: sha512-sKfEUo2/CkQFuERxvkuF6mGQZDKu3IQdj5rV9Fm0weJXtchDSSQ+zt8qPCNUEhh9Y8shy5PzxbvAfOOkCwlCXg==} + dev: false + + /@vant/popperjs/1.3.0: + resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==} + dev: false + + /@vant/use/1.6.0_vue@3.2.33: + resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==} + peerDependencies: + vue: ^3.0.0 + dependencies: + vue: 3.2.33 + dev: false + + /@vitejs/plugin-vue/2.3.4_vite@2.9.18+vue@3.2.33: + resolution: {integrity: sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg==} + engines: {node: '>=12.0.0'} + peerDependencies: + vite: ^2.5.10 + vue: ^3.2.25 + dependencies: + vite: 2.9.18 + vue: 3.2.33 + dev: true + + /@vue/compiler-core/3.2.33: + resolution: {integrity: sha512-AAmr52ji3Zhk7IKIuigX2osWWsb2nQE5xsdFYjdnmtQ4gymmqXbjLvkSE174+fF3A3kstYrTgGkqgOEbsdLDpw==} + dependencies: + '@babel/parser': 7.26.5 + '@vue/shared': 3.2.33 + estree-walker: 2.0.2 + source-map: 0.6.1 + + /@vue/compiler-dom/3.2.33: + resolution: {integrity: sha512-GhiG1C8X98Xz9QUX/RlA6/kgPBWJkjq0Rq6//5XTAGSYrTMBgcLpP9+CnlUg1TFxnnCVughAG+KZl28XJqw8uQ==} + dependencies: + '@vue/compiler-core': 3.2.33 + '@vue/shared': 3.2.33 + + /@vue/compiler-sfc/3.2.33: + resolution: {integrity: sha512-H8D0WqagCr295pQjUYyO8P3IejM3vEzeCO1apzByAEaAR/WimhMYczHfZVvlCE/9yBaEu/eu9RdiWr0kF8b71Q==} + dependencies: + '@babel/parser': 7.26.5 + '@vue/compiler-core': 3.2.33 + '@vue/compiler-dom': 3.2.33 + '@vue/compiler-ssr': 3.2.33 + '@vue/reactivity-transform': 3.2.33 + '@vue/shared': 3.2.33 + estree-walker: 2.0.2 + magic-string: 0.25.9 + postcss: 8.5.1 + source-map: 0.6.1 + + /@vue/compiler-ssr/3.2.33: + resolution: {integrity: sha512-XQh1Xdk3VquDpXsnoCd7JnMoWec9CfAzQDQsaMcSU79OrrO2PNR0ErlIjm/mGq3GmBfkQjzZACV+7GhfRB8xMQ==} + dependencies: + '@vue/compiler-dom': 3.2.33 + '@vue/shared': 3.2.33 + + /@vue/devtools-api/6.6.4: + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + dev: false + + /@vue/reactivity-transform/3.2.33: + resolution: {integrity: sha512-4UL5KOIvSQb254aqenW4q34qMXbfZcmEsV/yVidLUgvwYQQ/D21bGX3DlgPUGI3c4C+iOnNmDCkIxkILoX/Pyw==} + dependencies: + '@babel/parser': 7.26.5 + '@vue/compiler-core': 3.2.33 + '@vue/shared': 3.2.33 + estree-walker: 2.0.2 + magic-string: 0.25.9 + + /@vue/reactivity/3.2.33: + resolution: {integrity: sha512-62Sq0mp9/0bLmDuxuLD5CIaMG2susFAGARLuZ/5jkU1FCf9EDbwUuF+BO8Ub3Rbodx0ziIecM/NsmyjardBxfQ==} + dependencies: + '@vue/shared': 3.2.33 + + /@vue/runtime-core/3.2.33: + resolution: {integrity: sha512-N2D2vfaXsBPhzCV3JsXQa2NECjxP3eXgZlFqKh4tgakp3iX6LCGv76DLlc+IfFZq+TW10Y8QUfeihXOupJ1dGw==} + dependencies: + '@vue/reactivity': 3.2.33 + '@vue/shared': 3.2.33 + + /@vue/runtime-dom/3.2.33: + resolution: {integrity: sha512-LSrJ6W7CZTSUygX5s8aFkraDWlO6K4geOwA3quFF2O+hC3QuAMZt/0Xb7JKE3C4JD4pFwCSO7oCrZmZ0BIJUnw==} + dependencies: + '@vue/runtime-core': 3.2.33 + '@vue/shared': 3.2.33 + csstype: 2.6.21 + + /@vue/server-renderer/3.2.33_vue@3.2.33: + resolution: {integrity: sha512-4jpJHRD4ORv8PlbYi+/MfP8ec1okz6rybe36MdpkDrGIdEItHEUyaHSKvz+ptNEyQpALmmVfRteHkU9F8vxOew==} + peerDependencies: + vue: 3.2.33 + dependencies: + '@vue/compiler-ssr': 3.2.33 + '@vue/shared': 3.2.33 + vue: 3.2.33 + + /@vue/shared/3.2.33: + resolution: {integrity: sha512-UBc1Pg1T3yZ97vsA2ueER0F6GbJebLHYlEi4ou1H5YL4KWvMOOWwpYo9/QpWq93wxKG6Wo13IY74Hcn/f7c7Bg==} + + /@vueuse/core/8.9.4_vue@3.2.33: + resolution: {integrity: sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==} + peerDependencies: + '@vue/composition-api': ^1.1.0 + vue: ^2.6.0 || ^3.2.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue: + optional: true + dependencies: + '@types/web-bluetooth': 0.0.14 + '@vueuse/metadata': 8.9.4 + '@vueuse/shared': 8.9.4_vue@3.2.33 + vue: 3.2.33 + vue-demi: 0.14.10_vue@3.2.33 + dev: false + + /@vueuse/metadata/8.9.4: + resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==} + dev: false + + /@vueuse/shared/8.9.4_vue@3.2.33: + resolution: {integrity: sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==} + peerDependencies: + '@vue/composition-api': ^1.1.0 + vue: ^2.6.0 || ^3.2.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue: + optional: true + dependencies: + vue: 3.2.33 + vue-demi: 0.14.10_vue@3.2.33 + dev: false + + /acorn/8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv/8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + dev: true + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /array-buffer-byte-length/1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + is-array-buffer: 3.0.5 + dev: true + + /arraybuffer.prototype.slice/1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + is-array-buffer: 3.0.5 + dev: true + + /async-validator/4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + dev: false + + /async/3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + dev: true + + /at-least-node/1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: true + + /available-typed-arrays/1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + + /axios/0.26.1: + resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} + dependencies: + follow-redirects: 1.15.9 + transitivePeerDependencies: + - debug + dev: false + + /babel-plugin-polyfill-corejs2/0.4.12_@babel+core@7.26.0: + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.26.5 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3_@babel+core@7.26.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3/0.10.6_@babel+core@7.26.0: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3_@babel+core@7.26.0 + core-js-compat: 3.40.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator/0.6.3_@babel+core@7.26.0: + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3_@babel+core@7.26.0 + transitivePeerDependencies: + - supports-color + dev: true + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion/2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces/3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /browserslist/4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001695 + electron-to-chromium: 1.5.84 + node-releases: 2.0.19 + update-browserslist-db: 1.1.2_browserslist@4.24.4 + dev: true + + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /builtin-modules/3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /call-bind-apply-helpers/1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + dev: true + + /call-bind/1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + get-intrinsic: 1.2.7 + set-function-length: 1.2.2 + dev: true + + /call-bound/1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.7 + dev: true + + /caniuse-lite/1.0.30001695: + resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} + dev: true + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /commander/2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /common-tags/1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map/2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /core-js-compat/3.40.0: + resolution: {integrity: sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==} + dependencies: + browserslist: 4.24.4 + dev: true + + /crypto-random-string/2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + dev: true + + /csstype/2.6.21: + resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} + + /data-view-buffer/1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + dev: true + + /data-view-byte-length/1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + dev: true + + /data-view-byte-offset/1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + dev: true + + /dayjs/1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dev: false + + /debug/4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /deepmerge/4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + + /define-data-property/1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + dev: true + + /define-properties/1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + dev: true + + /dunder-proto/1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + dev: true + + /ejs/3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.9.2 + dev: true + + /electron-to-chromium/1.5.84: + resolution: {integrity: sha512-I+DQ8xgafao9Ha6y0qjHHvpZ9OfyA1qKlkHkjywxzniORU2awxyz7f/iVJcULmrF2yrM3nHQf+iDjJtbbexd/g==} + dev: true + + /element-plus/2.1.9_vue@3.2.33: + resolution: {integrity: sha512-6mWqS3YrmJPnouWP4otzL8+MehfOnDFqDbcIdnmC07p+Z0JkWe/CVKc4Wky8AYC8nyDMUQyiZYvooCbqGuM7pg==} + peerDependencies: + vue: ^3.2.0 + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 1.1.4_vue@3.2.33 + '@floating-ui/dom': 0.4.5 + '@popperjs/core': 2.11.8 + '@types/lodash': 4.17.14 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 8.9.4_vue@3.2.33 + async-validator: 4.2.5 + dayjs: 1.11.13 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3_vpgwo5v3ie2bia5ss74pgoa5ly + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.2.33 + transitivePeerDependencies: + - '@vue/composition-api' + dev: false + + /es-abstract/1.23.9: + resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.2.7 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.0 + math-intrinsics: 1.1.0 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.18 + dev: true + + /es-define-property/1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + dev: true + + /es-errors/1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + + /es-object-atoms/1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true + + /es-set-tostringtag/2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + + /es-to-primitive/1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + dev: true + + /esbuild-android-64/0.14.54: + resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.14.54: + resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.14.54: + resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.14.54: + resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.14.54: + resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.14.54: + resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.14.54: + resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.14.54: + resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.14.54: + resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.14.54: + resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.14.54: + resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.14.54: + resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64/0.14.54: + resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x/0.14.54: + resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.14.54: + resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.14.54: + resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.14.54: + resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.14.54: + resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.14.54: + resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.14.54: + resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.14.54: + resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/linux-loong64': 0.14.54 + esbuild-android-64: 0.14.54 + esbuild-android-arm64: 0.14.54 + esbuild-darwin-64: 0.14.54 + esbuild-darwin-arm64: 0.14.54 + esbuild-freebsd-64: 0.14.54 + esbuild-freebsd-arm64: 0.14.54 + esbuild-linux-32: 0.14.54 + esbuild-linux-64: 0.14.54 + esbuild-linux-arm: 0.14.54 + esbuild-linux-arm64: 0.14.54 + esbuild-linux-mips64le: 0.14.54 + esbuild-linux-ppc64le: 0.14.54 + esbuild-linux-riscv64: 0.14.54 + esbuild-linux-s390x: 0.14.54 + esbuild-netbsd-64: 0.14.54 + esbuild-openbsd-64: 0.14.54 + esbuild-sunos-64: 0.14.54 + esbuild-windows-32: 0.14.54 + esbuild-windows-64: 0.14.54 + esbuild-windows-arm64: 0.14.54 + dev: true + + /escalade/3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + dev: true + + /escape-html/1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /estree-walker/1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + + /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'} + dev: true + + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob/3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + 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 + dev: true + + /fast-json-stable-stringify/2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-uri/3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + dev: true + + /fastq/1.18.0: + resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} + dependencies: + reusify: 1.0.4 + dev: true + + /filelist/1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.6 + dev: true + + /fill-range/7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /follow-redirects/1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /for-each/0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /fs-extra/9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents/2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /function.prototype.name/1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + dev: true + + /functions-have-names/1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /gensync/1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-intrinsic/1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + dev: true + + /get-own-enumerable-property-symbols/3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + dev: true + + /get-proto/1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + dev: true + + /get-symbol-description/1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + dev: true + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + 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 + dev: true + + /globals/11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globalthis/1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + dev: true + + /gopd/1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + dev: true + + /graceful-fs/4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /has-bigints/1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + dev: true + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors/1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.1 + dev: true + + /has-proto/1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + dev: true + + /has-symbols/1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag/1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.1.0 + dev: true + + /hasown/2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /idb/7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + dev: true + + /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. + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /internal-slot/1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + dev: true + + /is-array-buffer/3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + dev: true + + /is-async-function/2.1.0: + resolution: {integrity: sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + dev: true + + /is-bigint/1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + dependencies: + has-bigints: 1.1.0 + dev: true + + /is-boolean-object/1.2.1: + resolution: {integrity: sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + dev: true + + /is-callable/1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module/2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + dev: true + + /is-data-view/1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + is-typed-array: 1.1.15 + dev: true + + /is-date-object/1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-finalizationregistry/1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + dev: true + + /is-generator-function/1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-map/2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + dev: true + + /is-module/1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + + /is-number-object/1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-obj/1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-reference/1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.6 + dev: true + + /is-regex/1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + + /is-regexp/1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + dev: true + + /is-set/2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + dev: true + + /is-shared-array-buffer/1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + dev: true + + /is-stream/2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-string/1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + dev: true + + /is-symbol/1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + dev: true + + /is-typed-array/1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.18 + dev: true + + /is-weakmap/2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + dev: true + + /is-weakref/1.1.0: + resolution: {integrity: sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + dev: true + + /is-weakset/2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + dev: true + + /isarray/2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /jake/10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: true + + /jest-worker/26.6.2: + resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 22.10.7 + merge-stream: 2.0.0 + supports-color: 7.2.0 + dev: true + + /js-tokens/4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /jsesc/3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsesc/3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /json-schema-traverse/1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-schema/0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: true + + /json5/2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonfile/6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsonpointer/5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + dev: true + + /leven/3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + + /lodash-es/4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + + /lodash-unified/1.0.3_vpgwo5v3ie2bia5ss74pgoa5ly: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + dev: false + + /lodash.debounce/4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.sortby/4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /lru-cache/5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /magic-string/0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + + /math-intrinsics/1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + dev: true + + /memoize-one/6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + dev: false + + /merge-stream/2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch/4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + dev: true + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch/5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /mitt/2.1.0: + resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==} + dev: false + + /ms/2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid/3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /node-releases/2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + dev: true + + /normalize-wheel-es/1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + dev: false + + /object-inspect/1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + dev: true + + /object-keys/1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign/4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + dev: true + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /own-keys/1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.7 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /picocolors/1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /possible-typed-array-names/1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + + /postcss/8.5.1: + resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + /pretty-bytes/5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + dev: true + + /pretty-bytes/6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + dev: true + + /punycode/2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /randombytes/2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /reflect.getprototypeof/1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.2.7 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + dev: true + + /regenerate-unicode-properties/10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate/1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime/0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + dev: true + + /regenerator-transform/0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.26.0 + dev: true + + /regexp.prototype.flags/1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + dev: true + + /regexpu-core/6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + dev: true + + /regjsgen/0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + dev: true + + /regjsparser/0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + dependencies: + jsesc: 3.0.2 + dev: true + + /require-from-string/2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve/1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rollup-plugin-external-globals/0.6.1_rollup@2.79.2: + resolution: {integrity: sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==} + peerDependencies: + rollup: ^2.25.0 + dependencies: + '@rollup/pluginutils': 4.2.1 + estree-walker: 2.0.2 + is-reference: 1.2.1 + magic-string: 0.25.9 + rollup: 2.79.2 + dev: true + + /rollup-plugin-terser/7.0.2_rollup@2.79.2: + resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser + peerDependencies: + rollup: ^2.0.0 + dependencies: + '@babel/code-frame': 7.26.2 + jest-worker: 26.6.2 + rollup: 2.79.2 + serialize-javascript: 4.0.0 + terser: 5.37.0 + dev: true + + /rollup/2.77.3: + resolution: {integrity: sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /rollup/2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-array-concat/1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + has-symbols: 1.1.0 + isarray: 2.0.5 + dev: true + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-push-apply/1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + dev: true + + /safe-regex-test/1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-regex: 1.2.1 + dev: true + + /semver/6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /serialize-javascript/4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + dependencies: + randombytes: 2.1.0 + dev: true + + /set-function-length/1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.7 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + dev: true + + /set-function-name/2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + + /set-proto/1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + dev: true + + /side-channel-list/1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + dev: true + + /side-channel-map/1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + dev: true + + /side-channel-weakmap/1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.3 + side-channel-map: 1.0.1 + dev: true + + /side-channel/1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.3 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + dev: true + + /source-map-js/1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /source-map/0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + /string.prototype.matchall/4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.2.7 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + dev: true + + /string.prototype.trim/1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + dev: true + + /string.prototype.trimend/1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + dev: true + + /string.prototype.trimstart/1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + dev: true + + /stringify-object/3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + dev: true + + /strip-comments/2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + dev: true + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /temp-dir/2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + dev: true + + /tempy/0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + dev: true + + /terser/5.37.0: + resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tr46/1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.1 + dev: true + + /type-fest/0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + dev: true + + /typed-array-buffer/1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + dev: true + + /typed-array-byte-length/1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + dev: true + + /typed-array-byte-offset/1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + dev: true + + /typed-array-length/1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.0.0 + reflect.getprototypeof: 1.0.10 + dev: true + + /unbox-primitive/1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + dev: true + + /undici-types/6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + dev: true + + /unicode-canonical-property-names-ecmascript/2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript/2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript/2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript/2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /unique-string/2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + dependencies: + crypto-random-string: 2.0.0 + dev: true + + /universalify/2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /upath/1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + dev: true + + /update-browserslist-db/1.1.2_browserslist@4.24.4: + resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 + dev: true + + /vant/3.6.16_vue@3.2.33: + resolution: {integrity: sha512-9pZao0NEeZQ0ZEb6N7SZxtqcdTp24o8IizhZS1G+FtStlXeKOFzCl+Nf1pIWRneQ9Kn+K+mNrfi2eiIZjVVppw==} + peerDependencies: + vue: ^3.0.0 + dependencies: + '@vant/icons': 1.8.0 + '@vant/popperjs': 1.3.0 + '@vant/use': 1.6.0_vue@3.2.33 + vue: 3.2.33 + dev: false + + /vite-plugin-cdn-import/0.3.5_rollup@2.79.2: + resolution: {integrity: sha512-e1raoalfBiIhv+hnMeSp1UNjloDDBhHpeFxkwRRdPBmTdDRqdEEn8owUmT5u8UBSVCs4xN3n/od4a91vXEhXPQ==} + dependencies: + rollup-plugin-external-globals: 0.6.1_rollup@2.79.2 + transitivePeerDependencies: + - rollup + dev: true + + /vite-plugin-pwa/0.12.8_vite@2.9.18: + resolution: {integrity: sha512-pSiFHmnJGMQJJL8aJzQ8SaraZBSBPMGvGUkCNzheIq9UQCEk/eP3UmANNmS9eupuhIpTK8AdxTOHcaMcAqAbCA==} + peerDependencies: + vite: ^2.0.0 || ^3.0.0-0 + dependencies: + debug: 4.4.0 + fast-glob: 3.3.3 + pretty-bytes: 6.1.1 + rollup: 2.79.2 + vite: 2.9.18 + workbox-build: 6.6.0 + workbox-window: 6.6.1 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: true + + /vite/2.9.18: + resolution: {integrity: sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==} + engines: {node: '>=12.2.0'} + hasBin: true + peerDependencies: + less: '*' + sass: '*' + stylus: '*' + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + dependencies: + esbuild: 0.14.54 + postcss: 8.5.1 + resolve: 1.22.10 + rollup: 2.77.3 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vue-demi/0.14.10_vue@3.2.33: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.2.33 + dev: false + + /vue-observe-visibility/2.0.0-alpha.1_vue@3.2.33: + resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==} + peerDependencies: + vue: ^3.0.0 + dependencies: + vue: 3.2.33 + dev: false + + /vue-resize/2.0.0-alpha.1_vue@3.2.33: + resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==} + peerDependencies: + vue: ^3.0.0 + dependencies: + vue: 3.2.33 + dev: false + + /vue-router/4.0.12_vue@3.2.33: + resolution: {integrity: sha512-CPXvfqe+mZLB1kBWssssTiWg4EQERyqJZes7USiqfW9B5N2x+nHlnsM1D3b5CaJ6qgCvMmYJnz+G0iWjNCvXrg==} + peerDependencies: + vue: ^3.0.0 + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.2.33 + dev: false + + /vue-virtual-scroller/2.0.0-alpha.1_vue@3.2.33: + resolution: {integrity: sha512-Mn5w3Qe06t7c3Imm2RHD43RACab1CCWplpdgzq+/FWJcpQtcGKd5vDep8i+nIwFtzFLsWAqEK0RzM7KrfAcBng==} + peerDependencies: + vue: ^3.0.11 + dependencies: + mitt: 2.1.0 + vue: 3.2.33 + vue-observe-visibility: 2.0.0-alpha.1_vue@3.2.33 + vue-resize: 2.0.0-alpha.1_vue@3.2.33 + dev: false + + /vue/3.2.33: + resolution: {integrity: sha512-si1ExAlDUrLSIg/V7D/GgA4twJwfsfgG+t9w10z38HhL/HA07132pUQ2KuwAo8qbCyMJ9e6OqrmWrOCr+jW7ZQ==} + dependencies: + '@vue/compiler-dom': 3.2.33 + '@vue/compiler-sfc': 3.2.33 + '@vue/runtime-dom': 3.2.33 + '@vue/server-renderer': 3.2.33_vue@3.2.33 + '@vue/shared': 3.2.33 + + /vuex/4.0.2_vue@3.2.33: + resolution: {integrity: sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==} + peerDependencies: + vue: ^3.0.2 + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.2.33 + dev: false + + /webidl-conversions/4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + + /whatwg-url/7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + + /which-boxed-primitive/1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.1 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + dev: true + + /which-builtin-type/1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.0 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.0 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.18 + dev: true + + /which-collection/1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + dev: true + + /which-typed-array/1.1.18: + resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + for-each: 0.3.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + dev: true + + /workbox-background-sync/6.6.0: + resolution: {integrity: sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==} + dependencies: + idb: 7.1.1 + workbox-core: 6.6.0 + dev: true + + /workbox-broadcast-update/6.6.0: + resolution: {integrity: sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==} + dependencies: + workbox-core: 6.6.0 + dev: true + + /workbox-build/6.6.0: + resolution: {integrity: sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==} + engines: {node: '>=10.0.0'} + dependencies: + '@apideck/better-ajv-errors': 0.3.6_ajv@8.17.1 + '@babel/core': 7.26.0 + '@babel/preset-env': 7.26.0_@babel+core@7.26.0 + '@babel/runtime': 7.26.0 + '@rollup/plugin-babel': 5.3.1_jv6xoz77vs757ecddpkzporema + '@rollup/plugin-node-resolve': 11.2.1_rollup@2.79.2 + '@rollup/plugin-replace': 2.4.2_rollup@2.79.2 + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.17.1 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + pretty-bytes: 5.6.0 + rollup: 2.79.2 + rollup-plugin-terser: 7.0.2_rollup@2.79.2 + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 6.6.0 + workbox-broadcast-update: 6.6.0 + workbox-cacheable-response: 6.6.0 + workbox-core: 6.6.0 + workbox-expiration: 6.6.0 + workbox-google-analytics: 6.6.0 + workbox-navigation-preload: 6.6.0 + workbox-precaching: 6.6.0 + workbox-range-requests: 6.6.0 + workbox-recipes: 6.6.0 + workbox-routing: 6.6.0 + workbox-strategies: 6.6.0 + workbox-streams: 6.6.0 + workbox-sw: 6.6.0 + workbox-window: 6.6.0 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: true + + /workbox-cacheable-response/6.6.0: + resolution: {integrity: sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==} + deprecated: workbox-background-sync@6.6.0 + dependencies: + workbox-core: 6.6.0 + dev: true + + /workbox-core/6.6.0: + resolution: {integrity: sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==} + dev: true + + /workbox-core/6.6.1: + resolution: {integrity: sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw==} + deprecated: this package has been deprecated + dev: true + + /workbox-expiration/6.6.0: + resolution: {integrity: sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==} + dependencies: + idb: 7.1.1 + workbox-core: 6.6.0 + dev: true + + /workbox-google-analytics/6.6.0: + resolution: {integrity: sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained + dependencies: + workbox-background-sync: 6.6.0 + workbox-core: 6.6.0 + workbox-routing: 6.6.0 + workbox-strategies: 6.6.0 + dev: true + + /workbox-navigation-preload/6.6.0: + resolution: {integrity: sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==} + dependencies: + workbox-core: 6.6.0 + dev: true + + /workbox-precaching/6.6.0: + resolution: {integrity: sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==} + dependencies: + workbox-core: 6.6.0 + workbox-routing: 6.6.0 + workbox-strategies: 6.6.0 + dev: true + + /workbox-range-requests/6.6.0: + resolution: {integrity: sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==} + dependencies: + workbox-core: 6.6.0 + dev: true + + /workbox-recipes/6.6.0: + resolution: {integrity: sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==} + dependencies: + workbox-cacheable-response: 6.6.0 + workbox-core: 6.6.0 + workbox-expiration: 6.6.0 + workbox-precaching: 6.6.0 + workbox-routing: 6.6.0 + workbox-strategies: 6.6.0 + dev: true + + /workbox-routing/6.6.0: + resolution: {integrity: sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==} + dependencies: + workbox-core: 6.6.0 + dev: true + + /workbox-strategies/6.6.0: + resolution: {integrity: sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==} + dependencies: + workbox-core: 6.6.0 + dev: true + + /workbox-streams/6.6.0: + resolution: {integrity: sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==} + dependencies: + workbox-core: 6.6.0 + workbox-routing: 6.6.0 + dev: true + + /workbox-sw/6.6.0: + resolution: {integrity: sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==} + dev: true + + /workbox-window/6.6.0: + resolution: {integrity: sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==} + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 6.6.0 + dev: true + + /workbox-window/6.6.1: + resolution: {integrity: sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ==} + deprecated: this package has been deprecated + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 6.6.1 + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist/3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + github.com/foamzou/howler.js/b8764072a5e9e48adec50ed57691f04c49c0500d: + resolution: {tarball: https://codeload.github.com/foamzou/howler.js/tar.gz/b8764072a5e9e48adec50ed57691f04c49c0500d} + name: howler + version: 2.2.3 + dev: false diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..012ab25 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/github-logo.png b/frontend/public/github-logo.png new file mode 100644 index 0000000..182a1a3 Binary files /dev/null and b/frontend/public/github-logo.png differ diff --git a/frontend/public/melody-192x192.png b/frontend/public/melody-192x192.png new file mode 100644 index 0000000..f5fa2f2 Binary files /dev/null and b/frontend/public/melody-192x192.png differ diff --git a/frontend/public/melody-512x512.png b/frontend/public/melody-512x512.png new file mode 100644 index 0000000..8569f3f Binary files /dev/null and b/frontend/public/melody-512x512.png differ diff --git a/frontend/public/melody.png b/frontend/public/melody.png new file mode 100644 index 0000000..e495971 Binary files /dev/null and b/frontend/public/melody.png differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..77ec23a --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,451 @@ + + + + + diff --git a/frontend/src/Mobile.vue b/frontend/src/Mobile.vue new file mode 100644 index 0000000..3f60c58 --- /dev/null +++ b/frontend/src/Mobile.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/frontend/src/api/axios.js b/frontend/src/api/axios.js new file mode 100644 index 0000000..bf25b98 --- /dev/null +++ b/frontend/src/api/axios.js @@ -0,0 +1,74 @@ +import axios from "axios"; +const axiosApiInstance = axios.create(); +import storage from "../utils/storage" + +axiosApiInstance.defaults.baseURL = import.meta.env.VITE_APP_API_URL + +//post请求头 +//允许跨域携带cookie信息 +axiosApiInstance.defaults.withCredentials = true; +//设置超时 +axiosApiInstance.defaults.timeout = 12000; + +axiosApiInstance.interceptors.request.use( + config => { + config.headers = { + 'mk': (config.params && config.params['mk']) ? config.params['mk'] : storage.get('mk') + } + return config; + }, + error => { + return Promise.reject(error); + } +); + +axiosApiInstance.interceptors.response.use( + response => { + return Promise.resolve(response); + }, + error => { + // 返回错误响应中的数据 + if (error.response && error.response.data) { + return Promise.resolve(error.response); + } + // 如果没有response.data,返回一个统一的错误格式 + return Promise.resolve({ + data: { + code: -1, + message: error.message || '网络错误' + } + }); + } +); + +export const post = (url, data) => { + return new Promise((resolve, reject) => { + axiosApiInstance({ + method: 'post', + url, + data, + }) + .then(res => { + resolve(res ? res.data : false) + }) + .catch(err => { + reject(err.data) + }); + }) +}; + +export const get = (url, data) => { + return new Promise((resolve, reject) => { + axiosApiInstance({ + method: 'get', + url, + params: data, + }) + .then(res => { + resolve(res ? res.data : false) + }) + .catch(err => { + reject(err) + }) + }) +} \ No newline at end of file diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js new file mode 100644 index 0000000..2d16d41 --- /dev/null +++ b/frontend/src/api/index.js @@ -0,0 +1,78 @@ +import { get, post} from "./axios"; + +export const searchSongs = data => get("/songs", data); +export const getSongsMeta = data => get("/songs-meta", data); +export const getPlayUrl = songId => get(`/songs/netease/${songId}/playUrl`); + +export const getAccount = data => get("/account", data); +export const setAccount = data => post("/account", data); +export const qrLoginCreate = _ => get("/account/qrlogin-create", {}); +export const qrLoginCheck = qrKey => get("/account/qrlogin-check", {qrKey}); + +export const getAllPlaylist = data => get("/playlists", data); +export const getPlaylistDetail = playlistId => get(`/playlists/netease/${playlistId}/songs`); +export const getJobDetail = jobId => get(`/sync-jobs/${jobId}`); +export const createSyncSongFromUrlJob = (url, songId = "") => { + return post("/sync-jobs", { + "jobType": "SyncSongFromUrl", + "urlJob": { + "url": url, + "meta": { + "songId": songId + } + } + }); +}; +export const createDownloadSongFromUrlJob = (url, songId = "") => { + return post("/sync-jobs", { + "jobType": "DownloadSongFromUrl", + "urlJob": { + "url": url, + "meta": { + "songId": songId + } + } + }); +}; +export const createSyncSongFromPlaylistJob = (playlistId, options) => { + return post("/sync-jobs", { + "jobType": "UnblockedPlaylist", + "playlist": { + "id": playlistId, + "source": "netease" + }, + "options": options + }); +}; +export const createSyncThePlaylistToLocalServiceJob = (playlistId) => { + return post("/sync-jobs", { + "jobType": "SyncThePlaylistToLocalService", + "playlist": { + "id": playlistId, + "source": "netease" + } + }); +}; +export const createSyncSongWithSongIdJob = (songId) => { + return post("/sync-jobs", { + "jobType": "UnblockedSong", + "songId": songId, + "source": "netease" + }); +}; + +export const checkMediaFetcherLib = data => get("/media-fetcher-lib/version-check", data); +export const updateMediaFetcherLib = (version) => { + return post("/media-fetcher-lib/update", { + "version": version, + }); +}; + +export const getGlobalConfig = _ => get("/config/global", {}); +export const setGlobalConfig = (config) => { + return post("/config/global", config); +}; + +export const getAllAccounts = _ => get("/accounts", {}); + +export const getNextRunInfo = () => get("/scheduler/next-run", {}); diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..f3d2503 Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/components/Player.vue b/frontend/src/components/Player.vue new file mode 100644 index 0000000..c06d018 --- /dev/null +++ b/frontend/src/components/Player.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/frontend/src/components/SearchResultListForMobile.vue b/frontend/src/components/SearchResultListForMobile.vue new file mode 100644 index 0000000..016fc16 --- /dev/null +++ b/frontend/src/components/SearchResultListForMobile.vue @@ -0,0 +1,219 @@ + + + diff --git a/frontend/src/components/SearchResultTable.vue b/frontend/src/components/SearchResultTable.vue new file mode 100644 index 0000000..b516a8a --- /dev/null +++ b/frontend/src/components/SearchResultTable.vue @@ -0,0 +1,328 @@ + + + + + diff --git a/frontend/src/components/TaskNotification.js b/frontend/src/components/TaskNotification.js new file mode 100644 index 0000000..96029ec --- /dev/null +++ b/frontend/src/components/TaskNotification.js @@ -0,0 +1,53 @@ +import { ElNotification } from 'element-plus' +import { getJobDetail } from "../api"; + +export function startTaskListener(jobID) { + let lastTip = ''; + + const task = (jobID) => { + getJobDetail(jobID).then(res => { + const status = res.data.jobs.status; + if (status === '已完成' || status === '失败') { + clearInterval(interval) + } + if (lastTip == res.data.jobs.tip) { + return; + }; + lastTip = res.data.jobs.tip; + + let title; + let type; + let duration = 4500; + if (status === '已完成') { + title = '任务完成'; + type = 'success'; + duration = 6000; + } else if (status === '失败') { + title = '任务失败'; + type = 'error'; + } else { + title = '任务进度提示'; + type = 'info'; + duration = 2500; + } + + if (lastTip == "") { + duration = 4500; + } + + ElNotification({ + title, + message: "" + res.data.jobs.name + "
" + "" + res.data.jobs.desc + "
" + res.data.jobs.tip, + dangerouslyUseHTMLString: true, + type, + duration, + }); + }) + }; + + task(jobID); + const interval = setInterval(() => { + task(jobID); + }, 1000) +} + diff --git a/frontend/src/components/TaskNotificationForMobile.js b/frontend/src/components/TaskNotificationForMobile.js new file mode 100644 index 0000000..f91b0b7 --- /dev/null +++ b/frontend/src/components/TaskNotificationForMobile.js @@ -0,0 +1,50 @@ +import { getJobDetail } from "../api"; +import { Notify } from 'vant'; + +export function startTaskListener(jobID) { + let lastTip = ''; + + const task = (jobID) => { + getJobDetail(jobID).then(res => { + const status = res.data.jobs.status; + if (status === '已完成' || status === '失败') { + clearInterval(interval) + } + if (lastTip == res.data.jobs.tip) { + return; + }; + lastTip = res.data.jobs.tip; + + let title; + let type; + let duration = 4500; + if (status === '已完成') { + type = 'success'; + duration = 6000; + } else if (status === '失败') { + type = 'danger'; + } else { + title = '任务进度提示'; + type = 'primary'; + duration = 2500; + } + + if (lastTip == "") { + duration = 4500; + } + + const options = { + message: `${res.data.jobs.tip}\n${res.data.jobs.name}\n${res.data.jobs.desc}`, + type, + duration, + }; + Notify(options); + }) + }; + + task(jobID); + const interval = setInterval(() => { + task(jobID); + }, 1000) +} + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..ae5a713 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,11 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import ElementPlusLocaleZhCn from 'element-plus/lib/locale/lang/zh-cn' +import App from './App.vue' +import router from './router' + +const app = createApp(App) +app.use(router) +app.use(ElementPlus, { locale: ElementPlusLocaleZhCn }) +app.mount('#app') + diff --git a/frontend/src/mobile.js b/frontend/src/mobile.js new file mode 100644 index 0000000..e6b1427 --- /dev/null +++ b/frontend/src/mobile.js @@ -0,0 +1,18 @@ +import vant from 'vant'; +import 'vant/lib/index.css'; +import VueVirtualScroller from 'vue-virtual-scroller' +import { createApp } from 'vue' +import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' +import { useRegisterSW } from 'virtual:pwa-register/vue'; + +import App from './Mobile.vue' +import router from './router/mobile' + +useRegisterSW(); + +const app = createApp(App) +app.use(router) +app.use(vant) +app.use(VueVirtualScroller) +app.mount('#app') + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..2840196 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,50 @@ +import { createRouter, createWebHistory, createWebHashHistory } from "vue-router" +import storage from "../utils/storage" + +const PathPlaylist = '/playlist'; +const PathSetting = '/setting'; + +const routes = [ + { + path: '/', + component: () => import('../views/pc/Home.vue') + }, + { + path: '/account', + name: "Account", + component: () => import('../views/pc/Account.vue') + }, + { + path: PathPlaylist, + name: "Playlist", + component: () => import('../views/pc/Playlist.vue') + }, + { + path: PathSetting, + name: "Setting", + component: () => import('../views/pc/Setting.vue') + }, +] +export const router = createRouter({ + history: createWebHashHistory(), + routes: routes +}) + +router.beforeEach((to, from, next) => { + if (to.path === "/account") { + next(); + return; + } + const mk = storage.get('mk') + const wyAccount = storage.get('wyAccount') + if (!mk) { + next("/account"); + } + if ([PathPlaylist].includes(to.path) && !wyAccount) { + next("/account"); + return; + } + next(); + }); + +export default router \ No newline at end of file diff --git a/frontend/src/router/mobile.js b/frontend/src/router/mobile.js new file mode 100644 index 0000000..0f5c4c3 --- /dev/null +++ b/frontend/src/router/mobile.js @@ -0,0 +1,42 @@ +import { createRouter, createWebHistory, createWebHashHistory } from "vue-router" +import storage from "../utils/storage" + +const routes = [ + { + path: '/', + component: () => import('../views/mobile/Home.vue') + }, + { + path: '/account', + name: "Account", + component: () => import('../views/mobile/Account.vue') + }, + { + path: '/playlist', + name: "Playlist", + component: () => import('../views/mobile/Playlist.vue') + }, +] +export const router = createRouter({ + history: createWebHashHistory(), + routes: routes +}) + +router.beforeEach((to, from, next) => { + if (to.path === "/account") { + next(); + return; + } + const mk = storage.get('mk') + const wyAccount = storage.get('wyAccount') + if (!mk) { + next("/account"); + } + if (to.path === "/playlist" && !wyAccount) { + next("/account"); + return; + } + next(); + }); + +export default router \ No newline at end of file diff --git a/frontend/src/utils/audio.js b/frontend/src/utils/audio.js new file mode 100644 index 0000000..5e8624d --- /dev/null +++ b/frontend/src/utils/audio.js @@ -0,0 +1,22 @@ +/** + * Get the proper play URL based on source + * @param {string} source - The source platform (e.g., 'bilibili', 'netease') + * @param {string} url - The original audio URL + * @param {string} referer - The referer URL + * @returns {string} The processed play URL + */ +export function getProperPlayUrl(source, url, referer) { + console.log("--------getProperPlayUrl----------------"); + console.log(source); + console.log(url); + console.log(referer); + if (source === "bilibili") { + const params = new URLSearchParams({ + url: url, + source: 'bilibili', + referer: referer + }); + return `${import.meta.env.VITE_APP_API_URL}/proxy/audio?${params}`; + } + return url; +} \ No newline at end of file diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js new file mode 100644 index 0000000..b394715 --- /dev/null +++ b/frontend/src/utils/index.js @@ -0,0 +1,46 @@ +export function secondDurationToDisplayDuration(secondDuration, allowZero = false) { + if (!secondDuration) { + return allowZero ? "00:00" : " - "; + } + secondDuration = parseInt(secondDuration); + let duration = secondDuration; + let minute = Math.floor(duration / 60); + let second = duration % 60; + if (minute < 10) { + minute = "0" + minute; + } + if (second < 10) { + second = "0" + second; + } + return minute + ":" + second; +} + +export function sourceCodeToName(source) { + return { + "qq": "QQ音乐", + "xiami": "虾米音乐", + "netease": "网易云音乐", + "kugou": "酷狗音乐", + "kuwo": "酷我音乐", + "migu": "咪咕音乐", + "bilibili": "Bilibili", + "douyin": "抖音", + "youtube": "YouTube", + }[source] || "未知"; +} + +export function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export function ellipsis(value, maxLength) { + if (!value) { + return ""; + } + value = value.trim(); + if (!value) return ""; + if (value.length > maxLength) { + return value.slice(0, maxLength) + "..."; + } + return value; + } \ No newline at end of file diff --git a/frontend/src/utils/pwa.js b/frontend/src/utils/pwa.js new file mode 100644 index 0000000..fc79ef3 --- /dev/null +++ b/frontend/src/utils/pwa.js @@ -0,0 +1,24 @@ +let request; + +let isInstallable = false; + +// The file is not useful now. +window.addEventListener('beforeinstallprompt', (r) => { + console.log('beforeinstallprompt') + // Prevent Chrome 67 and earlier from automatically showing the prompt + r.preventDefault() + request = r + isInstallable = true; +}); + +export async function installPWA() { + if (request) { + console.log('start install') + let installResponse = await request.prompt(); + console.info({installResponse}); + return installResponse.outcome === 'accepted'; + } else { + console.log('The request is not available'); + return false; + } +} \ No newline at end of file diff --git a/frontend/src/utils/storage.js b/frontend/src/utils/storage.js new file mode 100644 index 0000000..a5577d5 --- /dev/null +++ b/frontend/src/utils/storage.js @@ -0,0 +1,20 @@ +export default { + set (key, val) { + if (typeof val === 'object') { + val = JSON.stringify(val) + } + window.localStorage.setItem(key, val) + }, + get (key) { + let data = window.localStorage.getItem(key) + try { + data = JSON.parse(data); + return data; + } catch { + return data; + } + }, + del (key) { + window.localStorage.removeItem(key) + }, +} \ No newline at end of file diff --git a/frontend/src/views/mobile/Account.vue b/frontend/src/views/mobile/Account.vue new file mode 100644 index 0000000..8eed29b --- /dev/null +++ b/frontend/src/views/mobile/Account.vue @@ -0,0 +1,235 @@ + + + diff --git a/frontend/src/views/mobile/Home.vue b/frontend/src/views/mobile/Home.vue new file mode 100644 index 0000000..4eff0e3 --- /dev/null +++ b/frontend/src/views/mobile/Home.vue @@ -0,0 +1,502 @@ + + + diff --git a/frontend/src/views/mobile/Playlist.vue b/frontend/src/views/mobile/Playlist.vue new file mode 100644 index 0000000..67f63c3 --- /dev/null +++ b/frontend/src/views/mobile/Playlist.vue @@ -0,0 +1,422 @@ + + + + + diff --git a/frontend/src/views/pc/Account.vue b/frontend/src/views/pc/Account.vue new file mode 100644 index 0000000..85041c8 --- /dev/null +++ b/frontend/src/views/pc/Account.vue @@ -0,0 +1,842 @@ + + + + + diff --git a/frontend/src/views/pc/Home.vue b/frontend/src/views/pc/Home.vue new file mode 100644 index 0000000..6dfa6af --- /dev/null +++ b/frontend/src/views/pc/Home.vue @@ -0,0 +1,514 @@ + + + + + diff --git a/frontend/src/views/pc/Playlist.vue b/frontend/src/views/pc/Playlist.vue new file mode 100644 index 0000000..65b660a --- /dev/null +++ b/frontend/src/views/pc/Playlist.vue @@ -0,0 +1,465 @@ + + + + + + + + + diff --git a/frontend/src/views/pc/Setting.vue b/frontend/src/views/pc/Setting.vue new file mode 100644 index 0000000..1775981 --- /dev/null +++ b/frontend/src/views/pc/Setting.vue @@ -0,0 +1,513 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..e8c3b76 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,100 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import importToCDN from 'vite-plugin-cdn-import' +import path from 'path' +import { VitePWA } from 'vite-plugin-pwa' + +export default defineConfig(({command, mode}) => { + return { + server: { + host: '0.0.0.0' + }, + base: './', + define: { + 'process.env': process.env + }, + plugins: [ + VitePWA({ + registerType: 'autoUpdate', + manifest: { + name: 'Melody', + short_name: 'Melody', + description: 'Enjoy your music with Melody', + theme_color: '#ffffff', + start_url:"./mobile.html", + icons: [ + { + src: 'melody-192x192.png', + sizes: '192x192', + type: 'image/png' + }, + { + src: 'melody-512x512.png', + sizes: '512x512', + type: 'image/png' + } + ] + } + }), + vue(), + importToCDN({ + modules:[ + { + name: "vue", + var: "Vue", + path: "https://cdnjs.cloudflare.com/ajax/libs/vue/3.2.33/vue.global.min.js", + }, + { + name: "vue-router", + var: "VueRouter", + path: "https://cdnjs.cloudflare.com/ajax/libs/vue-router/4.0.14/vue-router.global.min.js", + }, + { + name: "vuex", + var: "Vuex", + path: 'https://cdnjs.cloudflare.com/ajax/libs/vuex/4.0.2/vuex.global.min.js', + }, + { + name: "axios", + var: "axios", + path: 'https://cdnjs.cloudflare.com/ajax/libs/axios/0.26.1/axios.min.js', + }, + { + name: "element-plus", + var: "ElementPlus", + path: 'https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.1.9/index.full.js', + css: ["https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.1.9/theme-chalk/index.min.css"], + }, + { + name: "@element-plus/icons-vue", + var: "ElementPlusIconsVue", + path: 'https://cdn.jsdelivr.net/npm/@element-plus/icons-vue@1.1.4/dist/index.iife.min.js', + }, + { + name: "element-plus/lib/locale/lang/zh-cn", + var: "ElementPlusLocaleZhCn", + path: 'https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.1.9/locale/zh-cn.min.js', + }, + { + name: "vant", + var: "vant", + path: 'https://cdnjs.cloudflare.com/ajax/libs/vant/3.4.8/vant.js', + css: ["https://cdnjs.cloudflare.com/ajax/libs/vant/3.4.8/index.min.css"], + } + ] + }), + ], + build: { + rollupOptions: { + input: { + index: path.resolve(__dirname, 'index.html'), + mobile: path.resolve(__dirname, 'mobile.html'), + }, output: { + chunkFileNames: 'static/js/[name]-[hash].js', + entryFileNames: "static/js/[name]-[hash].js", + assetFileNames: "static/[ext]/name-[hash].[ext]" + } + }, + } + } +}) diff --git a/imgs/1-home-search-keyword.png b/imgs/1-home-search-keyword.png new file mode 100644 index 0000000..c0c21ce Binary files /dev/null and b/imgs/1-home-search-keyword.png differ diff --git a/imgs/2-home-search-url.png b/imgs/2-home-search-url.png new file mode 100644 index 0000000..1a2449e Binary files /dev/null and b/imgs/2-home-search-url.png differ diff --git a/imgs/3-playlist.png b/imgs/3-playlist.png new file mode 100644 index 0000000..7666449 Binary files /dev/null and b/imgs/3-playlist.png differ diff --git a/imgs/4-playlist-search.png b/imgs/4-playlist-search.png new file mode 100644 index 0000000..c96cfee Binary files /dev/null and b/imgs/4-playlist-search.png differ diff --git a/imgs/billing.jpeg b/imgs/billing.jpeg new file mode 100644 index 0000000..ea9e69a Binary files /dev/null and b/imgs/billing.jpeg differ diff --git a/imgs/melody.png b/imgs/melody.png new file mode 100644 index 0000000..e495971 Binary files /dev/null and b/imgs/melody.png differ diff --git a/imgs/mobile-1.png b/imgs/mobile-1.png new file mode 100644 index 0000000..8b0231e Binary files /dev/null and b/imgs/mobile-1.png differ diff --git a/imgs/mobile-2.png b/imgs/mobile-2.png new file mode 100644 index 0000000..c99729e Binary files /dev/null and b/imgs/mobile-2.png differ diff --git a/imgs/mobile-3.png b/imgs/mobile-3.png new file mode 100644 index 0000000..eb660fc Binary files /dev/null and b/imgs/mobile-3.png differ diff --git a/imgs/mobile-4.png b/imgs/mobile-4.png new file mode 100644 index 0000000..081bc74 Binary files /dev/null and b/imgs/mobile-4.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..a90ca04 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "foamzou-melody", + "version": "0.1.2", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "init": "node scripts/setup.js", + "app": "node backend/src/index.js", + "update": "git pull && npm run init" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/foamzou/melody.git" + }, + "author": "foamzou", + "bugs": { + "url": "https://github.com/foamzou/melody/issues" + }, + "homepage": "https://github.com/foamzou/melody#readme" +} diff --git a/scripts/setup-for-build-docker.js b/scripts/setup-for-build-docker.js new file mode 100644 index 0000000..3944c55 --- /dev/null +++ b/scripts/setup-for-build-docker.js @@ -0,0 +1,70 @@ +const path = require('path'); +const fs = require('fs'); +const isWin = require('os').platform().indexOf('win32') > -1; +const ROOT_DIR = `${__dirname}/../`; +const l = m => console.log(m); + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +function getMediaGetBinPath() { + return path.join(ROOT_DIR, 'backend', 'bin', `media-get${isWin ? '.exe' : ''}`); +} + +async function downloadMediaGetWithRetry() { + const MediaGetService = require('../backend/src/service/media_fetcher/media_get'); + const maxRetries = 3; + let retryCount = 0; + + // Get latest version first + const latestVersion = await MediaGetService.getLatestMediaGetVersion(); + if (latestVersion === false) { + l('Failed to get latest media-get version'); + return false; + } + + while (retryCount < maxRetries) { + l(`Downloading media-get (attempt ${retryCount + 1})`); + const success = await MediaGetService.downloadTheLatestMediaGet(latestVersion); + if (success) { + return true; + } + retryCount++; + if (retryCount < maxRetries) { + l(`Download failed, waiting 5 seconds before retry...`); + await sleep(5000); + } + } + return false; +} + +async function run() { + try { + l('Starting media-get installation...'); + + const mediaGetPath = getMediaGetBinPath(); + if (!fs.existsSync(mediaGetPath)) { + l('Downloading media-get...'); + if (await downloadMediaGetWithRetry() === false) { + l('Failed to download media-get'); + return false; + } + l('Successfully downloaded media-get'); + } else { + l('media-get already exists'); + } + + return true; + } catch (error) { + l('Error during execution:'); + l(error.stack || error.message || error); + return false; + } +} + +run().then(success => { + if (!success) { + l('Setup failed'); + process.exit(1); + } + l('Setup completed successfully'); +}); \ No newline at end of file diff --git a/scripts/setup.js b/scripts/setup.js new file mode 100644 index 0000000..0b9ad25 --- /dev/null +++ b/scripts/setup.js @@ -0,0 +1,236 @@ +const exec = require('child_process').exec; +const https = require('https'); +const path = require('path'); +const fs = require('fs'); +const isWin = require('os').platform().indexOf('win32') > -1; +const isLinux = require('os').platform().indexOf('linux') > -1; +const isDarwin = require('os').platform().indexOf('darwin') > -1; +const ROOT_DIR = `${__dirname}/../`; +const l = m => console.log(m); + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +const runCmd = (cmd, shouldOutput = true, cwd = null) => { + const startTime = new Date(); + const option = cwd ? {cwd} : {}; + const currentCwd = cwd || process.cwd(); + l(`[${startTime.toISOString()}] 开始执行命令: ${cmd}`); + l(`执行目录: ${currentCwd}`); + + return new Promise(r => { + const childProcess = exec(cmd, option); + l(`进程ID: ${childProcess.pid}`); + let result = ''; + let error = ''; + + childProcess.stdout.on('data', function(data) { + shouldOutput && console.log(data); + result += data.toString(); + }); + + childProcess.stderr.on('data', (data) => { + shouldOutput && console.log(data); + error += data.toString(); + }) + + childProcess.on('exit', (code, signal) => { + const endTime = new Date(); + const duration = (endTime - startTime) / 1000; + l(`[${endTime.toISOString()}] 命令执行完成,耗时: ${duration}秒`); + if (signal) { + l(`进程被信号 ${signal} 终止`); + } + l(`退出码: ${code}`); + r({code, signal, result, error}) + }) + }); +} + +const runCmdAndExitWhenFailed = async (cmd, msg, shouldOutput = true, cwd = null) => { + l('----------------------------------------'); + l(`执行命令: ${cmd}`); + l(`工作目录: ${cwd || process.cwd()}`); + const ret = await runCmd(cmd, shouldOutput, cwd); + if (ret.code !== 0 || ret.code === null) { + l('命令执行失败:'); + l(msg); + l(`命令: ${cmd}`); + l(`工作目录: ${cwd || process.cwd()}`); + l('退出码: ' + ret.code); + l('错误输出: ' + ret.error); + l('标准输出: ' + ret.result); + l('系统信息:'); + l(`- 平台: ${process.platform}`); + l(`- 架构: ${process.arch}`); + l(`- Node版本: ${process.version}`); + l(`- 内存使用: ${JSON.stringify(process.memoryUsage())}`); + process.exit(1); + } + l('----------------------------------------'); + return ret; +} + +function getMediaGetBinPath() { + return path.join(ROOT_DIR, 'backend', 'bin', `media-get${isWin ? '.exe' : ''}`); +} + +async function checkAndUpdateMediaGet(currentMediaGetVersion) { + const MediaGetService = require('../backend/src/service/media_fetcher/media_get'); + + const latestVersion = await MediaGetService.getLatestMediaGetVersion(); + if (latestVersion === false) { + return; + } + if (currentMediaGetVersion.localeCompare(latestVersion, undefined, { numeric: true, sensitivity: 'base' }) >= 0) { + l('当前 media-get 版本已经是最新版本'); + return; + } + l(`当前 media-get(${currentMediaGetVersion})版本不是最新版本, 开始更新到${latestVersion}`); + await MediaGetService.downloadTheLatestMediaGet(latestVersion); +} + +function copyDir(src, dest) { + fs.mkdirSync(dest); + fs.readdirSync(src).forEach(file => { + const srcPath = path.join(src, file); + const destPath = path.join(dest, file); + if (fs.statSync(srcPath).isDirectory()) { + copyDir(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + }); +} + +async function getPackageManager() { + let ret = await runCmd('pnpm --version', false); + if (ret.code === 0) { + return 'pnpm'; + } + ret = await runCmd('yarn --version', false); + if (ret.code === 0) { + return 'yarn'; + } + + return 'npm'; +} + +async function downloadMediaGetWithRetry(latestVersion) { + const MediaGetService = require('../backend/src/service/media_fetcher/media_get'); + const maxRetries = 3; + let retryCount = 0; + + while (retryCount < maxRetries) { + l(`尝试下载 media-get (第 ${retryCount + 1} 次尝试)`); + const success = await MediaGetService.downloadTheLatestMediaGet(latestVersion); + if (success) { + return true; + } + retryCount++; + if (retryCount < maxRetries) { + l(`下载失败,等待 5 秒后重试...`); + await sleep(5000); + } + } + return false; +} + +async function run() { + try { + l('开始执行...'); + + // 检查 FFmpeg 安装和位置 + l('检查 FFmpeg 安装状态...'); + if (!process.env.CROSS_COMPILING) { + const ffmpegRet = await runCmd('which ffmpeg && ls -l $(which ffmpeg)', true); + l('FFmpeg location and permissions:'); + l(ffmpegRet.result); + await runCmdAndExitWhenFailed('ffmpeg -version', '请先安装 ffmpeg', false); + } else { + l('跳过 FFmpeg 检查 (CROSS_COMPILING=1)'); + } + + await runCmdAndExitWhenFailed('npm version', '请先安装 npm', false); + + const pm = await getPackageManager(); + l(`安装 node_module via ${pm}`) + l('开始安装后端依赖...'); + l(`执行命令: ${pm} install --production --verbose in ${path.join(ROOT_DIR, 'backend')}`); + const backendInstallResult = await runCmdAndExitWhenFailed(`${pm} install --production --verbose`, '安装后端 node_module 失败', true, path.join(ROOT_DIR, 'backend')); + l('后端依赖安装结果:'); + l('Exit code: ' + backendInstallResult.code); + l('Output: ' + backendInstallResult.result); + if (backendInstallResult.error) { + l('Error output: ' + backendInstallResult.error); + } + + l('检查 media-get'); + const MediaGetService = require('../backend/src/service/media_fetcher/media_get'); + + const mediaGetPath = getMediaGetBinPath(); + l(`检查 media-get 权限: ${mediaGetPath}`); + await runCmd(`ls -l ${mediaGetPath}`, true); + + if (!fs.existsSync(mediaGetPath)) { + const latestVersion = await MediaGetService.getLatestMediaGetVersion(); + if (latestVersion === false) { + l('获取 media-get 最新版本失败,无法继续安装'); + return false; + } + l('开始下载核心程序 media-get'); + if (await downloadMediaGetWithRetry(latestVersion) === false) { + l('下载核心程序 media-get 失败'); + return false; + } + } else { + const currentMediaGetVersion = await MediaGetService.getLatestMediaGetVersion(); + await checkAndUpdateMediaGet(currentMediaGetVersion); + } + + l('开始安装前端依赖...'); + const frontendInstallResult = await runCmdAndExitWhenFailed(`${pm} install --verbose`, '安装前端 node_module 失败', true, path.join(ROOT_DIR, 'frontend')); + l('前端依赖安装结果:'); + l('Exit code: ' + frontendInstallResult.code); + l('Output: ' + frontendInstallResult.result); + if (frontendInstallResult.error) { + l('Error output: ' + frontendInstallResult.error); + } + + l('开始编译前端...'); + const buildResult = await runCmdAndExitWhenFailed(`${pm} run build`, '前端编译失败', true, path.join(ROOT_DIR, 'frontend')); + l('前端编译结果:'); + l('Exit code: ' + buildResult.code); + l('Output: ' + buildResult.result); + if (buildResult.error) { + l('Error output: ' + buildResult.error); + } + + l('删除老目录'); + try { + fs.rmdirSync(path.join(ROOT_DIR, 'backend', 'public'), { recursive: true }); + } catch(e) { + l('删除老目录失败,但继续执行: ' + e.message); + } + + l('拷贝前端目录'); + try { + copyDir(path.join(ROOT_DIR, 'frontend', 'dist'), path.join(ROOT_DIR, 'backend', 'public')); + } catch(e) { + l('拷贝前端目录失败: ' + e.message); + return false; + } + + return true; + } catch (error) { + l('执行过程中出现错误:'); + l(error.stack || error.message || error); + return false; + } +} + +run().then(isFine => { + l(isFine ? `执行完毕,执行以下命令启动服务:\r\n\r\nnpm run app` : '执行出错,请检查'); + if (!isFine) { + process.exit(1); + } +}); \ No newline at end of file