Merge branch 'dev'
This commit is contained in:
+4
-1
@@ -1,2 +1,5 @@
|
||||
.env
|
||||
.env*.local
|
||||
.env*.local
|
||||
public/screenshot1.png
|
||||
public/screenshot2.png
|
||||
public/screenshot3.png
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
name: Build & Push Docker lite image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Docker 标签'
|
||||
required: false
|
||||
default: 'latest'
|
||||
type: string
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
os: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
os: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Prepare platform name
|
||||
run: |
|
||||
echo "PLATFORM_NAME=${{ matrix.platform }}" | sed 's|/|-|g' >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/mtvpls/moontvplus-lite
|
||||
tags: |
|
||||
type=raw,value=${{ github.event.inputs.tag || 'latest' }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.lite
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ghcr.io/mtvpls/moontvplus-lite:${{ github.event.inputs.tag || 'latest' }}
|
||||
outputs: type=image,name=ghcr.io/mtvpls/moontvplus-lite,name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_NAME }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create -t ghcr.io/mtvpls/moontvplus-lite:${{ github.event.inputs.tag || 'latest' }} \
|
||||
$(printf 'ghcr.io/mtvpls/moontvplus-lite@sha256:%s ' *)
|
||||
|
||||
cleanup-refresh:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- merge
|
||||
if: always()
|
||||
steps:
|
||||
- name: Delete workflow runs
|
||||
uses: Mattraks/delete-workflow-runs@main
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
retain_days: 0
|
||||
keep_minimum_runs: 2
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit "$1"
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm install
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
@@ -1,3 +1,25 @@
|
||||
## [216.0.0] - 2026-03-30
|
||||
### Added
|
||||
- 新增视频源脚本
|
||||
- 私人影库增加本机转码功能
|
||||
- tvbox订阅增加黄色过滤
|
||||
- 播放记录显示直链播放的链接
|
||||
- 磁链增加代理配置
|
||||
- 弹幕选集面板增强
|
||||
- 电视直播聚合同名节目
|
||||
- douban页面大屏自动预加载第二页
|
||||
- 增加docker lite镜像
|
||||
- 详情面板增加外部跳转
|
||||
|
||||
### Changed
|
||||
- GlobalError自动消失
|
||||
- 站点配置子服务配置项折叠
|
||||
|
||||
### Fixed
|
||||
- 修复手动选择弹幕因缓存问题无法变更弹幕集数
|
||||
- 修复当前集拉回开头显示恢复进度按钮
|
||||
- 修复视频源权重的一些问题
|
||||
|
||||
## [215.0.0] - 2026-03-20
|
||||
### Added
|
||||
- 增加主动恢复进度按钮
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# ---- 第 1 阶段:安装依赖 ----
|
||||
FROM node:24-alpine AS deps
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# ---- 第 2 阶段:构建项目 ----
|
||||
FROM node:24-alpine AS builder
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV DOCKER_ENV=true
|
||||
ENV MOONTV_LITE=true
|
||||
ENV WATCH_ROOM_ENABLED=false
|
||||
ENV WATCH_ROOM_SERVER_TYPE=external
|
||||
|
||||
RUN pnpm run build
|
||||
|
||||
# ---- 第 3 阶段:生成 lite 运行时镜像 ----
|
||||
FROM node:24-alpine AS runner
|
||||
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -u 1001 -S nextjs -G nodejs
|
||||
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV PORT=3000
|
||||
ENV DOCKER_ENV=true
|
||||
ENV MOONTV_LITE=true
|
||||
ENV WATCH_ROOM_ENABLED=false
|
||||
ENV WATCH_ROOM_SERVER_TYPE=external
|
||||
|
||||
# standalone 输出自带运行所需的最小服务端依赖
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/start.js ./start.js
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "start.js"]
|
||||
@@ -27,11 +27,13 @@
|
||||
- ✨ **视频超分 (Anime4K)**:使用 WebGPU 技术实现实时视频画质增强(支持 1.5x/2x/3x/4x 超分)
|
||||
- 💬 **弹幕系统**:完整的弹幕搜索、匹配、加载功能,支持弹幕设置持久化、弹幕屏蔽
|
||||
- 📝 **豆瓣评论抓取**:自动抓取并展示豆瓣电影短评,支持分页加载
|
||||
- 🧩 **视频源脚本**:支持通过脚本自定义视频源、搜索、详情与播放解析逻辑(实验性)
|
||||
- 🪒**自定义去广告**:你可以自定义你的去广告代码,实现更强力的去广告功能
|
||||
- 🚀 **更快更顺滑**:相较原版项目整体速度更快,交互体验更好
|
||||
- 🎭 **观影室**:支持多人同步观影、实时聊天、语音通话等功能(实验性)。
|
||||
- 📥 **M3U8完整下载**:通过合并m3u8片段实现完整视频下载。
|
||||
- 📥 **M3U8完整下载**:支持浏览器内合并 m3u8 片段下载,也支持下载到本地文件夹并无感播放本地视频。
|
||||
- 💾 **服务器离线下载**:支持在服务器端下载视频文件,支持断点续传,提前下载到家秒加载 。
|
||||
- 📚 **私人影库**:接入 OpenList或Emby,可打造专属私人影库,亦可观看网盘资源。
|
||||
- 📚 **私人影库**:接入 OpenList、Emby 或小雅,可打造专属私人影库,亦可观看网盘资源。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
@@ -283,6 +285,44 @@ services:
|
||||
- UPSTASH_TOKEN=上面的 TOKEN
|
||||
```
|
||||
|
||||
#### Lite 镜像说明
|
||||
|
||||
`ghcr.io/mtvpls/moontvplus-lite:latest` 为更小的镜像,但不支持启动内置观影室服务。
|
||||
|
||||
示例:
|
||||
|
||||
```yml
|
||||
services:
|
||||
moontv-core:
|
||||
image: ghcr.io/mtvpls/moontvplus-lite:latest
|
||||
container_name: moontv-core
|
||||
restart: on-failure
|
||||
ports:
|
||||
- '3000:3000'
|
||||
environment:
|
||||
- USERNAME=admin
|
||||
- PASSWORD=admin_password
|
||||
- NEXT_PUBLIC_STORAGE_TYPE=kvrocks
|
||||
- KVROCKS_URL=redis://moontv-kvrocks:6666
|
||||
networks:
|
||||
- moontv-network
|
||||
depends_on:
|
||||
- moontv-kvrocks
|
||||
moontv-kvrocks:
|
||||
image: apache/kvrocks
|
||||
container_name: moontv-kvrocks
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- kvrocks-data:/var/lib/kvrocks/data
|
||||
networks:
|
||||
- moontv-network
|
||||
networks:
|
||||
moontv-network:
|
||||
driver: bridge
|
||||
volumes:
|
||||
kvrocks-data:
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
完成部署后为空壳应用,无播放源,需要站长在管理后台的配置文件设置中填写配置文件,本版本已不支持无数据库运行。
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# 视频源脚本编写教程
|
||||
|
||||
## 最小模板
|
||||
|
||||
```js
|
||||
return {
|
||||
meta: {
|
||||
name: '示例脚本',
|
||||
author: 'admin'
|
||||
},
|
||||
|
||||
async getSources(ctx) {
|
||||
return [{ id: 'default', name: '默认源' }];
|
||||
},
|
||||
|
||||
async search(ctx, { keyword, page, sourceId }) {
|
||||
return {
|
||||
list: [],
|
||||
page,
|
||||
pageCount: 1,
|
||||
total: 0
|
||||
};
|
||||
},
|
||||
|
||||
async recommend(ctx, { page }) {
|
||||
return {
|
||||
list: [],
|
||||
page: page || 1,
|
||||
pageCount: 1,
|
||||
total: 0
|
||||
};
|
||||
},
|
||||
|
||||
async detail(ctx, { id, sourceId }) {
|
||||
return {
|
||||
id,
|
||||
title: '',
|
||||
poster: '',
|
||||
year: '',
|
||||
desc: '',
|
||||
playbacks: [
|
||||
{
|
||||
sourceId,
|
||||
sourceName: '默认源',
|
||||
episodes: [],
|
||||
episodes_titles: []
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
async resolvePlayUrl(ctx, { playUrl, sourceId, episodeIndex }) {
|
||||
return {
|
||||
url: playUrl,
|
||||
type: 'auto',
|
||||
headers: {}
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 支持的 hook
|
||||
|
||||
1. `getSources()`
|
||||
返回脚本管理的子源列表
|
||||
2. `search({ keyword, page, sourceId })`
|
||||
搜索
|
||||
3. `recommend({ page })`
|
||||
推荐
|
||||
4. `detail({ id, sourceId })`
|
||||
详情
|
||||
5. `resolvePlayUrl({ playUrl, sourceId, episodeIndex })`
|
||||
播放前解析最终地址
|
||||
|
||||
## `ctx` 里能用什么
|
||||
|
||||
1. `ctx.fetch(...)`
|
||||
发请求
|
||||
2. `ctx.request.get/getJson/getHtml/post`
|
||||
快捷请求
|
||||
3. `ctx.html.load(html)`
|
||||
`cheerio` 风格解析
|
||||
4. `ctx.cache.get/set/del`
|
||||
脚本缓存
|
||||
5. `ctx.log.info/warn/error`
|
||||
输出测试日志
|
||||
6. `ctx.utils.buildUrl/joinUrl/randomUA/sleep/base64Encode/base64Decode/now`
|
||||
常用工具
|
||||
7. `ctx.config.get/require/all`
|
||||
读脚本配置
|
||||
8. `ctx.runtime`
|
||||
当前脚本信息
|
||||
|
||||
## `search` 返回格式
|
||||
|
||||
```js
|
||||
{
|
||||
list: [
|
||||
{
|
||||
id: '123',
|
||||
title: '凡人修仙传',
|
||||
poster: 'https://...',
|
||||
year: '2025',
|
||||
desc: '简介',
|
||||
type_name: '动漫',
|
||||
douban_id: 0,
|
||||
vod_remarks: '更新至10集'
|
||||
}
|
||||
],
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
total: 1
|
||||
}
|
||||
```
|
||||
|
||||
## `detail` 返回格式
|
||||
|
||||
```js
|
||||
{
|
||||
id: '123',
|
||||
title: '凡人修仙传',
|
||||
poster: 'https://...',
|
||||
year: '2025',
|
||||
desc: '简介',
|
||||
playbacks: [
|
||||
{
|
||||
sourceId: 'default',
|
||||
sourceName: '默认源',
|
||||
episodes: [
|
||||
'https://example.com/play/1',
|
||||
{
|
||||
playUrl: 'https://example.com/play/2',
|
||||
needResolve: false
|
||||
}
|
||||
],
|
||||
episodes_titles: ['第1集', '第2集']
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `episodes` 可以是字符串,默认等价于 `{ playUrl: '...', needResolve: true }`
|
||||
- `needResolve` 默认为 `true`
|
||||
- 显式写 `needResolve: false` 时,播放页会直接使用该地址,不再调用 `resolvePlayUrl`
|
||||
|
||||
## `resolvePlayUrl` 返回格式
|
||||
|
||||
```js
|
||||
{
|
||||
url: 'https://real-url.m3u8',
|
||||
type: 'auto',
|
||||
headers: {}
|
||||
}
|
||||
```
|
||||
|
||||
## 最简单的搜索例子
|
||||
|
||||
```js
|
||||
async search(ctx, { keyword, page, sourceId }) {
|
||||
const data = await ctx.request.getJson('https://example.com/api/search', {
|
||||
query: { wd: keyword, pg: page }
|
||||
});
|
||||
|
||||
return {
|
||||
list: (data.list || []).map((item) => ({
|
||||
id: String(item.id),
|
||||
title: item.title,
|
||||
poster: item.pic || '',
|
||||
year: item.year || '',
|
||||
desc: item.desc || ''
|
||||
})),
|
||||
page,
|
||||
pageCount: data.pagecount || 1,
|
||||
total: data.total || 0
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 最简单的播放解析例子
|
||||
|
||||
```js
|
||||
async resolvePlayUrl(ctx, { playUrl }) {
|
||||
return {
|
||||
url: playUrl,
|
||||
type: 'auto',
|
||||
headers: {}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 导入格式
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "demo",
|
||||
"name": "演示脚本",
|
||||
"description": "test",
|
||||
"enabled": true,
|
||||
"code": "return { ... }"
|
||||
}
|
||||
```
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
215.0.0
|
||||
216.0.0
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
// TODO Add Scope Enum Here
|
||||
// 'scope-enum': [2, 'always', ['yourscope', 'yourscope']],
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'docs',
|
||||
'chore',
|
||||
'style',
|
||||
'refactor',
|
||||
'ci',
|
||||
'test',
|
||||
'perf',
|
||||
'revert',
|
||||
'vercel',
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
+1
-15
@@ -19,7 +19,6 @@
|
||||
"format:check": "prettier -c .",
|
||||
"gen:manifest": "node scripts/generate-manifest.js",
|
||||
"postbuild": "echo 'Build completed - sitemap generation disabled'",
|
||||
"prepare": "husky install",
|
||||
"watch-room:server": "node server/watch-room-standalone-server.js --port 3001 --auth YOUR_SECRET_KEY",
|
||||
"init:sqlite": "node scripts/init-sqlite.js",
|
||||
"init:postgres": "node scripts/init-postgres.js",
|
||||
@@ -78,8 +77,6 @@
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.3.0",
|
||||
"@commitlint/config-conventional": "^16.2.4",
|
||||
"@opennextjs/cloudflare": "^1.15.1",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
@@ -105,9 +102,7 @@
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.5.1",
|
||||
"lint-staged": "^12.5.0",
|
||||
"next-router-mock": "^0.9.0",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^2.8.8",
|
||||
@@ -117,14 +112,5 @@
|
||||
"vercel": "^50.4.10",
|
||||
"webpack-obfuscator": "^3.5.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint --max-warnings=0",
|
||||
"prettier -w"
|
||||
],
|
||||
"**/*.{json,css,scss,md,webmanifest}": [
|
||||
"prettier -w"
|
||||
]
|
||||
},
|
||||
"packageManager": "[email protected]"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+31
-902
@@ -159,12 +159,6 @@ importers:
|
||||
specifier: ^3.24.1
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@commitlint/cli':
|
||||
specifier: ^16.3.0
|
||||
version: 16.3.0
|
||||
'@commitlint/config-conventional':
|
||||
specifier: ^16.2.4
|
||||
version: 16.2.4
|
||||
'@opennextjs/cloudflare':
|
||||
specifier: ^1.15.1
|
||||
version: 1.15.1([email protected](@babel/[email protected])([email protected]([email protected]))([email protected]))([email protected]([email protected]))
|
||||
@@ -240,15 +234,9 @@ importers:
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected])([email protected]))([email protected])([email protected]))([email protected])
|
||||
husky:
|
||||
specifier: ^7.0.4
|
||||
version: 7.0.4
|
||||
jest:
|
||||
specifier: ^27.5.1
|
||||
version: 27.5.1([email protected])([email protected](@types/[email protected])([email protected]))
|
||||
lint-staged:
|
||||
specifier: ^12.5.0
|
||||
version: 12.5.0([email protected])
|
||||
next-router-mock:
|
||||
specifier: ^0.9.0
|
||||
version: 0.9.13([email protected](@babel/[email protected])([email protected]([email protected]))([email protected]))([email protected])
|
||||
@@ -1452,75 +1440,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-P+kvONlfsuTMnxSwWE1H+ZcPMY3STFaHb2kAacsqoIkNx66O0T7sTpBxpxkMrFPyhkJiLJnJWMhk4bbvYD3BMA==}
|
||||
engines: {node: '>=v12'}
|
||||
hasBin: true
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-av2UQJa3CuE5P0dzxj/o/B9XVALqYzEViHrMXtDrW9iuflrqCStWBAioijppj9URyz6ONpohJKAtSdgAOE0gkA==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-hogSe0WGg7CKmp4IfNbdNES3Rq3UEI4XRPB8JL4EPgo/ORq5nrGTVzxJh78omibNuB8Ho4501Czb1Er1MoDWpw==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-/h+lBTgf1r5fhbDNHOViLuej38i3rZqTQnBTk+xEg+ehOwQDXUuissQ5GsYXXqI5uGy+261ew++sT4EA3uBJ+A==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-oSls82fmUTLM6cl5V3epdVo4gHhbmBFvCvQGHBRdQ50H/690Uq1Dyd7hXMuKITCIdcnr9umyDkr8r5C6HZDF3g==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-Yyio9bdHWmNDRlEJrxHKglamIk3d6hC0NkEUW6Ti6ipEh2g0BAhy8Od6t4vLhdZRa1I2n+gY13foy+tUgk0i1Q==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-Lxdq9aOAYCOOOjKi58ulbwK/oBiiKz+7Sq0+/SpFIEFwhHkIVugvDvWjh2VRBXmRC/x5lNcjDcYEwS/uYUvlYQ==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-AUDuwOxb2eGqsXbTMON3imUGkc1jRdtXrbbohiLSCSk3jFVXgJLTMaEcr39pR00N8nE9uZ+V2sYaiILByZVmxQ==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-3tykjV/iwbkv2FU9DG+NZ/JqmP0Nm3b7aDwgCNQhhKV5P74JAuByULkafnhn+zsFGypG1qMtI5u+BZoa9APm0A==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-2eWX/47rftViYg7a3axYDdrgwKv32mxbycBJT6OQY/MJM7SUfYNYYvbMFOQFaA4xIVZt7t2Alyqslbl6blVwWw==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-2NP2dDQNL378VZYioLrgGVZhWdnJO4nAxQl5LXwYb08nEcN+cgxHN1dJV8OLJ5uxlGJtDeR8UZZ1mnQ1gSAD/g==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-tViXGuaxLTrw2r7PiYMQOFA2fueZxnnt0lkOWqKyxT+n2XdEMGYcI9ID5ndJKXnfPGPppD0w/IItKsIXlZ+alw==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-NbbCMPKTFf2J805kwfP9EO+vV+XvnaHRcBy6ud5dF35dxMsvdJqke54W3XazXF1ZAxC4a3LBy4i/GNVBAthsEg==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-rK5rNBIN2ZQNQK+I6trRPK3dWa0MtaTN4xnwOma1qxa4d5wQMQJtScwTZjTJeallFxhOgbNOgr48AMHkdounVg==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-9/VjpYj5j1QeY3eiog1zQWY6axsdWAc0AonUUfyZ7B0MVcRI0R56YsHAfzF6uK/g/WwPZaoe4Lb1QCyDVnpVaQ==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-lS6GSieHW9y6ePL73ied71Z9bOKyK+Ib9hTkRsB8oZFAyQZcyRwq2w6nIa6Fngir1QW51oKzzaXfJL94qwImyw==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
resolution: {integrity: sha512-7/z7pA7BM0i8XvMSBynO7xsB3mVQPUZbVn6zMIlp/a091XJ3qAXRXc+HwLYhiIdzzS5fuxxNIHZMGHVD4HJxdA==}
|
||||
engines: {node: '>=v12'}
|
||||
|
||||
'@cspotcode/[email protected]':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3260,9 +3179,6 @@ packages:
|
||||
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
|
||||
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
@@ -3281,15 +3197,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==}
|
||||
|
||||
@@ -3678,10 +3588,6 @@ packages:
|
||||
'@xtuc/[email protected]':
|
||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
deprecated: Use your platform's native atob() and btoa() methods instead
|
||||
@@ -3751,10 +3657,6 @@ packages:
|
||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
@@ -3864,9 +3766,6 @@ packages:
|
||||
resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3907,10 +3806,6 @@ packages:
|
||||
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3927,10 +3822,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4157,10 +4048,6 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4256,38 +4143,18 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
webpack: '>=4.0.0 <6.0.0'
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -4320,9 +4187,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -4349,10 +4213,6 @@ packages:
|
||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||
engines: {node: ^12.20.0 || >=14}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
@@ -4360,9 +4220,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
@@ -4386,19 +4243,6 @@ packages:
|
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4431,18 +4275,6 @@ packages:
|
||||
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KmE+bMjWMXJbkWCeY4FJX/npHuZPNr9XF9q9CIQ/bpFwi1qHfCmSiKarrCcRa0LO4fWjk93pVoeRtJAkTGcYNw==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
cosmiconfig: '>=7'
|
||||
typescript: '>=3'
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -4512,10 +4344,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4562,14 +4390,6 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
@@ -4694,10 +4514,6 @@ packages:
|
||||
resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -5216,10 +5032,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==}
|
||||
engines: {node: '>=14.14'}
|
||||
@@ -5304,11 +5116,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
|
||||
@@ -5345,10 +5152,6 @@ packages:
|
||||
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
|
||||
|
||||
@@ -5382,10 +5185,6 @@ packages:
|
||||
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5426,13 +5225,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5477,11 +5269,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -5619,10 +5406,6 @@ packages:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5668,10 +5451,6 @@ packages:
|
||||
resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5688,10 +5467,6 @@ packages:
|
||||
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -5730,10 +5505,6 @@ packages:
|
||||
resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6043,10 +5814,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
||||
engines: {'0': node >= 0.2.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6092,10 +5859,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -6103,20 +5866,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-BKLUjWDsKquV/JuIcoQW4MSAI3ggwEImF1+sB4zaKvyVx1wBk3FsG7UK9bpnmBTN1pm7EH2BBcMwINJzCRv12g==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
enquirer: '>= 2.3.0 < 3'
|
||||
peerDependenciesMeta:
|
||||
enquirer:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
|
||||
engines: {node: '>=6.11.5'}
|
||||
@@ -6145,10 +5894,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
@@ -6199,14 +5944,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
@@ -6300,10 +6037,6 @@ packages:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -6534,10 +6267,6 @@ packages:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
@@ -6743,13 +6472,6 @@ packages:
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6877,10 +6599,6 @@ packages:
|
||||
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -7007,11 +6725,6 @@ packages:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==}
|
||||
engines: {node: '>=0.10'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7247,14 +6960,6 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
|
||||
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
|
||||
deprecated: |-
|
||||
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
|
||||
|
||||
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
@@ -7265,10 +6970,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
@@ -7320,14 +7021,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -7408,10 +7101,6 @@ packages:
|
||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
@@ -7428,10 +7117,6 @@ packages:
|
||||
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -7440,9 +7125,6 @@ packages:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
@@ -7476,9 +7158,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -7519,19 +7198,10 @@ packages:
|
||||
resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7629,18 +7299,6 @@ packages:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||
|
||||
@@ -7693,21 +7351,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
|
||||
@@ -7748,10 +7391,6 @@ packages:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||
engines: {node: '>=0.6.19'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7891,10 +7530,6 @@ packages:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7995,10 +7630,6 @@ packages:
|
||||
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
@@ -8016,12 +7647,6 @@ packages:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8069,10 +7694,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
||||
|
||||
@@ -8155,10 +7776,6 @@ packages:
|
||||
resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8167,14 +7784,6 @@ packages:
|
||||
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -8369,9 +7978,6 @@ packages:
|
||||
resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -8592,10 +8198,6 @@ packages:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8694,10 +8296,6 @@ packages:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
|
||||
engines: {node: '>= 14.6'}
|
||||
@@ -8707,10 +8305,6 @@ packages:
|
||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||
@@ -8719,10 +8313,6 @@ packages:
|
||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||
@@ -10246,7 +9836,7 @@ snapshots:
|
||||
'@babel/types': 7.28.5
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
@@ -10298,7 +9888,7 @@ snapshots:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-compilation-targets': 7.27.2
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
lodash.debounce: 4.0.8
|
||||
resolve: 1.22.11
|
||||
transitivePeerDependencies:
|
||||
@@ -11018,7 +10608,7 @@ snapshots:
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/types': 7.28.5
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -11052,113 +10642,6 @@ snapshots:
|
||||
'@cloudflare/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/format': 16.2.1
|
||||
'@commitlint/lint': 16.2.4
|
||||
'@commitlint/load': 16.3.0
|
||||
'@commitlint/read': 16.2.1
|
||||
'@commitlint/types': 16.2.1
|
||||
lodash: 4.17.21
|
||||
resolve-from: 5.0.0
|
||||
resolve-global: 1.0.0
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- '@swc/wasm'
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
conventional-changelog-conventionalcommits: 4.6.3
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/types': 16.2.1
|
||||
ajv: 6.12.6
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/types': 16.2.1
|
||||
lodash: 4.17.21
|
||||
|
||||
'@commitlint/[email protected]': {}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/types': 16.2.1
|
||||
chalk: 4.1.2
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/types': 16.2.1
|
||||
semver: 7.3.7
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/is-ignored': 16.2.4
|
||||
'@commitlint/parse': 16.2.1
|
||||
'@commitlint/rules': 16.2.4
|
||||
'@commitlint/types': 16.2.1
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/config-validator': 16.2.1
|
||||
'@commitlint/execute-rule': 16.2.1
|
||||
'@commitlint/resolve-extends': 16.2.1
|
||||
'@commitlint/types': 16.2.1
|
||||
'@types/node': 24.0.3
|
||||
chalk: 4.1.2
|
||||
cosmiconfig: 7.1.0
|
||||
cosmiconfig-typescript-loader: 2.0.2(@types/[email protected])([email protected])([email protected])
|
||||
lodash: 4.17.21
|
||||
resolve-from: 5.0.0
|
||||
typescript: 4.9.5
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- '@swc/wasm'
|
||||
|
||||
'@commitlint/[email protected]': {}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/types': 16.2.1
|
||||
conventional-changelog-angular: 5.0.13
|
||||
conventional-commits-parser: 3.2.4
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/top-level': 16.2.1
|
||||
'@commitlint/types': 16.2.1
|
||||
fs-extra: 10.1.0
|
||||
git-raw-commits: 2.0.11
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/config-validator': 16.2.1
|
||||
'@commitlint/types': 16.2.1
|
||||
import-fresh: 3.3.1
|
||||
lodash: 4.17.21
|
||||
resolve-from: 5.0.0
|
||||
resolve-global: 1.0.0
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
'@commitlint/ensure': 16.2.1
|
||||
'@commitlint/message': 16.2.1
|
||||
'@commitlint/to-lines': 16.2.1
|
||||
'@commitlint/types': 16.2.1
|
||||
execa: 5.1.1
|
||||
|
||||
'@commitlint/[email protected]': {}
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
find-up: 5.0.0
|
||||
|
||||
'@commitlint/[email protected]':
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
|
||||
'@cspotcode/[email protected]':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
@@ -11402,7 +10885,7 @@ snapshots:
|
||||
'@eslint/[email protected]':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
espree: 9.6.1
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.2
|
||||
@@ -11459,7 +10942,7 @@ snapshots:
|
||||
'@humanwhocodes/[email protected]':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -13094,8 +12577,6 @@ snapshots:
|
||||
dependencies:
|
||||
minimatch: 10.1.1
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
@@ -13122,12 +12603,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/node': 24.0.3
|
||||
@@ -13198,7 +12675,7 @@ snapshots:
|
||||
'@typescript-eslint/scope-manager': 5.62.0
|
||||
'@typescript-eslint/type-utils': 5.62.0([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 5.62.0([email protected])([email protected])
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
eslint: 8.57.1
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.2
|
||||
@@ -13215,7 +12692,7 @@ snapshots:
|
||||
'@typescript-eslint/scope-manager': 5.62.0
|
||||
'@typescript-eslint/types': 5.62.0
|
||||
'@typescript-eslint/typescript-estree': 5.62.0([email protected])
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
eslint: 8.57.1
|
||||
optionalDependencies:
|
||||
typescript: 4.9.5
|
||||
@@ -13231,7 +12708,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 5.62.0([email protected])
|
||||
'@typescript-eslint/utils': 5.62.0([email protected])([email protected])
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
eslint: 8.57.1
|
||||
tsutils: 3.21.0([email protected])
|
||||
optionalDependencies:
|
||||
@@ -13245,7 +12722,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.62.0
|
||||
'@typescript-eslint/visitor-keys': 5.62.0
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.7.3
|
||||
@@ -13714,11 +13191,6 @@ snapshots:
|
||||
|
||||
'@xtuc/[email protected]': {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
jsonparse: 1.3.1
|
||||
through: 2.3.8
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -13766,7 +13238,7 @@ snapshots:
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -13776,11 +13248,6 @@ snapshots:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
clean-stack: 2.2.0
|
||||
indent-string: 4.0.0
|
||||
|
||||
[email protected]([email protected]):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -13875,8 +13342,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -13947,8 +13412,6 @@ snapshots:
|
||||
get-intrinsic: 1.3.0
|
||||
is-array-buffer: 3.0.5
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -13967,8 +13430,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -14139,7 +13600,7 @@ snapshots:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.5
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.2
|
||||
on-finished: 2.4.1
|
||||
@@ -14228,12 +13689,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
map-obj: 4.3.0
|
||||
quick-lru: 4.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -14329,27 +13784,11 @@ snapshots:
|
||||
libphonenumber-js: 1.12.33
|
||||
validator: 13.15.26
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
del: 4.1.1
|
||||
webpack: 5.104.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
restore-cursor: 3.1.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
slice-ansi: 3.0.0
|
||||
string-width: 4.2.3
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
slice-ansi: 5.0.0
|
||||
string-width: 5.1.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -14358,12 +13797,6 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
string-width: 7.2.0
|
||||
@@ -14398,8 +13831,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
@@ -14416,17 +13847,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
array-ify: 1.0.0
|
||||
dot-prop: 5.3.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -14449,26 +13873,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
compare-func: 2.0.0
|
||||
q: 1.5.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
compare-func: 2.0.0
|
||||
lodash: 4.17.21
|
||||
q: 1.5.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
JSONStream: 1.3.5
|
||||
is-text-path: 1.0.1
|
||||
lodash: 4.17.21
|
||||
meow: 8.1.2
|
||||
split2: 3.2.2
|
||||
through2: 4.0.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -14492,24 +13896,6 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
vary: 1.1.2
|
||||
|
||||
[email protected](@types/[email protected])([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@types/node': 24.0.3
|
||||
cosmiconfig: 7.1.0
|
||||
ts-node: 10.9.2(@types/[email protected])([email protected])
|
||||
typescript: 4.9.5
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- '@swc/wasm'
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@types/parse-json': 4.0.2
|
||||
import-fresh: 3.3.1
|
||||
parse-json: 5.2.0
|
||||
path-type: 4.0.0
|
||||
yaml: 1.10.2
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
import-fresh: 3.3.1
|
||||
@@ -14573,8 +13959,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
abab: 2.0.6
|
||||
@@ -14611,18 +13995,9 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
[email protected]([email protected]):
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 9.4.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
decamelize: 1.2.0
|
||||
map-obj: 1.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -14737,10 +14112,6 @@ snapshots:
|
||||
dependencies:
|
||||
type-fest: 5.3.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-obj: 2.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -14808,7 +14179,7 @@ snapshots:
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
engine.io-parser: 5.2.3
|
||||
ws: 8.18.3([email protected])
|
||||
xmlhttprequest-ssl: 2.1.2
|
||||
@@ -14827,7 +14198,7 @@ snapshots:
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
cors: 2.8.5
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
engine.io-parser: 5.2.3
|
||||
ws: 8.18.3([email protected])
|
||||
transitivePeerDependencies:
|
||||
@@ -15074,7 +14445,7 @@ snapshots:
|
||||
[email protected]([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
eslint: 8.57.1
|
||||
get-tsconfig: 4.13.0
|
||||
is-bun-module: 2.0.0
|
||||
@@ -15216,7 +14587,7 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
doctrine: 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 7.2.2
|
||||
@@ -15335,7 +14706,7 @@ snapshots:
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.2.2
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
depd: 2.0.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
@@ -15418,7 +14789,7 @@ snapshots:
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
@@ -15505,12 +14876,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.2.0
|
||||
universalify: 2.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -15596,14 +14961,6 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
dargs: 7.0.0
|
||||
lodash: 4.17.21
|
||||
meow: 8.1.2
|
||||
split2: 3.2.2
|
||||
through2: 4.0.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -15655,10 +15012,6 @@ snapshots:
|
||||
minipass: 4.2.8
|
||||
path-scurry: 1.11.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ini: 1.3.8
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
min-document: 2.19.2
|
||||
@@ -15700,8 +15053,6 @@ snapshots:
|
||||
dependencies:
|
||||
duplexer: 0.1.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -15752,12 +15103,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
whatwg-encoding: 1.0.5
|
||||
@@ -15793,21 +15138,21 @@ snapshots:
|
||||
dependencies:
|
||||
'@tootallnate/once': 1.1.2
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -15819,8 +15164,6 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -15955,8 +15298,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -15995,8 +15336,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -16009,8 +15348,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -16045,10 +15382,6 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
safe-regex-test: 1.1.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
text-extensions: 1.9.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
which-typed-array: 1.1.19
|
||||
@@ -16092,7 +15425,7 @@ snapshots:
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
source-map: 0.6.1
|
||||
transitivePeerDependencies:
|
||||
@@ -16646,8 +15979,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -16687,44 +16018,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
cli-truncate: 3.1.0
|
||||
colorette: 2.0.20
|
||||
commander: 9.5.0
|
||||
debug: 4.4.3([email protected])
|
||||
execa: 5.1.1
|
||||
lilconfig: 2.0.5
|
||||
listr2: 4.0.5([email protected])
|
||||
micromatch: 4.0.8
|
||||
normalize-path: 3.0.0
|
||||
object-inspect: 1.13.4
|
||||
pidtree: 0.5.0
|
||||
string-argv: 0.3.2
|
||||
supports-color: 9.4.0
|
||||
yaml: 1.10.2
|
||||
transitivePeerDependencies:
|
||||
- enquirer
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
cli-truncate: 2.1.0
|
||||
colorette: 2.0.20
|
||||
log-update: 4.0.0
|
||||
p-map: 4.0.0
|
||||
rfdc: 1.4.1
|
||||
rxjs: 7.8.2
|
||||
through: 2.3.8
|
||||
wrap-ansi: 7.0.0
|
||||
optionalDependencies:
|
||||
enquirer: 2.4.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -16749,13 +16046,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-escapes: 4.3.2
|
||||
cli-cursor: 3.1.0
|
||||
slice-ansi: 4.0.0
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -16802,10 +16092,6 @@ snapshots:
|
||||
dependencies:
|
||||
tmpl: 1.0.5
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -17010,20 +16296,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@types/minimist': 1.2.5
|
||||
camelcase-keys: 6.2.2
|
||||
decamelize-keys: 1.1.1
|
||||
hard-rejection: 2.1.0
|
||||
minimist-options: 4.1.0
|
||||
normalize-package-data: 3.0.3
|
||||
read-pkg-up: 7.0.1
|
||||
redent: 3.0.0
|
||||
trim-newlines: 3.0.1
|
||||
type-fest: 0.18.1
|
||||
yargs-parser: 20.2.9
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -17319,7 +16591,7 @@ snapshots:
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
decode-named-character-reference: 1.2.0
|
||||
micromark-core-commonmark: 1.1.0
|
||||
micromark-factory-space: 1.1.0
|
||||
@@ -17341,7 +16613,7 @@ snapshots:
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
decode-named-character-reference: 1.2.0
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.3
|
||||
@@ -17424,12 +16696,6 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
arrify: 1.0.1
|
||||
is-plain-obj: 1.1.0
|
||||
kind-of: 6.0.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -17604,20 +16870,6 @@ snapshots:
|
||||
dependencies:
|
||||
abbrev: 3.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
hosted-git-info: 2.8.9
|
||||
resolve: 1.22.11
|
||||
semver: 5.7.2
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
hosted-git-info: 4.1.0
|
||||
is-core-module: 2.16.1
|
||||
semver: 7.7.3
|
||||
validate-npm-package-license: 3.0.4
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -17755,10 +17007,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
aggregate-error: 3.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -17873,8 +17121,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -18041,8 +17287,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
@@ -18051,8 +17295,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@@ -18122,19 +17364,6 @@ snapshots:
|
||||
dependencies:
|
||||
pify: 2.3.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
find-up: 4.1.0
|
||||
read-pkg: 5.2.0
|
||||
type-fest: 0.8.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@types/normalize-package-data': 2.4.4
|
||||
normalize-package-data: 2.5.0
|
||||
parse-json: 5.2.0
|
||||
type-fest: 0.6.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
@@ -18244,10 +17473,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
global-dirs: 0.1.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -18264,17 +17489,10 @@ snapshots:
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
@@ -18316,7 +17534,7 @@ snapshots:
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
depd: 2.0.0
|
||||
is-promise: 4.0.0
|
||||
parseurl: 1.3.3
|
||||
@@ -18328,10 +17546,6 @@ snapshots:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
mri: 1.2.0
|
||||
@@ -18382,14 +17596,8 @@ snapshots:
|
||||
ajv-formats: 2.1.1([email protected])
|
||||
ajv-keywords: 5.1.0([email protected])
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
@@ -18398,7 +17606,7 @@ snapshots:
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
@@ -18540,23 +17748,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
astral-regex: 2.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
astral-regex: 2.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
is-fullwidth-code-point: 4.0.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
@@ -18564,7 +17755,7 @@ snapshots:
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
ws: 8.18.3([email protected])
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -18574,7 +17765,7 @@ snapshots:
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
engine.io-client: 6.6.4([email protected])
|
||||
socket.io-parser: 4.2.5
|
||||
transitivePeerDependencies:
|
||||
@@ -18585,7 +17776,7 @@ snapshots:
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -18594,7 +17785,7 @@ snapshots:
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cors: 2.8.5
|
||||
debug: 4.4.3([email protected])
|
||||
debug: 4.4.3
|
||||
engine.io: 6.6.5([email protected])
|
||||
socket.io-adapter: 2.5.6([email protected])
|
||||
socket.io-parser: 4.2.5
|
||||
@@ -18628,24 +17819,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
spdx-expression-parse: 3.0.1
|
||||
spdx-license-ids: 3.0.22
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
spdx-exceptions: 2.5.0
|
||||
spdx-license-ids: 3.0.22
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
readable-stream: 3.6.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -18681,8 +17854,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
char-regex: 1.0.2
|
||||
@@ -18846,8 +18017,6 @@ snapshots:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
@@ -18982,8 +18151,6 @@ snapshots:
|
||||
glob: 7.2.3
|
||||
minimatch: 3.1.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -18998,12 +18165,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
readable-stream: 3.6.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
convert-hrtime: 3.0.0
|
||||
@@ -19046,8 +18207,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -19092,6 +18251,7 @@ snapshots:
|
||||
typescript: 4.9.5
|
||||
v8-compile-cache-lib: 3.0.1
|
||||
yn: 3.1.1
|
||||
optional: true
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -19136,16 +18296,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -19389,11 +18543,6 @@ snapshots:
|
||||
convert-source-map: 1.9.0
|
||||
source-map: 0.7.6
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
spdx-correct: 3.2.0
|
||||
spdx-expression-parse: 3.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -19770,12 +18919,6 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -19846,14 +18989,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -19866,16 +19005,6 @@ snapshots:
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 20.2.9
|
||||
|
||||
[email protected]:
|
||||
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
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
cliui: 9.0.1
|
||||
|
||||
+983
-302
@@ -45,7 +45,7 @@ import {
|
||||
Video,
|
||||
} from 'lucide-react';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import { memo, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
|
||||
@@ -317,6 +317,18 @@ const useLoadingState = () => {
|
||||
return { loadingStates, setLoading, isLoading, withLoading };
|
||||
};
|
||||
|
||||
interface StandaloneSourceScript {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
version: string;
|
||||
code: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// 新增站点配置类型
|
||||
interface SiteConfig {
|
||||
SiteName: string;
|
||||
@@ -340,6 +352,10 @@ interface SiteConfig {
|
||||
PansouUsername?: string;
|
||||
PansouPassword?: string;
|
||||
PansouKeywordBlocklist?: string;
|
||||
MagnetProxy?: string;
|
||||
MagnetMikanReverseProxy?: string;
|
||||
MagnetDmhyReverseProxy?: string;
|
||||
MagnetAcgripReverseProxy?: string;
|
||||
EnableComments: boolean;
|
||||
EnableRegistration?: boolean;
|
||||
RegistrationRequireTurnstile?: boolean;
|
||||
@@ -5489,6 +5505,7 @@ const VideoSourceConfig = ({
|
||||
message={alertModal.message}
|
||||
timer={alertModal.timer}
|
||||
showConfirm={alertModal.showConfirm}
|
||||
onConfirm={alertModal.onConfirm}
|
||||
/>
|
||||
|
||||
{/* 批量操作确认弹窗 */}
|
||||
@@ -5953,6 +5970,540 @@ const CategoryConfig = ({
|
||||
message={alertModal.message}
|
||||
timer={alertModal.timer}
|
||||
showConfirm={alertModal.showConfirm}
|
||||
onConfirm={alertModal.onConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VideoSourceScriptLab = () => {
|
||||
const { alertModal, showAlert, hideAlert } = useAlertModal();
|
||||
const { isLoading, withLoading } = useLoadingState();
|
||||
const [scripts, setScripts] = useState<StandaloneSourceScript[]>([]);
|
||||
const [loadingScripts, setLoadingScripts] = useState(true);
|
||||
const [template, setTemplate] = useState('');
|
||||
const [selectedScriptId, setSelectedScriptId] = useState<string | null>(null);
|
||||
const [editor, setEditor] = useState<{
|
||||
id?: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
code: string;
|
||||
enabled: boolean;
|
||||
version?: string;
|
||||
updatedAt?: number;
|
||||
}>({
|
||||
key: '',
|
||||
name: '',
|
||||
description: '',
|
||||
code: '',
|
||||
enabled: true,
|
||||
});
|
||||
const [testHook, setTestHook] = useState<'getSources' | 'search' | 'recommend' | 'detail' | 'resolvePlayUrl'>('getSources');
|
||||
const [testPayload, setTestPayload] = useState(
|
||||
JSON.stringify({}, null, 2)
|
||||
);
|
||||
const [testOutput, setTestOutput] = useState('');
|
||||
const importInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const applyEditorFromScript = (script: StandaloneSourceScript | null) => {
|
||||
if (!script) {
|
||||
setEditor({
|
||||
key: '',
|
||||
name: '',
|
||||
description: '',
|
||||
code: template,
|
||||
enabled: true,
|
||||
});
|
||||
setSelectedScriptId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setEditor({
|
||||
id: script.id,
|
||||
key: script.key,
|
||||
name: script.name,
|
||||
description: script.description || '',
|
||||
code: script.code,
|
||||
enabled: script.enabled,
|
||||
version: script.version,
|
||||
updatedAt: script.updatedAt,
|
||||
});
|
||||
setSelectedScriptId(script.id);
|
||||
};
|
||||
|
||||
const loadScripts = async (preferId?: string | null) => {
|
||||
setLoadingScripts(true);
|
||||
try {
|
||||
const response = await fetch('/api/admin/source-script', {
|
||||
cache: 'no-store',
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '加载脚本失败');
|
||||
}
|
||||
|
||||
const nextScripts = (data.items || []) as StandaloneSourceScript[];
|
||||
setScripts(nextScripts);
|
||||
setTemplate(data.template || '');
|
||||
|
||||
const targetId =
|
||||
preferId !== undefined
|
||||
? preferId
|
||||
: selectedScriptId || nextScripts[0]?.id || null;
|
||||
|
||||
const selected = nextScripts.find((item) => item.id === targetId) || null;
|
||||
if (selected) {
|
||||
applyEditorFromScript(selected);
|
||||
} else {
|
||||
setEditor({
|
||||
key: '',
|
||||
name: '',
|
||||
description: '',
|
||||
code: data.template || '',
|
||||
enabled: true,
|
||||
});
|
||||
setSelectedScriptId(null);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '加载脚本失败', showAlert);
|
||||
} finally {
|
||||
setLoadingScripts(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadScripts();
|
||||
}, []);
|
||||
|
||||
const handleCreateNew = () => {
|
||||
setSelectedScriptId(null);
|
||||
setEditor({
|
||||
key: '',
|
||||
name: '',
|
||||
description: '',
|
||||
code: template,
|
||||
enabled: true,
|
||||
});
|
||||
setTestOutput('');
|
||||
};
|
||||
|
||||
const handleExportCurrent = () => {
|
||||
if (!editor.key || !editor.name || !editor.code) {
|
||||
showError('当前没有可导出的脚本', showAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
key: editor.key,
|
||||
name: editor.name,
|
||||
description: editor.description,
|
||||
code: editor.code,
|
||||
enabled: editor.enabled,
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
||||
type: 'application/json',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${editor.key}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleImportFile = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const raw = await file.text();
|
||||
const parsed = JSON.parse(raw);
|
||||
const items = Array.isArray(parsed) ? parsed : [parsed];
|
||||
|
||||
await withLoading('importSourceScript', async () => {
|
||||
const response = await fetch('/api/admin/source-script', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'import',
|
||||
items,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '导入失败');
|
||||
}
|
||||
|
||||
showSuccess(`已导入 ${data.items?.length || 0} 个脚本`, showAlert);
|
||||
await loadScripts(data.items?.[0]?.id || null);
|
||||
});
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '导入失败', showAlert);
|
||||
} finally {
|
||||
event.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editor.key || !editor.name || !editor.code) {
|
||||
showError('请填写脚本 Key、名称和代码', showAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
await withLoading('saveSourceScript', async () => {
|
||||
const response = await fetch('/api/admin/source-script', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'save',
|
||||
id: editor.id,
|
||||
key: editor.key,
|
||||
name: editor.name,
|
||||
description: editor.description,
|
||||
code: editor.code,
|
||||
enabled: editor.enabled,
|
||||
}),
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '保存失败');
|
||||
}
|
||||
|
||||
showSuccess('脚本已保存', showAlert);
|
||||
await loadScripts(data.item?.id || editor.id || null);
|
||||
}).catch((error) => {
|
||||
showError(error instanceof Error ? error.message : '保存失败', showAlert);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!editor.id) {
|
||||
handleCreateNew();
|
||||
return;
|
||||
}
|
||||
|
||||
showAlert({
|
||||
type: 'warning',
|
||||
title: '删除脚本',
|
||||
message: `确定要删除脚本 "${editor.name}" 吗?`,
|
||||
showConfirm: true,
|
||||
onConfirm: async () => {
|
||||
hideAlert();
|
||||
await withLoading('deleteSourceScript', async () => {
|
||||
const response = await fetch('/api/admin/source-script', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'delete',
|
||||
id: editor.id,
|
||||
}),
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '删除失败');
|
||||
}
|
||||
showSuccess('脚本已删除', showAlert);
|
||||
await loadScripts(null);
|
||||
}).catch((error) => {
|
||||
showError(error instanceof Error ? error.message : '删除失败', showAlert);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleEnabled = async (id: string) => {
|
||||
await withLoading(`toggleSourceScript_${id}`, async () => {
|
||||
const response = await fetch('/api/admin/source-script', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'toggle_enabled',
|
||||
id,
|
||||
}),
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '更新失败');
|
||||
}
|
||||
await loadScripts(id);
|
||||
}).catch((error) => {
|
||||
showError(error instanceof Error ? error.message : '更新失败', showAlert);
|
||||
});
|
||||
};
|
||||
|
||||
const handleTest = async () => {
|
||||
let payload = {};
|
||||
try {
|
||||
payload = testPayload.trim() ? JSON.parse(testPayload) : {};
|
||||
} catch {
|
||||
showError('测试输入必须是合法 JSON', showAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
await withLoading('testSourceScript', async () => {
|
||||
const response = await fetch('/api/admin/source-script', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'test',
|
||||
key: editor.key || 'test-script',
|
||||
name: editor.name || '测试脚本',
|
||||
code: editor.code,
|
||||
hook: testHook,
|
||||
payload,
|
||||
}),
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
setTestOutput(JSON.stringify(data, null, 2));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.message || '测试失败');
|
||||
}
|
||||
showSuccess('测试执行完成', showAlert);
|
||||
}).catch((error) => {
|
||||
showError(error instanceof Error ? error.message : '测试失败', showAlert);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTestPayload(
|
||||
testHook === 'getSources'
|
||||
? JSON.stringify({}, null, 2)
|
||||
: testHook === 'search'
|
||||
? JSON.stringify({ keyword: '凡人修仙传', page: 1, sourceId: 'main' }, null, 2)
|
||||
: testHook === 'recommend'
|
||||
? JSON.stringify({ page: 1 }, null, 2)
|
||||
: testHook === 'detail'
|
||||
? JSON.stringify({ id: 'demo-id', sourceId: 'main' }, null, 2)
|
||||
: JSON.stringify(
|
||||
{
|
||||
sourceId: 'main',
|
||||
playUrl: 'https://example.com/video.m3u8',
|
||||
episodeIndex: 0,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}, [testHook]);
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<div className='flex flex-col lg:flex-row gap-6'>
|
||||
<div className='lg:w-80 space-y-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
脚本列表
|
||||
</h4>
|
||||
<div className='flex items-center gap-2'>
|
||||
<input
|
||||
ref={importInputRef}
|
||||
type='file'
|
||||
accept='application/json,.json'
|
||||
onChange={handleImportFile}
|
||||
className='hidden'
|
||||
/>
|
||||
<button
|
||||
onClick={() => importInputRef.current?.click()}
|
||||
disabled={isLoading('importSourceScript')}
|
||||
className={isLoading('importSourceScript') ? buttonStyles.disabledSmall : buttonStyles.primarySmall}
|
||||
>
|
||||
导入
|
||||
</button>
|
||||
<button
|
||||
onClick={() => loadScripts(selectedScriptId)}
|
||||
disabled={loadingScripts}
|
||||
className={loadingScripts ? buttonStyles.disabledSmall : buttonStyles.secondarySmall}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
<button onClick={handleCreateNew} className={buttonStyles.successSmall}>
|
||||
新建
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-3 max-h-[38rem] overflow-y-auto pr-1'>
|
||||
{loadingScripts ? (
|
||||
<div className='text-sm text-gray-500 dark:text-gray-400'>加载中...</div>
|
||||
) : scripts.length === 0 ? (
|
||||
<div className='p-4 rounded-lg border border-dashed border-gray-300 dark:border-gray-700 text-sm text-gray-500 dark:text-gray-400'>
|
||||
还没有脚本,点右上角新建一个。
|
||||
</div>
|
||||
) : (
|
||||
scripts.map((script) => (
|
||||
<button
|
||||
key={script.id}
|
||||
onClick={() => {
|
||||
applyEditorFromScript(script);
|
||||
setTestOutput('');
|
||||
}}
|
||||
className={`w-full text-left p-4 rounded-xl border transition-colors ${
|
||||
selectedScriptId === script.id
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900'
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='min-w-0'>
|
||||
<div className='font-medium text-gray-900 dark:text-gray-100 truncate'>
|
||||
{script.name}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 dark:text-gray-400 truncate'>
|
||||
{script.key}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs rounded-full ${
|
||||
script.enabled
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{script.enabled ? '启用' : '停用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className='mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400'>
|
||||
<span>{new Date(script.updatedAt).toLocaleString('zh-CN')}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleEnabled(script.id);
|
||||
}}
|
||||
disabled={isLoading(`toggleSourceScript_${script.id}`)}
|
||||
className={script.enabled ? buttonStyles.warningSmall : buttonStyles.successSmall}
|
||||
>
|
||||
{script.enabled ? '停用' : '启用'}
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 space-y-6'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='脚本名称'
|
||||
value={editor.name}
|
||||
onChange={(e) => setEditor((prev) => ({ ...prev, name: e.target.value }))}
|
||||
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||
/>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='脚本 Key'
|
||||
value={editor.key}
|
||||
onChange={(e) => setEditor((prev) => ({ ...prev, key: e.target.value }))}
|
||||
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
placeholder='脚本描述(可选)'
|
||||
value={editor.description}
|
||||
onChange={(e) => setEditor((prev) => ({ ...prev, description: e.target.value }))}
|
||||
rows={2}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<label className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
脚本代码
|
||||
</label>
|
||||
<div className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
{editor.version ? `当前版本: ${editor.version}` : '未保存'}
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
value={editor.code}
|
||||
onChange={(e) => setEditor((prev) => ({ ...prev, code: e.target.value }))}
|
||||
rows={24}
|
||||
spellCheck={false}
|
||||
className='w-full px-3 py-3 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-950 text-gray-100'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap items-center gap-3'>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isLoading('saveSourceScript')}
|
||||
className={isLoading('saveSourceScript') ? buttonStyles.disabled : buttonStyles.success}
|
||||
>
|
||||
{isLoading('saveSourceScript') ? '保存中...' : '保存脚本'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleTest}
|
||||
disabled={isLoading('testSourceScript')}
|
||||
className={isLoading('testSourceScript') ? buttonStyles.disabled : buttonStyles.primary}
|
||||
>
|
||||
{isLoading('testSourceScript') ? '测试中...' : '运行测试'}
|
||||
</button>
|
||||
<button onClick={handleExportCurrent} className={buttonStyles.secondary}>
|
||||
导出当前脚本
|
||||
</button>
|
||||
<button onClick={handleDelete} className={buttonStyles.danger}>
|
||||
{editor.id ? '删除脚本' : '清空编辑器'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 xl:grid-cols-2 gap-6'>
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<label className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
测试 Hook
|
||||
</label>
|
||||
<select
|
||||
value={testHook}
|
||||
onChange={(e) => setTestHook(e.target.value as 'getSources' | 'search' | 'recommend' | 'detail' | 'resolvePlayUrl')}
|
||||
className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||
>
|
||||
<option value='getSources'>getSources</option>
|
||||
<option value='search'>search</option>
|
||||
<option value='recommend'>recommend</option>
|
||||
<option value='detail'>detail</option>
|
||||
<option value='resolvePlayUrl'>resolvePlayUrl</option>
|
||||
</select>
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
现在脚本可以自己管理多个源,测试入参可传 `sourceId`。
|
||||
</p>
|
||||
<textarea
|
||||
value={testPayload}
|
||||
onChange={(e) => setTestPayload(e.target.value)}
|
||||
rows={10}
|
||||
spellCheck={false}
|
||||
className='w-full px-3 py-3 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='space-y-3'>
|
||||
<div className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
测试输出
|
||||
</div>
|
||||
<pre className='w-full min-h-[16rem] whitespace-pre-wrap break-all px-3 py-3 text-xs border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-950 text-gray-100 overflow-auto'>
|
||||
{testOutput || '运行测试后会显示结果、日志和错误信息'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
onClose={hideAlert}
|
||||
type={alertModal.type}
|
||||
title={alertModal.title}
|
||||
message={alertModal.message}
|
||||
timer={alertModal.timer}
|
||||
showConfirm={alertModal.showConfirm}
|
||||
onConfirm={alertModal.onConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -7374,12 +7925,17 @@ const SiteConfigComponent = ({
|
||||
DanmakuApiToken: '87654321',
|
||||
TMDBApiKey: '',
|
||||
TMDBProxy: '',
|
||||
TMDBReverseProxy: '',
|
||||
BannerDataSource: 'Douban',
|
||||
RecommendationDataSource: 'Mixed',
|
||||
PansouApiUrl: '',
|
||||
PansouUsername: '',
|
||||
PansouPassword: '',
|
||||
PansouKeywordBlocklist: '',
|
||||
MagnetProxy: '',
|
||||
MagnetMikanReverseProxy: '',
|
||||
MagnetDmhyReverseProxy: '',
|
||||
MagnetAcgripReverseProxy: '',
|
||||
EnableComments: false,
|
||||
EnableRegistration: false,
|
||||
RegistrationRequireTurnstile: false,
|
||||
@@ -7465,12 +8021,17 @@ const SiteConfigComponent = ({
|
||||
DanmakuApiToken: config.SiteConfig.DanmakuApiToken || '87654321',
|
||||
TMDBApiKey: config.SiteConfig.TMDBApiKey || '',
|
||||
TMDBProxy: config.SiteConfig.TMDBProxy || '',
|
||||
TMDBReverseProxy: config.SiteConfig.TMDBReverseProxy || '',
|
||||
BannerDataSource: config.SiteConfig.BannerDataSource || 'Douban',
|
||||
RecommendationDataSource: config.SiteConfig.RecommendationDataSource || 'Mixed',
|
||||
PansouApiUrl: config.SiteConfig.PansouApiUrl || '',
|
||||
PansouUsername: config.SiteConfig.PansouUsername || '',
|
||||
PansouPassword: config.SiteConfig.PansouPassword || '',
|
||||
PansouKeywordBlocklist: config.SiteConfig.PansouKeywordBlocklist || '',
|
||||
MagnetProxy: config.SiteConfig.MagnetProxy || '',
|
||||
MagnetMikanReverseProxy: config.SiteConfig.MagnetMikanReverseProxy || '',
|
||||
MagnetDmhyReverseProxy: config.SiteConfig.MagnetDmhyReverseProxy || '',
|
||||
MagnetAcgripReverseProxy: config.SiteConfig.MagnetAcgripReverseProxy || '',
|
||||
EnableComments: config.SiteConfig.EnableComments || false,
|
||||
});
|
||||
}
|
||||
@@ -7943,334 +8504,442 @@ const SiteConfigComponent = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 轮播图数据源 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
轮播图数据源
|
||||
</label>
|
||||
<select
|
||||
value={siteSettings.BannerDataSource || 'Douban'}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
BannerDataSource: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
>
|
||||
<option value='Douban'>豆瓣</option>
|
||||
<option value='TMDB'>TMDB</option>
|
||||
<option value='TX'>TX</option>
|
||||
</select>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
选择首页轮播图的数据来源
|
||||
</p>
|
||||
</div>
|
||||
<details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
|
||||
数据源配置
|
||||
</summary>
|
||||
<div className='mt-4 space-y-4'>
|
||||
{/* 轮播图数据源 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
轮播图数据源
|
||||
</label>
|
||||
<select
|
||||
value={siteSettings.BannerDataSource || 'Douban'}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
BannerDataSource: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
>
|
||||
<option value='Douban'>豆瓣</option>
|
||||
<option value='TMDB'>TMDB</option>
|
||||
<option value='TX'>TX</option>
|
||||
</select>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
选择首页轮播图的数据来源
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 更多推荐数据源 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
更多推荐数据源
|
||||
</label>
|
||||
<select
|
||||
value={siteSettings.RecommendationDataSource || 'Mixed'}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
RecommendationDataSource: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
>
|
||||
<option value='Mixed'>混合</option>
|
||||
<option value='Douban'>豆瓣</option>
|
||||
<option value='TMDB'>TMDB</option>
|
||||
</select>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
选择详情页"更多推荐"的数据来源。混合模式会根据豆瓣ID和评论开关自动切换数据源
|
||||
</p>
|
||||
</div>
|
||||
{/* 更多推荐数据源 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
更多推荐数据源
|
||||
</label>
|
||||
<select
|
||||
value={siteSettings.RecommendationDataSource || 'Mixed'}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
RecommendationDataSource: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
>
|
||||
<option value='Mixed'>混合</option>
|
||||
<option value='Douban'>豆瓣</option>
|
||||
<option value='TMDB'>TMDB</option>
|
||||
</select>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
选择详情页"更多推荐"的数据来源。混合模式会根据豆瓣ID和评论开关自动切换数据源
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* 弹幕 API 配置 */}
|
||||
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||
<details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
|
||||
弹幕配置
|
||||
</h3>
|
||||
</summary>
|
||||
<div className='mt-4 space-y-4'>
|
||||
{/* 弹幕 API 地址 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
弹幕 API 地址
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='http://localhost:9321'
|
||||
value={siteSettings.DanmakuApiBase}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
DanmakuApiBase: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
弹幕服务器的 API 地址,默认为 http://localhost:9321。API部署参考
|
||||
<a
|
||||
href='https://github.com/huangxd-/danmu_api.git'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300'
|
||||
>
|
||||
danmu_api
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 弹幕 API 地址 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
弹幕 API 地址
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='http://localhost:9321'
|
||||
value={siteSettings.DanmakuApiBase}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
DanmakuApiBase: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
弹幕服务器的 API 地址,默认为 http://localhost:9321。API部署参考
|
||||
<a
|
||||
href='https://github.com/huangxd-/danmu_api.git'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300'
|
||||
>
|
||||
danmu_api
|
||||
</a>
|
||||
</p>
|
||||
{/* 弹幕 API Token */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
弹幕 API Token
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='87654321'
|
||||
value={siteSettings.DanmakuApiToken}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
DanmakuApiToken: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
弹幕服务器的访问令牌,默认为 87654321
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 弹幕 API Token */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
弹幕 API Token
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='87654321'
|
||||
value={siteSettings.DanmakuApiToken}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
DanmakuApiToken: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
弹幕服务器的访问令牌,默认为 87654321
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* TMDB 配置 */}
|
||||
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||
<details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
|
||||
TMDB 配置
|
||||
</h3>
|
||||
|
||||
{/* TMDB API Key */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
TMDB API Key
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入 TMDB API Key(多个key用英文逗号分隔)'
|
||||
value={siteSettings.TMDBApiKey}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
TMDBApiKey: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置后首页将显示 TMDB 即将上映电影。支持配置多个 API Key(用英文逗号分隔)以实现轮询,避免单个 Key 请求限制。获取 API Key 请访问{' '}
|
||||
<a
|
||||
href='https://www.themoviedb.org/settings/api'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300'
|
||||
>
|
||||
TMDB API 设置页面
|
||||
</a>
|
||||
</summary>
|
||||
<div className='mt-4 space-y-4'>
|
||||
<p className='text-xs text-amber-600 dark:text-amber-400'>
|
||||
由于国内网络环境限制,TMDB 服务通常需要配置代理后才能正常使用。
|
||||
</p>
|
||||
</div>
|
||||
{/* TMDB API Key */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
TMDB API Key
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入 TMDB API Key(多个key用英文逗号分隔)'
|
||||
value={siteSettings.TMDBApiKey}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
TMDBApiKey: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置后首页将显示 TMDB 即将上映电影。支持配置多个 API Key(用英文逗号分隔)以实现轮询,避免单个 Key 请求限制。获取 API Key 请访问{' '}
|
||||
<a
|
||||
href='https://www.themoviedb.org/settings/api'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300'
|
||||
>
|
||||
TMDB API 设置页面
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TMDB Proxy */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
TMDB 系统代理
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入代理地址(可选)'
|
||||
value={siteSettings.TMDBProxy}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
TMDBProxy: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置代理服务器地址,用于访问 TMDB API(可选)
|
||||
</p>
|
||||
</div>
|
||||
{/* TMDB Proxy */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
TMDB 系统代理
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入代理地址(可选)'
|
||||
value={siteSettings.TMDBProxy}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
TMDBProxy: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置代理服务器地址,用于访问 TMDB API(可选)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TMDB Reverse Proxy */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
TMDB 反代代理
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入反代 Base URL(可选)'
|
||||
value={siteSettings.TMDBReverseProxy}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
TMDBReverseProxy: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置 TMDB 反向代理 Base URL(可选)
|
||||
</p>
|
||||
{/* TMDB Reverse Proxy */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
TMDB 反代代理
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入反代 Base URL(可选)'
|
||||
value={siteSettings.TMDBReverseProxy}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
TMDBReverseProxy: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置 TMDB 反向代理 Base URL(可选)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
|
||||
磁链配置
|
||||
</summary>
|
||||
<div className='mt-4 space-y-4'>
|
||||
<p className='text-xs text-amber-600 dark:text-amber-400'>
|
||||
由于国内网络环境限制,部分磁链搜索站点通常需要配置代理后才能正常访问。
|
||||
</p>
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
系统代理
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入代理地址(可选)'
|
||||
value={siteSettings.MagnetProxy || ''}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
MagnetProxy: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
用于访问磁链搜索站点的系统代理。Cloudflare 部署环境下不会使用该代理。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
Mikan 反代代理
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入 Mikan 反代 Base URL(可选)'
|
||||
value={siteSettings.MagnetMikanReverseProxy || ''}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
MagnetMikanReverseProxy: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置后将使用该地址替代默认的 Mikan 域名进行请求。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
动漫花园反代代理
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入动漫花园反代 Base URL(可选)'
|
||||
value={siteSettings.MagnetDmhyReverseProxy || ''}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
MagnetDmhyReverseProxy: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置后将使用该地址替代默认的动漫花园域名进行请求。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
ACG.RIP 反代代理
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入 ACG.RIP 反代 Base URL(可选)'
|
||||
value={siteSettings.MagnetAcgripReverseProxy || ''}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
MagnetAcgripReverseProxy: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置后将使用该地址替代默认的 ACG.RIP 域名进行请求。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Pansou 配置 */}
|
||||
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||
<details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
|
||||
Pansou 网盘搜索配置
|
||||
</h3>
|
||||
</summary>
|
||||
<div className='mt-4 space-y-4'>
|
||||
{/* Pansou API 地址 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
Pansou API 地址
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入 Pansou API 地址,如:http://localhost:8888'
|
||||
value={siteSettings.PansouApiUrl}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
PansouApiUrl: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置 Pansou 服务器地址,用于网盘资源搜索。项目地址:{' '}
|
||||
<a
|
||||
href='https://github.com/fish2018/pansou'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300'
|
||||
>
|
||||
https://github.com/fish2018/pansou
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pansou API 地址 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
Pansou API 地址
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='请输入 Pansou API 地址,如:http://localhost:8888'
|
||||
value={siteSettings.PansouApiUrl}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
PansouApiUrl: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置 Pansou 服务器地址,用于网盘资源搜索。项目地址:{' '}
|
||||
<a
|
||||
href='https://github.com/fish2018/pansou'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300'
|
||||
>
|
||||
https://github.com/fish2018/pansou
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{/* Pansou 账号 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
Pansou 账号(可选)
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='如果 Pansou 启用了认证,请输入账号'
|
||||
value={siteSettings.PansouUsername}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
PansouUsername: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
如果 Pansou 服务启用了认证功能,需要提供账号密码
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pansou 账号 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
Pansou 账号(可选)
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='如果 Pansou 启用了认证,请输入账号'
|
||||
value={siteSettings.PansouUsername}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
PansouUsername: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
如果 Pansou 服务启用了认证功能,需要提供账号密码
|
||||
</p>
|
||||
</div>
|
||||
{/* Pansou 密码 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
Pansou 密码(可选)
|
||||
</label>
|
||||
<input
|
||||
type='password'
|
||||
placeholder='如果 Pansou 启用了认证,请输入密码'
|
||||
value={siteSettings.PansouPassword}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
PansouPassword: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置账号密码后,系统会自动登录并缓存 Token
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pansou 密码 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
Pansou 密码(可选)
|
||||
</label>
|
||||
<input
|
||||
type='password'
|
||||
placeholder='如果 Pansou 启用了认证,请输入密码'
|
||||
value={siteSettings.PansouPassword}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
PansouPassword: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
配置账号密码后,系统会自动登录并缓存 Token
|
||||
</p>
|
||||
{/* 关键词屏蔽 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
关键词屏蔽(可选)
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='多个关键词用中文或英文逗号分隔'
|
||||
value={siteSettings.PansouKeywordBlocklist}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
PansouKeywordBlocklist: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
设置后会过滤包含这些关键词的搜索结果
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关键词屏蔽 */}
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
关键词屏蔽(可选)
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='多个关键词用中文或英文逗号分隔'
|
||||
value={siteSettings.PansouKeywordBlocklist}
|
||||
onChange={(e) =>
|
||||
setSiteSettings((prev) => ({
|
||||
...prev,
|
||||
PansouKeywordBlocklist: e.target.value,
|
||||
}))
|
||||
}
|
||||
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent'
|
||||
/>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
设置后会过滤包含这些关键词的搜索结果
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* 评论功能配置 */}
|
||||
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
|
||||
<details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
|
||||
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
|
||||
评论配置
|
||||
</h3>
|
||||
|
||||
{/* 开启评论与相似推荐 */}
|
||||
<div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
开启评论与相似推荐
|
||||
</label>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleCommentsToggle(!siteSettings.EnableComments)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
|
||||
siteSettings.EnableComments
|
||||
? buttonStyles.toggleOn
|
||||
: buttonStyles.toggleOff
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full ${
|
||||
buttonStyles.toggleThumb
|
||||
} transition-transform ${
|
||||
</summary>
|
||||
<div className='mt-4 space-y-4'>
|
||||
{/* 开启评论与相似推荐 */}
|
||||
<div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
开启评论与相似推荐
|
||||
</label>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleCommentsToggle(!siteSettings.EnableComments)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
|
||||
siteSettings.EnableComments
|
||||
? buttonStyles.toggleThumbOn
|
||||
: buttonStyles.toggleThumbOff
|
||||
? buttonStyles.toggleOn
|
||||
: buttonStyles.toggleOff
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full ${
|
||||
buttonStyles.toggleThumb
|
||||
} transition-transform ${
|
||||
siteSettings.EnableComments
|
||||
? buttonStyles.toggleThumbOn
|
||||
: buttonStyles.toggleThumbOff
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
开启后将显示豆瓣评论与相似推荐。评论为逆向抓取,请自行承担责任。
|
||||
</p>
|
||||
</div>
|
||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||
开启后将显示豆瓣评论与相似推荐。评论为逆向抓取,请自行承担责任。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className='flex justify-end'>
|
||||
@@ -11931,6 +12600,7 @@ function AdminPageClient() {
|
||||
const [expandedTabs, setExpandedTabs] = useState<{ [key: string]: boolean }>({
|
||||
userConfig: false,
|
||||
videoSource: false,
|
||||
sourceScriptLab: false,
|
||||
mediaLibrary: false,
|
||||
openListConfig: false,
|
||||
embyConfig: false,
|
||||
@@ -12306,6 +12976,17 @@ function AdminPageClient() {
|
||||
<VideoSourceConfig config={config} refreshConfig={fetchConfig} />
|
||||
</CollapsibleTab>
|
||||
|
||||
<CollapsibleTab
|
||||
title='视频源脚本'
|
||||
icon={
|
||||
<Bot size={20} className='text-gray-600 dark:text-gray-400' />
|
||||
}
|
||||
isExpanded={expandedTabs.sourceScriptLab}
|
||||
onToggle={() => toggleTab('sourceScriptLab')}
|
||||
>
|
||||
<VideoSourceScriptLab />
|
||||
</CollapsibleTab>
|
||||
|
||||
{/* 电视直播源配置标签 */}
|
||||
<CollapsibleTab
|
||||
title='电视直播源配置'
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
'use client';
|
||||
|
||||
import { Blend, Loader2 } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { SearchResult } from '@/lib/types';
|
||||
|
||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import VideoCard from '@/components/VideoCard';
|
||||
|
||||
interface ScriptSourceOption {
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function AdvancedRecommendationPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
const initialUrlSourceRef = useRef(searchParams.get('source') || '');
|
||||
|
||||
const [sources, setSources] = useState<ScriptSourceOption[]>([]);
|
||||
const [selectedSource, setSelectedSource] = useState('');
|
||||
const [videos, setVideos] = useState<SearchResult[]>([]);
|
||||
const [isLoadingSources, setIsLoadingSources] = useState(true);
|
||||
const [isLoadingVideos, setIsLoadingVideos] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const initializedRef = useRef(false);
|
||||
const hasSyncedUrlRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSources = async () => {
|
||||
setIsLoadingSources(true);
|
||||
try {
|
||||
const response = await fetch('/api/advanced-recommendation/sources');
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '获取脚本源失败');
|
||||
}
|
||||
|
||||
const nextSources: ScriptSourceOption[] = Array.isArray(data.sources)
|
||||
? data.sources
|
||||
: [];
|
||||
setSources(nextSources);
|
||||
|
||||
const initialSource =
|
||||
nextSources.find((item) => item.key === initialUrlSourceRef.current)
|
||||
?.key ||
|
||||
nextSources[0]?.key ||
|
||||
'';
|
||||
|
||||
setSelectedSource(initialSource);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取脚本源失败');
|
||||
} finally {
|
||||
setIsLoadingSources(false);
|
||||
initializedRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
fetchSources();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initializedRef.current || !selectedSource) return;
|
||||
if (!hasSyncedUrlRef.current) {
|
||||
hasSyncedUrlRef.current = true;
|
||||
if (initialUrlSourceRef.current === selectedSource) return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('source', selectedSource);
|
||||
router.replace(`/advanced-recommendation?${params.toString()}`, {
|
||||
scroll: false,
|
||||
});
|
||||
}, [selectedSource, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSource) return;
|
||||
|
||||
setVideos([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
setError('');
|
||||
}, [selectedSource]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSource) return;
|
||||
|
||||
const fetchVideos = async () => {
|
||||
setIsLoadingVideos(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/advanced-recommendation/videos?source=${encodeURIComponent(selectedSource)}&page=${page}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '获取推荐失败');
|
||||
}
|
||||
|
||||
const nextResults = Array.isArray(data.results) ? data.results : [];
|
||||
setVideos((prev) => (page === 1 ? nextResults : [...prev, ...nextResults]));
|
||||
setHasMore(Number(data.page || page) < Number(data.pageCount || 1));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取推荐失败');
|
||||
} finally {
|
||||
setIsLoadingVideos(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchVideos();
|
||||
}, [selectedSource, page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadMoreRef.current || !hasMore || isLoadingVideos) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) {
|
||||
setPage((prev) => prev + 1);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(loadMoreRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [hasMore, isLoadingVideos]);
|
||||
|
||||
return (
|
||||
<PageLayout activePath='/advanced-recommendation'>
|
||||
<div className='px-4 sm:px-10 py-4 sm:py-8 mb-10'>
|
||||
<div className='mb-6'>
|
||||
<h1 className='text-2xl font-bold text-gray-800 dark:text-gray-200 flex items-center gap-2'>
|
||||
<Blend className='w-6 h-6 text-green-500' />
|
||||
高级推荐
|
||||
</h1>
|
||||
<p className='text-sm text-gray-500 dark:text-gray-400 mt-1'>
|
||||
浏览视频源脚本提供的推荐内容
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='max-w-6xl mx-auto space-y-6'>
|
||||
<div>
|
||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3'>
|
||||
选择脚本源
|
||||
</label>
|
||||
{isLoadingSources ? (
|
||||
<div className='flex items-center justify-center h-12 bg-gray-50/80 rounded-lg border border-gray-200/50 dark:bg-gray-800 dark:border-gray-700'>
|
||||
<Loader2 className='h-5 w-5 animate-spin text-gray-400' />
|
||||
<span className='ml-2 text-sm text-gray-500 dark:text-gray-400'>
|
||||
加载脚本源中...
|
||||
</span>
|
||||
</div>
|
||||
) : sources.length === 0 ? (
|
||||
<div className='flex items-center justify-center h-24 rounded-xl border border-dashed border-gray-300 bg-gray-50/70 text-sm text-gray-500 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-400'>
|
||||
暂无可用的视频源脚本
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex justify-center'>
|
||||
<CapsuleSwitch
|
||||
options={sources.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.key,
|
||||
}))}
|
||||
active={selectedSource}
|
||||
onChange={setSelectedSource}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!error && (
|
||||
<div className='rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600 dark:border-red-900/50 dark:bg-red-900/10 dark:text-red-300'>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingSources && sources.length > 0 && (
|
||||
<>
|
||||
{videos.length > 0 ? (
|
||||
<div className='grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3 sm:gap-4'>
|
||||
{videos.map((video, index) => (
|
||||
<VideoCard
|
||||
key={`${video.source}-${video.id}-${index}`}
|
||||
id={video.id}
|
||||
source={video.source}
|
||||
source_name={video.source_name}
|
||||
title={video.title}
|
||||
poster={video.poster}
|
||||
year={video.year}
|
||||
rate={
|
||||
typeof video.rating === 'number'
|
||||
? String(video.rating)
|
||||
: undefined
|
||||
}
|
||||
douban_id={video.douban_id}
|
||||
tmdb_id={video.tmdb_id}
|
||||
from='source-search'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : !isLoadingVideos && !error ? (
|
||||
<div className='flex items-center justify-center h-32 rounded-xl border border-dashed border-gray-300 bg-gray-50/70 text-sm text-gray-500 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-400'>
|
||||
当前脚本暂无推荐内容
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isLoadingVideos && (
|
||||
<div className='flex justify-center py-6'>
|
||||
<Loader2 className='h-6 w-6 animate-spin text-gray-400' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={loadMoreRef} className='h-10' />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { parseStringPromise } from 'xml2js';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -48,11 +50,17 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
// 请求 acg.rip RSS
|
||||
const searchUrl = `https://acg.rip/page/${pageNum}.xml?term=${encodeURIComponent(trimmedKeyword)}`;
|
||||
const config = await getConfig();
|
||||
const searchBaseUrl = getMagnetBaseUrl(
|
||||
'https://acg.rip',
|
||||
config.SiteConfig.MagnetAcgripReverseProxy
|
||||
);
|
||||
const searchUrl = `${searchBaseUrl}/page/${pageNum}.xml?term=${encodeURIComponent(trimmedKeyword)}`;
|
||||
|
||||
const response = await fetch(searchUrl, {
|
||||
const response = await universalMagnetFetch(searchUrl, config.SiteConfig.MagnetProxy, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -123,4 +131,3 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { parseStringPromise } from 'xml2js';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -56,11 +58,15 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = 'http://share.dmhy.org/topics/rss/rss.xml';
|
||||
const config = await getConfig();
|
||||
const baseUrl = `${getMagnetBaseUrl(
|
||||
'http://share.dmhy.org',
|
||||
config.SiteConfig.MagnetDmhyReverseProxy
|
||||
)}/topics/rss/rss.xml`;
|
||||
const params = new URLSearchParams({ keyword: trimmedKeyword });
|
||||
const searchUrl = `${baseUrl}?${params.toString()}`;
|
||||
|
||||
const response = await fetch(searchUrl, {
|
||||
const response = await universalMagnetFetch(searchUrl, config.SiteConfig.MagnetProxy, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
|
||||
@@ -3,6 +3,8 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { parseStringPromise } from 'xml2js';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -62,11 +64,17 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
const searchUrl = `https://mikanani.me/RSS/Search?searchstr=${encodeURIComponent(trimmedKeyword)}`;
|
||||
const config = await getConfig();
|
||||
const searchBaseUrl = getMagnetBaseUrl(
|
||||
'https://mikanani.me',
|
||||
config.SiteConfig.MagnetMikanReverseProxy
|
||||
);
|
||||
const searchUrl = `${searchBaseUrl}/RSS/Search?searchstr=${encodeURIComponent(trimmedKeyword)}`;
|
||||
|
||||
const response = await fetch(searchUrl, {
|
||||
const response = await universalMagnetFetch(searchUrl, config.SiteConfig.MagnetProxy, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -144,4 +152,3 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ export async function POST(request: NextRequest) {
|
||||
PansouUsername,
|
||||
PansouPassword,
|
||||
PansouKeywordBlocklist,
|
||||
MagnetProxy,
|
||||
MagnetMikanReverseProxy,
|
||||
MagnetDmhyReverseProxy,
|
||||
MagnetAcgripReverseProxy,
|
||||
EnableComments,
|
||||
CustomAdFilterCode,
|
||||
CustomAdFilterVersion,
|
||||
@@ -91,6 +95,10 @@ export async function POST(request: NextRequest) {
|
||||
PansouUsername?: string;
|
||||
PansouPassword?: string;
|
||||
PansouKeywordBlocklist?: string;
|
||||
MagnetProxy?: string;
|
||||
MagnetMikanReverseProxy?: string;
|
||||
MagnetDmhyReverseProxy?: string;
|
||||
MagnetAcgripReverseProxy?: string;
|
||||
EnableComments: boolean;
|
||||
CustomAdFilterCode?: string;
|
||||
CustomAdFilterVersion?: number;
|
||||
@@ -132,6 +140,10 @@ export async function POST(request: NextRequest) {
|
||||
(BannerDataSource !== undefined && typeof BannerDataSource !== 'string') ||
|
||||
(RecommendationDataSource !== undefined && typeof RecommendationDataSource !== 'string') ||
|
||||
(PansouKeywordBlocklist !== undefined && typeof PansouKeywordBlocklist !== 'string') ||
|
||||
(MagnetProxy !== undefined && typeof MagnetProxy !== 'string') ||
|
||||
(MagnetMikanReverseProxy !== undefined && typeof MagnetMikanReverseProxy !== 'string') ||
|
||||
(MagnetDmhyReverseProxy !== undefined && typeof MagnetDmhyReverseProxy !== 'string') ||
|
||||
(MagnetAcgripReverseProxy !== undefined && typeof MagnetAcgripReverseProxy !== 'string') ||
|
||||
typeof EnableComments !== 'boolean' ||
|
||||
(CustomAdFilterCode !== undefined && typeof CustomAdFilterCode !== 'string') ||
|
||||
(CustomAdFilterVersion !== undefined && typeof CustomAdFilterVersion !== 'number') ||
|
||||
@@ -188,6 +200,10 @@ export async function POST(request: NextRequest) {
|
||||
PansouUsername,
|
||||
PansouPassword,
|
||||
PansouKeywordBlocklist,
|
||||
MagnetProxy,
|
||||
MagnetMikanReverseProxy,
|
||||
MagnetDmhyReverseProxy,
|
||||
MagnetAcgripReverseProxy,
|
||||
EnableComments,
|
||||
CustomAdFilterCode,
|
||||
CustomAdFilterVersion,
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
deleteSourceScript,
|
||||
getDefaultSourceScriptTemplate,
|
||||
importSourceScripts,
|
||||
listSourceScripts,
|
||||
saveSourceScript,
|
||||
testSourceScript,
|
||||
toggleSourceScriptEnabled,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
async function assertAdmin(request: NextRequest) {
|
||||
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||
if (storageType === 'localstorage') {
|
||||
throw new Error('不支持本地存储进行管理员配置');
|
||||
}
|
||||
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo?.username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authInfo.username === process.env.USERNAME) {
|
||||
return authInfo.username;
|
||||
}
|
||||
|
||||
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||
if (!userInfoV2 || (userInfoV2.role !== 'admin' && userInfoV2.role !== 'owner') || userInfoV2.banned) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return authInfo.username;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const username = await assertAdmin(request);
|
||||
if (!username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const items = await listSourceScripts();
|
||||
return NextResponse.json(
|
||||
{
|
||||
items,
|
||||
template: getDefaultSourceScriptTemplate(),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || '获取脚本列表失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const username = await assertAdmin(request);
|
||||
if (!username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = (await request.json()) as Record<string, any>;
|
||||
const action = body.action as string;
|
||||
|
||||
switch (action) {
|
||||
case 'save': {
|
||||
const saved = await saveSourceScript({
|
||||
id: body.id,
|
||||
key: body.key,
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
code: body.code,
|
||||
enabled: body.enabled,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ ok: true, item: saved },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
case 'delete': {
|
||||
await deleteSourceScript(body.id);
|
||||
return NextResponse.json(
|
||||
{ ok: true },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
case 'toggle_enabled': {
|
||||
const item = await toggleSourceScriptEnabled(body.id);
|
||||
return NextResponse.json(
|
||||
{ ok: true, item },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
case 'test': {
|
||||
const result = await testSourceScript({
|
||||
code: body.code,
|
||||
hook: body.hook,
|
||||
payload: body.payload || {},
|
||||
name: body.name,
|
||||
key: body.key,
|
||||
configValues: body.configValues,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(result, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json(result, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
});
|
||||
}
|
||||
case 'import': {
|
||||
const imported = await importSourceScripts(
|
||||
Array.isArray(body.items) ? body.items : []
|
||||
);
|
||||
return NextResponse.json(
|
||||
{ ok: true, items: imported },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
default:
|
||||
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: (error as Error).message || '脚本操作失败',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { listEnabledSourceScripts } from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const scripts = await listEnabledSourceScripts();
|
||||
|
||||
return NextResponse.json({
|
||||
sources: scripts.map((item) => ({
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: '获取高级推荐脚本失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
normalizeScriptRecommendResults,
|
||||
normalizeScriptSources,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const sourceKey = searchParams.get('source');
|
||||
const page = Number(searchParams.get('page') || '1');
|
||||
|
||||
if (!sourceKey) {
|
||||
return NextResponse.json({ error: '缺少参数: source' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
let sources = [{ id: 'default', name: '默认源' }];
|
||||
|
||||
try {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: sourceKey,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
sources = normalizeScriptSources(sourcesExecution.result);
|
||||
} catch {
|
||||
// 允许脚本未实现 getSources,继续使用默认源
|
||||
}
|
||||
|
||||
const execution = await executeSavedSourceScript({
|
||||
key: sourceKey,
|
||||
hook: 'recommend',
|
||||
payload: { page },
|
||||
});
|
||||
|
||||
const results = normalizeScriptRecommendResults({
|
||||
scriptKey: sourceKey,
|
||||
scriptName: execution.meta?.name || sourceKey,
|
||||
result: execution.result,
|
||||
sources,
|
||||
defaultSourceId: sources[0]?.id || 'default',
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
results,
|
||||
page: Number(execution.result?.page || page),
|
||||
pageCount: Number(execution.result?.pageCount || 1),
|
||||
total: Number(execution.result?.total || results.length),
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || '获取高级推荐失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
setCachedMetaInfo,
|
||||
} from '@/lib/openlist-cache';
|
||||
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -22,6 +23,7 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const apiUrl = searchParams.get('api');
|
||||
const yellowFilter = searchParams.get('yellowFilter') === 'true';
|
||||
|
||||
if (!apiUrl) {
|
||||
return NextResponse.json(
|
||||
@@ -96,7 +98,7 @@ export async function GET(request: NextRequest) {
|
||||
console.log('CMS 代理 origin:', origin);
|
||||
|
||||
// 处理返回数据,替换播放链接为代理链接
|
||||
const processedData = processPlayUrls(data, origin);
|
||||
const processedData = processCmsResponse(data, origin, yellowFilter);
|
||||
|
||||
return NextResponse.json(processedData, {
|
||||
headers: {
|
||||
@@ -130,7 +132,7 @@ export async function GET(request: NextRequest) {
|
||||
/**
|
||||
* 处理 CMS API 返回数据,将播放链接替换为代理链接
|
||||
*/
|
||||
function processPlayUrls(data: any, proxyOrigin: string): any {
|
||||
function processCmsResponse(data: any, proxyOrigin: string, yellowFilter: boolean): any {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return data;
|
||||
}
|
||||
@@ -138,6 +140,28 @@ function processPlayUrls(data: any, proxyOrigin: string): any {
|
||||
// 深拷贝数据,避免修改原始对象
|
||||
const processedData = JSON.parse(JSON.stringify(data));
|
||||
|
||||
if (yellowFilter) {
|
||||
if (processedData.class && Array.isArray(processedData.class)) {
|
||||
processedData.class = processedData.class.filter((item: any) => !matchesYellowContent(item?.type_name));
|
||||
}
|
||||
|
||||
if (processedData.list && Array.isArray(processedData.list)) {
|
||||
processedData.list = processedData.list.filter((item: any) => !matchesYellowContent(
|
||||
item?.vod_name,
|
||||
item?.type_name,
|
||||
item?.vod_remarks,
|
||||
item?.vod_content,
|
||||
));
|
||||
|
||||
if (typeof processedData.total === 'number') {
|
||||
processedData.total = processedData.list.length;
|
||||
}
|
||||
if (typeof processedData.limit === 'number') {
|
||||
processedData.limit = processedData.list.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 M3U8 代理 token
|
||||
const proxyToken = process.env.NEXT_PUBLIC_PROXY_M3U8_TOKEN || '';
|
||||
const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : '';
|
||||
@@ -174,6 +198,19 @@ function processPlayUrls(data: any, proxyOrigin: string): any {
|
||||
return processedData;
|
||||
}
|
||||
|
||||
function matchesYellowContent(...values: Array<string | undefined>): boolean {
|
||||
const normalized = values
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return yellowWords.some((word) => normalized.includes(word.toLowerCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理播放地址字符串
|
||||
* 格式: 第01集$url1#第02集$url2#...
|
||||
|
||||
@@ -3,6 +3,12 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { getDetailFromApi } from '@/lib/downstream';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
normalizeScriptDetailResult,
|
||||
normalizeScriptSources,
|
||||
parseScriptSourceValue,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -20,6 +26,49 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsedScriptSource = parseScriptSourceValue(sourceCode);
|
||||
if (parsedScriptSource) {
|
||||
try {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: parsedScriptSource.scriptKey,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
const sources = normalizeScriptSources(sourcesExecution.result);
|
||||
const sourceInfo =
|
||||
sources.find((item) => item.id === parsedScriptSource.sourceId) || {
|
||||
id: parsedScriptSource.sourceId,
|
||||
name: parsedScriptSource.sourceId,
|
||||
};
|
||||
|
||||
const detailExecution = await executeSavedSourceScript({
|
||||
key: parsedScriptSource.scriptKey,
|
||||
hook: 'detail',
|
||||
payload: {
|
||||
id,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
},
|
||||
});
|
||||
|
||||
const normalized = normalizeScriptDetailResult({
|
||||
source: sourceCode,
|
||||
scriptKey: parsedScriptSource.scriptKey,
|
||||
scriptName: detailExecution.meta?.name || parsedScriptSource.scriptKey,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
sourceName: sourceInfo.name,
|
||||
detailId: id,
|
||||
result: detailExecution.result,
|
||||
});
|
||||
|
||||
return NextResponse.json(normalized);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理 openlist 源
|
||||
if (sourceCode === 'openlist') {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as cheerio from 'cheerio/slim';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { fetchDoubanWithVerification } from '@/lib/douban-anti-crawler';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as cheerio from 'cheerio/slim';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { fetchDoubanData } from '@/lib/douban';
|
||||
|
||||
@@ -3,6 +3,12 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
listEnabledSourceScripts,
|
||||
normalizeScriptSearchResults,
|
||||
normalizeScriptSources,
|
||||
} from '@/lib/source-script';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
@@ -37,6 +43,69 @@ export async function GET(request: NextRequest) {
|
||||
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||
|
||||
try {
|
||||
const enabledScripts = await listEnabledSourceScripts();
|
||||
const matchedScript = enabledScripts.find((item) => item.key === resourceId);
|
||||
if (matchedScript) {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: matchedScript.key,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
const sources = normalizeScriptSources(sourcesExecution.result);
|
||||
const scriptResults = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
const execution = await executeSavedSourceScript({
|
||||
key: matchedScript.key,
|
||||
hook: 'search',
|
||||
payload: {
|
||||
keyword: query,
|
||||
page: 1,
|
||||
sourceId: source.id,
|
||||
},
|
||||
});
|
||||
|
||||
return normalizeScriptSearchResults({
|
||||
scriptKey: matchedScript.key,
|
||||
scriptName: matchedScript.name,
|
||||
sourceId: source.id,
|
||||
sourceName: source.name,
|
||||
result: execution.result,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
let result = scriptResults.flat().filter((r) => r.title === query);
|
||||
if (!config.SiteConfig.DisableYellowFilter) {
|
||||
result = result.filter((item) => {
|
||||
const typeName = item.type_name || '';
|
||||
return !yellowWords.some((word: string) => typeName.includes(word));
|
||||
});
|
||||
}
|
||||
|
||||
const cacheTime = await getCacheTime();
|
||||
if (result.length === 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: '未找到结果',
|
||||
result: null,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ results: result },
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
|
||||
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
|
||||
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
|
||||
'Netlify-Vary': 'query',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 根据 resourceId 查找对应的 API 站点
|
||||
const targetSite = apiSites.find((site) => site.key === resourceId);
|
||||
if (!targetSite) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAvailableApiSites } from '@/lib/config';
|
||||
import { listEnabledSourceScripts } from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -11,8 +12,13 @@ export async function GET(request: NextRequest) {
|
||||
console.log('request', request.url);
|
||||
try {
|
||||
const apiSites = await getAvailableApiSites();
|
||||
const scriptSites = (await listEnabledSourceScripts()).map((item) => ({
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
script: true,
|
||||
}));
|
||||
|
||||
return NextResponse.json(apiSites);
|
||||
return NextResponse.json([...apiSites, ...scriptSites]);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: '获取资源失败' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -5,8 +5,14 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
listEnabledSourceScripts,
|
||||
normalizeScriptSearchResults,
|
||||
normalizeScriptSources,
|
||||
} from '@/lib/source-script';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -83,6 +89,7 @@ export async function GET(request: NextRequest) {
|
||||
id: item.Id,
|
||||
source: sourceValue,
|
||||
source_name: sourceName,
|
||||
weight: weightMap.get(sourceValue) ?? 0,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary', undefined, client.isProxyEnabled() ? proxyToken || undefined : undefined),
|
||||
episodes: [],
|
||||
@@ -138,6 +145,7 @@ export async function GET(request: NextRequest) {
|
||||
id: folderName,
|
||||
source: 'openlist',
|
||||
source_name: '私人影库',
|
||||
weight: weightMap.get('openlist') ?? 0,
|
||||
title: info.title,
|
||||
poster: getTMDBImageUrl(info.poster_path),
|
||||
episodes: [],
|
||||
@@ -176,24 +184,81 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
);
|
||||
|
||||
const scriptSummaries = await listEnabledSourceScripts();
|
||||
const scriptPromises = scriptSummaries.map((script) =>
|
||||
Promise.race([
|
||||
(async () => {
|
||||
try {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: script.key,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
const sources = normalizeScriptSources(sourcesExecution.result);
|
||||
|
||||
const searchResults = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
const execution = await executeSavedSourceScript({
|
||||
key: script.key,
|
||||
hook: 'search',
|
||||
payload: {
|
||||
keyword: query,
|
||||
page: 1,
|
||||
sourceId: source.id,
|
||||
},
|
||||
});
|
||||
|
||||
return normalizeScriptSearchResults({
|
||||
scriptKey: script.key,
|
||||
scriptName: script.name,
|
||||
sourceId: source.id,
|
||||
sourceName: source.name,
|
||||
result: execution.result,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return searchResults.flat();
|
||||
} catch (error) {
|
||||
console.error(`[Search] 搜索脚本 ${script.name} 失败:`, error);
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
new Promise<any[]>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`${script.name} timeout`)), 20000)
|
||||
),
|
||||
]).catch((error) => {
|
||||
console.error(`[Search] 搜索脚本 ${script.name} 超时:`, error);
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const allResults = await Promise.all([
|
||||
openlistPromise,
|
||||
...embyPromises,
|
||||
...searchPromises,
|
||||
...scriptPromises,
|
||||
]);
|
||||
|
||||
// 分离结果:第一个是 openlist,接下来是 emby 结果,最后是 api 结果
|
||||
// 添加安全检查,确保即使某个结果处理出错也不影响其他结果
|
||||
const openlistResults = Array.isArray(allResults[0]) ? allResults[0] : [];
|
||||
const embyResultsArray = allResults.slice(1, 1 + embyPromises.length);
|
||||
const apiResults = allResults.slice(1 + embyPromises.length);
|
||||
const apiResults = allResults.slice(1 + embyPromises.length, 1 + embyPromises.length + searchPromises.length);
|
||||
const scriptResults = allResults.slice(1 + embyPromises.length + searchPromises.length);
|
||||
|
||||
// 合并所有 Emby 结果,添加安全检查
|
||||
const embyResults = embyResultsArray.filter(Array.isArray).flat();
|
||||
const apiResultsFlat = apiResults.filter(Array.isArray).flat();
|
||||
const scriptResultsFlat = scriptResults.filter(Array.isArray).flat();
|
||||
|
||||
let flattenedResults = [...openlistResults, ...embyResults, ...apiResultsFlat];
|
||||
let flattenedResults = [...openlistResults, ...embyResults, ...apiResultsFlat, ...scriptResultsFlat];
|
||||
|
||||
flattenedResults = flattenedResults.map((result) => ({
|
||||
...result,
|
||||
weight: result.weight ?? (weightMap.get(result.source) ?? 0),
|
||||
}));
|
||||
|
||||
if (!config.SiteConfig.DisableYellowFilter) {
|
||||
flattenedResults = flattenedResults.filter((result) => {
|
||||
@@ -204,8 +269,8 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// 按权重降序排序
|
||||
flattenedResults.sort((a, b) => {
|
||||
const weightA = weightMap.get(a.source) ?? 0;
|
||||
const weightB = weightMap.get(b.source) ?? 0;
|
||||
const weightA = a.weight ?? 0;
|
||||
const weightB = b.weight ?? 0;
|
||||
return weightB - weightA;
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ import { getAvailableApiSites, getConfig } from '@/lib/config';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
import { yellowWords } from '@/lib/yellow';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
listEnabledSourceScripts,
|
||||
normalizeScriptSearchResults,
|
||||
normalizeScriptSources,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -61,6 +67,7 @@ export async function GET(request: NextRequest) {
|
||||
config.EmbyConfig.Sources.length > 0 &&
|
||||
config.EmbyConfig.Sources.some(s => s.enabled && s.ServerURL)
|
||||
);
|
||||
const enabledScripts = await listEnabledSourceScripts();
|
||||
|
||||
// 共享状态
|
||||
let streamClosed = false;
|
||||
@@ -103,7 +110,7 @@ export async function GET(request: NextRequest) {
|
||||
const startEvent = `data: ${JSON.stringify({
|
||||
type: 'start',
|
||||
query,
|
||||
totalSources: sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount,
|
||||
totalSources: sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount + enabledScripts.length,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
@@ -147,6 +154,7 @@ export async function GET(request: NextRequest) {
|
||||
id: item.Id,
|
||||
source: sourceValue,
|
||||
source_name: sourceName,
|
||||
weight: weightMap.get(sourceValue) ?? 0,
|
||||
title: item.Name,
|
||||
poster: client.getImageUrl(item.Id, 'Primary', undefined, client.isProxyEnabled() ? proxyToken || undefined : undefined),
|
||||
episodes: [],
|
||||
@@ -253,6 +261,7 @@ export async function GET(request: NextRequest) {
|
||||
id: key,
|
||||
source: 'openlist',
|
||||
source_name: '私人影库',
|
||||
weight: weightMap.get('openlist') ?? 0,
|
||||
title: info.title,
|
||||
poster: getTMDBImageUrl(info.poster_path),
|
||||
episodes: [],
|
||||
@@ -335,6 +344,11 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
filteredResults = filteredResults.map((result) => ({
|
||||
...result,
|
||||
weight: result.weight ?? (weightMap.get(result.source) ?? 0),
|
||||
}));
|
||||
|
||||
// 发送该源的搜索结果
|
||||
completedSources++;
|
||||
|
||||
@@ -380,7 +394,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 检查是否所有源都已完成
|
||||
if (completedSources === sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount) {
|
||||
if (completedSources === sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount + enabledScripts.length) {
|
||||
if (!streamClosed) {
|
||||
// 发送最终完成事件
|
||||
const completeEvent = `data: ${JSON.stringify({
|
||||
@@ -402,8 +416,118 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
});
|
||||
|
||||
const scriptPromises = enabledScripts.map(async (script) => {
|
||||
try {
|
||||
const sourcesExecution = await Promise.race([
|
||||
executeSavedSourceScript({
|
||||
key: script.key,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`${script.name} timeout`)), 20000)
|
||||
),
|
||||
]);
|
||||
|
||||
const sources = normalizeScriptSources((sourcesExecution as any).result);
|
||||
const sourceResults = await Promise.all(
|
||||
sources.map(async (source) => {
|
||||
const execution = await Promise.race([
|
||||
executeSavedSourceScript({
|
||||
key: script.key,
|
||||
hook: 'search',
|
||||
payload: {
|
||||
keyword: query,
|
||||
page: 1,
|
||||
sourceId: source.id,
|
||||
},
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`${script.name}/${source.name} timeout`)), 20000)
|
||||
),
|
||||
]);
|
||||
|
||||
return normalizeScriptSearchResults({
|
||||
scriptKey: script.key,
|
||||
scriptName: script.name,
|
||||
sourceId: source.id,
|
||||
sourceName: source.name,
|
||||
result: (execution as any).result,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
let filteredResults = sourceResults.flat();
|
||||
if (!config.SiteConfig.DisableYellowFilter) {
|
||||
filteredResults = filteredResults.filter((result) => {
|
||||
const typeName = result.type_name || '';
|
||||
return !yellowWords.some((word: string) => typeName.includes(word));
|
||||
});
|
||||
}
|
||||
|
||||
completedSources++;
|
||||
|
||||
if (!streamClosed) {
|
||||
const sourceEvent = `data: ${JSON.stringify({
|
||||
type: 'source_result',
|
||||
source: `script:${script.key}`,
|
||||
sourceName: script.name,
|
||||
results: filteredResults,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
if (!safeEnqueue(encoder.encode(sourceEvent))) {
|
||||
streamClosed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredResults.length > 0) {
|
||||
allResults.push(...filteredResults);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`搜索脚本失败 ${script.name}:`, error);
|
||||
|
||||
completedSources++;
|
||||
|
||||
if (!streamClosed) {
|
||||
const errorEvent = `data: ${JSON.stringify({
|
||||
type: 'source_error',
|
||||
source: `script:${script.key}`,
|
||||
sourceName: script.name,
|
||||
error: error instanceof Error ? error.message : '搜索失败',
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
if (!safeEnqueue(encoder.encode(errorEvent))) {
|
||||
streamClosed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (completedSources === sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount + enabledScripts.length) {
|
||||
if (!streamClosed) {
|
||||
const completeEvent = `data: ${JSON.stringify({
|
||||
type: 'complete',
|
||||
totalResults: allResults.length,
|
||||
completedSources,
|
||||
timestamp: Date.now()
|
||||
})}\n\n`;
|
||||
|
||||
if (safeEnqueue(encoder.encode(completeEvent))) {
|
||||
try {
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
console.warn('Failed to close controller:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 等待所有搜索完成
|
||||
await Promise.allSettled(searchPromises);
|
||||
await Promise.allSettled([...searchPromises, ...scriptPromises]);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { CURRENT_VERSION } from '@/lib/version'
|
||||
import { CURRENT_VERSION } from '@/lib/version';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic'; // 禁用缓存
|
||||
@@ -13,14 +13,22 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||
|
||||
// 观影室配置从环境变量读取
|
||||
const isLiteMode = process.env.MOONTV_LITE === 'true';
|
||||
|
||||
// Lite 镜像不暴露内置观影室能力,避免前端尝试连接本地 Socket.IO 服务
|
||||
// 注意:不要暴露 externalServerAuth 到前端,这是敏感凭据
|
||||
const watchRoomConfig = {
|
||||
enabled: process.env.WATCH_ROOM_ENABLED === 'true',
|
||||
serverType: (process.env.WATCH_ROOM_SERVER_TYPE as 'internal' | 'external') || 'internal',
|
||||
externalServerUrl: process.env.WATCH_ROOM_EXTERNAL_SERVER_URL,
|
||||
// externalServerAuth 不应该暴露给前端
|
||||
};
|
||||
const watchRoomConfig = isLiteMode
|
||||
? {
|
||||
enabled: false,
|
||||
serverType: 'external' as const,
|
||||
externalServerUrl: undefined,
|
||||
}
|
||||
: {
|
||||
enabled: process.env.WATCH_ROOM_ENABLED === 'true',
|
||||
serverType:
|
||||
(process.env.WATCH_ROOM_SERVER_TYPE as 'internal' | 'external') || 'internal',
|
||||
externalServerUrl: process.env.WATCH_ROOM_EXTERNAL_SERVER_URL,
|
||||
};
|
||||
|
||||
// 如果使用 localStorage,返回默认配置
|
||||
if (storageType === 'localstorage') {
|
||||
|
||||
@@ -6,6 +6,12 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import { getAvailableApiSites, getCacheTime, getConfig } from '@/lib/config';
|
||||
import { getDetailFromApiV2 } from '@/lib/downstream';
|
||||
import { getProxyToken } from '@/lib/emby-token';
|
||||
import {
|
||||
executeSavedSourceScript,
|
||||
normalizeScriptDetailResult,
|
||||
normalizeScriptSources,
|
||||
parseScriptSourceValue,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -28,6 +34,49 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsedScriptSource = parseScriptSourceValue(sourceCode);
|
||||
if (parsedScriptSource) {
|
||||
try {
|
||||
const sourcesExecution = await executeSavedSourceScript({
|
||||
key: parsedScriptSource.scriptKey,
|
||||
hook: 'getSources',
|
||||
payload: {},
|
||||
});
|
||||
const sources = normalizeScriptSources(sourcesExecution.result);
|
||||
const sourceInfo =
|
||||
sources.find((item) => item.id === parsedScriptSource.sourceId) || {
|
||||
id: parsedScriptSource.sourceId,
|
||||
name: parsedScriptSource.sourceId,
|
||||
};
|
||||
|
||||
const detailExecution = await executeSavedSourceScript({
|
||||
key: parsedScriptSource.scriptKey,
|
||||
hook: 'detail',
|
||||
payload: {
|
||||
id,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
},
|
||||
});
|
||||
|
||||
const normalized = normalizeScriptDetailResult({
|
||||
source: sourceCode,
|
||||
scriptKey: parsedScriptSource.scriptKey,
|
||||
scriptName: detailExecution.meta?.name || parsedScriptSource.scriptKey,
|
||||
sourceId: parsedScriptSource.sourceId,
|
||||
sourceName: sourceInfo.name,
|
||||
detailId: id,
|
||||
result: detailExecution.result,
|
||||
});
|
||||
|
||||
return NextResponse.json(normalized);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理 emby 源(支持多源)
|
||||
if (sourceCode === 'emby' || sourceCode.startsWith('emby_')) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||
import {
|
||||
parseScriptPlayUrlValue,
|
||||
resolveSavedScriptPlayUrl,
|
||||
} from '@/lib/source-script';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* GET /api/source-script/play?key=xxx&sourceId=xxx&episodeIndex=0&playUrl=base64url&format=json
|
||||
* format=json: 返回 JSON 格式(用于 play 页面)
|
||||
* 默认: 返回重定向(用于播放器或外部调用)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const key = searchParams.get('key');
|
||||
const sourceId = searchParams.get('sourceId');
|
||||
const episodeIndexRaw = searchParams.get('episodeIndex');
|
||||
const playUrlEncoded = searchParams.get('playUrl');
|
||||
const format = searchParams.get('format');
|
||||
|
||||
if (!key || !sourceId || !episodeIndexRaw || !playUrlEncoded) {
|
||||
return NextResponse.json({ error: '缺少参数' }, { status: 400 });
|
||||
}
|
||||
|
||||
const episodeIndex = Number.parseInt(episodeIndexRaw, 10);
|
||||
if (!Number.isInteger(episodeIndex) || episodeIndex < 0) {
|
||||
return NextResponse.json({ error: '无效的 episodeIndex' }, { status: 400 });
|
||||
}
|
||||
|
||||
const playUrl = parseScriptPlayUrlValue(playUrlEncoded);
|
||||
if (!playUrl) {
|
||||
return NextResponse.json({ error: '无效的播放地址' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await resolveSavedScriptPlayUrl({
|
||||
key,
|
||||
sourceId,
|
||||
episodeIndex,
|
||||
playUrl,
|
||||
});
|
||||
|
||||
if (!result.url || result.url.trim() === '') {
|
||||
throw new Error('获取到的播放链接为空');
|
||||
}
|
||||
|
||||
if (format === 'json') {
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
return NextResponse.redirect(result.url);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export async function GET(request: NextRequest) {
|
||||
const token = searchParams.get('token');
|
||||
const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN;
|
||||
const adFilter = searchParams.get('adFilter') === 'true'; // 获取去广告参数
|
||||
const yellowFilter = searchParams.get('yellowFilter') === 'true';
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
@@ -89,7 +90,7 @@ export async function GET(request: NextRequest) {
|
||||
baseUrl = `${proto}://${host}`;
|
||||
}
|
||||
|
||||
console.log('TVBOX 订阅 baseUrl:', baseUrl, 'adFilter:', adFilter);
|
||||
console.log('TVBOX 订阅 baseUrl:', baseUrl, 'adFilter:', adFilter, 'yellowFilter:', yellowFilter);
|
||||
|
||||
// 检查是否配置了 OpenList
|
||||
const hasOpenList = !!(
|
||||
@@ -142,9 +143,9 @@ export async function GET(request: NextRequest) {
|
||||
key: site.key,
|
||||
name: site.name,
|
||||
type: 1,
|
||||
// 如果开启去广告,使用 CMS 代理;否则使用原始 API
|
||||
api: adFilter
|
||||
? `${baseUrl}/api/cms-proxy?api=${encodeURIComponent(site.api)}`
|
||||
// 开启去广告或黄色过滤时使用 CMS 代理
|
||||
api: (adFilter || yellowFilter)
|
||||
? `${baseUrl}/api/cms-proxy?api=${encodeURIComponent(site.api)}${adFilter ? '&adFilter=true' : ''}${yellowFilter ? '&yellowFilter=true' : ''}`
|
||||
: site.api,
|
||||
searchable: 1,
|
||||
quickSearch: 1,
|
||||
|
||||
+30
-1
@@ -30,6 +30,7 @@ function DoubanPageClient() {
|
||||
const [selectorsReady, setSelectorsReady] = useState(false);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const loadingRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 用于存储最新参数值的 refs
|
||||
@@ -598,6 +599,34 @@ function DoubanPageClient() {
|
||||
};
|
||||
}, [hasMore, isLoadingMore, loading]);
|
||||
|
||||
// 首屏如果未被撑满,仅在第一页时额外请求一次下一页
|
||||
useEffect(() => {
|
||||
if (
|
||||
loading ||
|
||||
!selectorsReady ||
|
||||
isLoadingMore ||
|
||||
!hasMore ||
|
||||
doubanData.length === 0 ||
|
||||
currentPage !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rafId = window.requestAnimationFrame(() => {
|
||||
const contentEl = contentRef.current;
|
||||
if (!contentEl) return;
|
||||
|
||||
const rect = contentEl.getBoundingClientRect();
|
||||
const preloadThreshold = window.innerHeight + 120;
|
||||
|
||||
if (rect.bottom < preloadThreshold) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
});
|
||||
|
||||
return () => window.cancelAnimationFrame(rafId);
|
||||
}, [loading, selectorsReady, isLoadingMore, hasMore, doubanData.length, currentPage]);
|
||||
|
||||
// 处理选择器变化
|
||||
const handlePrimaryChange = useCallback(
|
||||
(value: string) => {
|
||||
@@ -781,7 +810,7 @@ function DoubanPageClient() {
|
||||
</div>
|
||||
|
||||
{/* 内容展示区域 */}
|
||||
<div className='max-w-[95%] mx-auto mt-8 overflow-visible'>
|
||||
<div ref={contentRef} className='max-w-[95%] mx-auto mt-8 overflow-visible'>
|
||||
{/* 内容网格 */}
|
||||
<div className='justify-start grid grid-cols-3 gap-x-2 gap-y-12 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,minmax(160px,1fr))] sm:gap-x-8 sm:gap-y-20'>
|
||||
{loading || !selectorsReady
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { listEnabledSourceScripts } from '@/lib/source-script';
|
||||
|
||||
import { DanmakuCacheCleanup } from '../components/DanmakuCacheCleanup';
|
||||
import { DownloadBubble } from '../components/DownloadBubble';
|
||||
@@ -91,6 +92,7 @@ export default async function RootLayout({
|
||||
let webLiveEnabled = false;
|
||||
let customAdFilterVersion = 0;
|
||||
let tuneHubEnabled = false;
|
||||
let advancedRecommendationEnabled = false;
|
||||
let customCategories = [] as {
|
||||
name: string;
|
||||
type: 'movie' | 'tv';
|
||||
@@ -145,6 +147,9 @@ export default async function RootLayout({
|
||||
customAdFilterVersion = config.SiteConfig?.CustomAdFilterVersion || 0;
|
||||
// TuneHub音乐功能配置
|
||||
tuneHubEnabled = config.MusicConfig?.TuneHubEnabled || false;
|
||||
// 高级推荐功能配置:存在已启用视频源脚本时显示
|
||||
advancedRecommendationEnabled =
|
||||
(await listEnabledSourceScripts()).length > 0;
|
||||
// 检查是否启用了 OpenList 功能
|
||||
openListEnabled = !!(
|
||||
config.OpenListConfig?.Enabled &&
|
||||
@@ -207,6 +212,7 @@ export default async function RootLayout({
|
||||
AI_DEFAULT_MESSAGE_WITH_VIDEO: aiDefaultMessageWithVideo,
|
||||
ENABLE_MOVIE_REQUEST: enableMovieRequest,
|
||||
WEB_LIVE_ENABLED: webLiveEnabled,
|
||||
ADVANCED_RECOMMENDATION_ENABLED: advancedRecommendationEnabled,
|
||||
CUSTOM_AD_FILTER_VERSION: customAdFilterVersion,
|
||||
TUNEHUB_ENABLED: tuneHubEnabled,
|
||||
FESTIVE_EFFECT_ENABLED:
|
||||
|
||||
+222
-37
@@ -2,9 +2,9 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { Heart, Radio, Tv } from 'lucide-react';
|
||||
import { GitBranch, Heart, Radio, Tv } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
deleteFavorite,
|
||||
@@ -43,6 +43,21 @@ interface LiveChannel {
|
||||
url: string;
|
||||
}
|
||||
|
||||
type MergedChannelItem =
|
||||
| {
|
||||
type: 'single';
|
||||
key: string;
|
||||
channel: LiveChannel;
|
||||
}
|
||||
| {
|
||||
type: 'merged';
|
||||
key: string;
|
||||
name: string;
|
||||
group: string;
|
||||
logo: string;
|
||||
channels: LiveChannel[];
|
||||
};
|
||||
|
||||
// 直播源接口
|
||||
interface LiveSource {
|
||||
key: string;
|
||||
@@ -120,6 +135,7 @@ function LivePageClient() {
|
||||
|
||||
// 搜索关键词
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [expandedMergedChannels, setExpandedMergedChannels] = useState<string[]>([]);
|
||||
|
||||
// 节目单信息
|
||||
const [epgData, setEpgData] = useState<{
|
||||
@@ -1127,6 +1143,69 @@ function LivePageClient() {
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const mergedChannelItems = useMemo<MergedChannelItem[]>(() => {
|
||||
if (!filteredChannels || filteredChannels.length === 0) return [];
|
||||
|
||||
const mergedMap = new Map<string, {
|
||||
key: string;
|
||||
name: string;
|
||||
group: string;
|
||||
logo: string;
|
||||
channels: LiveChannel[];
|
||||
}>();
|
||||
const order: string[] = [];
|
||||
|
||||
filteredChannels.forEach((channel) => {
|
||||
const mergedKey = `${channel.group}::${channel.name.trim().toLowerCase()}`;
|
||||
const existing = mergedMap.get(mergedKey);
|
||||
|
||||
if (existing) {
|
||||
existing.channels.push(channel);
|
||||
if (!existing.logo && channel.logo) {
|
||||
existing.logo = channel.logo;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mergedMap.set(mergedKey, {
|
||||
key: mergedKey,
|
||||
name: channel.name,
|
||||
group: channel.group,
|
||||
logo: channel.logo,
|
||||
channels: [channel],
|
||||
});
|
||||
order.push(mergedKey);
|
||||
});
|
||||
|
||||
return order.map((key) => {
|
||||
const item = mergedMap.get(key)!;
|
||||
if (item.channels.length === 1) {
|
||||
return {
|
||||
type: 'single',
|
||||
key,
|
||||
channel: item.channels[0],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'merged',
|
||||
key,
|
||||
name: item.name,
|
||||
group: item.group,
|
||||
logo: item.logo,
|
||||
channels: item.channels,
|
||||
};
|
||||
});
|
||||
}, [filteredChannels]);
|
||||
|
||||
const toggleMergedChannel = (key: string) => {
|
||||
setExpandedMergedChannels((prev) => (
|
||||
prev.includes(key)
|
||||
? prev.filter(item => item !== key)
|
||||
: [...prev, key]
|
||||
));
|
||||
};
|
||||
|
||||
// 切换分组
|
||||
const handleGroupChange = (group: string) => {
|
||||
// 如果正在切换直播源,则禁用分组切换
|
||||
@@ -2212,7 +2291,7 @@ function LivePageClient() {
|
||||
</div>
|
||||
|
||||
{/* 频道列表 */}
|
||||
<div className={`h-[300px] lg:h-full md:overflow-hidden transition-all duration-300 ease-in-out ${isChannelListCollapsed
|
||||
<div className={`h-[330px] lg:h-full md:overflow-hidden transition-all duration-300 ease-in-out ${isChannelListCollapsed
|
||||
? 'md:col-span-1 lg:hidden lg:opacity-0 lg:scale-95'
|
||||
: 'md:col-span-1 lg:opacity-100 lg:scale-100'
|
||||
}`}>
|
||||
@@ -2365,45 +2444,151 @@ function LivePageClient() {
|
||||
|
||||
{/* 频道列表 */}
|
||||
<div ref={channelListRef} className='flex-1 overflow-y-auto space-y-2 pb-4'>
|
||||
{filteredChannels?.length > 0 ? (
|
||||
filteredChannels.map(channel => {
|
||||
const isActive = channel.id === currentChannel?.id;
|
||||
{mergedChannelItems?.length > 0 ? (
|
||||
mergedChannelItems.map(item => {
|
||||
if (item.type === 'single') {
|
||||
const channel = item.channel;
|
||||
const isActive = channel.id === currentChannel?.id;
|
||||
return (
|
||||
<button
|
||||
key={channel.id}
|
||||
data-channel-id={channel.id}
|
||||
onClick={() => handleChannelChange(channel)}
|
||||
disabled={isSwitchingSource}
|
||||
className={`w-full p-3 rounded-lg text-left transition-all duration-200 ${isSwitchingSource
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: isActive
|
||||
? 'bg-green-100 dark:bg-green-900/30 border border-green-300 dark:border-green-700'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='w-10 h-10 bg-gray-300 dark:bg-gray-700 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden'>
|
||||
{channel.logo ? (
|
||||
<img
|
||||
src={getLogoUrl(channel.logo, currentSource?.key || '')}
|
||||
alt={channel.name}
|
||||
className='w-full h-full rounded object-contain'
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Tv className='w-5 h-5 text-gray-500' />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='text-sm font-medium text-gray-900 dark:text-gray-100 truncate' title={channel.name}>
|
||||
{channel.name}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 dark:text-gray-400 mt-1' title={channel.group}>
|
||||
{channel.group}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const isExpanded = expandedMergedChannels.includes(item.key);
|
||||
const activeLineIndex = item.channels.findIndex(channel => channel.id === currentChannel?.id);
|
||||
const hasActiveChild = activeLineIndex !== -1;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={channel.id}
|
||||
data-channel-id={channel.id}
|
||||
onClick={() => handleChannelChange(channel)}
|
||||
disabled={isSwitchingSource}
|
||||
className={`w-full p-3 rounded-lg text-left transition-all duration-200 ${isSwitchingSource
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: isActive
|
||||
? 'bg-green-100 dark:bg-green-900/30 border border-green-300 dark:border-green-700'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
<div
|
||||
key={item.key}
|
||||
className='space-y-2'
|
||||
>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='w-10 h-10 bg-gray-300 dark:bg-gray-700 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden'>
|
||||
{channel.logo ? (
|
||||
<img
|
||||
src={getLogoUrl(channel.logo, currentSource?.key || '')}
|
||||
alt={channel.name}
|
||||
className='w-full h-full rounded object-contain'
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<Tv className='w-5 h-5 text-gray-500' />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='text-sm font-medium text-gray-900 dark:text-gray-100 truncate' title={channel.name}>
|
||||
{channel.name}
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
handleChannelChange(item.channels[0]);
|
||||
}}
|
||||
disabled={isSwitchingSource}
|
||||
className={`w-full p-3 rounded-lg text-left transition-all duration-200 ${isSwitchingSource
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: hasActiveChild
|
||||
? 'bg-green-100 dark:bg-green-900/30 border border-green-300 dark:border-green-700'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='w-10 h-10 bg-gray-300 dark:bg-gray-700 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden'>
|
||||
{item.logo ? (
|
||||
<img
|
||||
src={getLogoUrl(item.logo, currentSource?.key || '')}
|
||||
alt={item.name}
|
||||
className='w-full h-full rounded object-contain'
|
||||
loading='lazy'
|
||||
/>
|
||||
) : (
|
||||
<Tv className='w-5 h-5 text-gray-500' />
|
||||
)}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 dark:text-gray-400 mt-1' title={channel.group}>
|
||||
{channel.group}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='text-sm font-medium text-gray-900 dark:text-gray-100 truncate' title={item.name}>
|
||||
{item.name}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2'>
|
||||
<span title={item.group}>{item.group}</span>
|
||||
<span>·</span>
|
||||
<span>{item.channels.length} 条线路</span>
|
||||
{hasActiveChild && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{`当前线路${activeLineIndex + 1}`}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col items-end gap-2 flex-shrink-0'>
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleMergedChannel(item.key);
|
||||
}}
|
||||
className='text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300'
|
||||
>
|
||||
{isExpanded ? '收起' : '展开'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className='pl-4 space-y-2'>
|
||||
{item.channels.map((channel, index) => {
|
||||
const isActive = channel.id === currentChannel?.id;
|
||||
return (
|
||||
<button
|
||||
key={channel.id}
|
||||
type='button'
|
||||
data-channel-id={channel.id}
|
||||
onClick={() => handleChannelChange(channel)}
|
||||
disabled={isSwitchingSource}
|
||||
className={`w-full p-3 rounded-lg text-left text-sm transition-all duration-200 ${
|
||||
isSwitchingSource
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: isActive
|
||||
? 'bg-green-100 dark:bg-green-900/30 border border-green-300 dark:border-green-700'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<span className='font-medium text-gray-900 dark:text-gray-100 flex items-center gap-2'>
|
||||
<GitBranch className='w-4 h-4 text-gray-500 dark:text-gray-400' />
|
||||
{`线路${index + 1}`}
|
||||
</span>
|
||||
{isActive && (
|
||||
<span className='text-xs text-green-600 dark:text-green-400'>
|
||||
当前播放
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
|
||||
+325
-59
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { AlertCircle, Cloud, Heart, Sparkles, X } from 'lucide-react';
|
||||
import { AlertCircle, Cloud, Heart, Loader2, Router, Sparkles, X } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||
|
||||
@@ -49,8 +49,8 @@ import { getDoubanDetail } from '@/lib/douban.client';
|
||||
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
||||
import { DanmakuFilterConfig, EpisodeFilterConfig, SearchResult } from '@/lib/types';
|
||||
import { base58Decode, getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
||||
import { useEnableComments } from '@/hooks/useEnableComments';
|
||||
import { useEnableAIComments } from '@/hooks/useEnableAIComments';
|
||||
import { useEnableComments } from '@/hooks/useEnableComments';
|
||||
import { usePlaySync } from '@/hooks/usePlaySync';
|
||||
|
||||
import AIChatPanel from '@/components/AIChatPanel';
|
||||
@@ -86,6 +86,7 @@ interface WakeLockSentinel {
|
||||
}
|
||||
|
||||
function PlayPageClient() {
|
||||
const LOCAL_TRANSCODER_BASE_URL = 'http://localhost:19080';
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const enableComments = useEnableComments();
|
||||
@@ -491,6 +492,7 @@ function PlayPageClient() {
|
||||
const [showDanmakuFilterSettings, setShowDanmakuFilterSettings] = useState(false);
|
||||
const [currentSearchKeyword, setCurrentSearchKeyword] = useState<string>(''); // 当前搜索使用的关键词
|
||||
const [toast, setToast] = useState<ToastProps | null>(null);
|
||||
const [isTranscoding, setIsTranscoding] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
danmakuSettingsRef.current = danmakuSettings;
|
||||
@@ -586,6 +588,29 @@ function PlayPageClient() {
|
||||
return { source };
|
||||
};
|
||||
|
||||
const isLazyDetailSource = (source?: string) => {
|
||||
if (!source) return false;
|
||||
return (
|
||||
source === 'openlist' ||
|
||||
source === 'emby' ||
|
||||
source.startsWith('emby_') ||
|
||||
source.startsWith('script:')
|
||||
);
|
||||
};
|
||||
|
||||
const isM3u8LikeUrl = (url?: string) => {
|
||||
if (!url) return false;
|
||||
const normalizedUrl = url.toLowerCase();
|
||||
return normalizedUrl.includes('.m3u8') || normalizedUrl.includes('/m3u8/');
|
||||
};
|
||||
|
||||
const buildAbsoluteUrl = (url: string) => {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
return `${window.location.origin}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
};
|
||||
|
||||
// 搜索所需信息
|
||||
const [searchTitle] = useState(searchParams.get('stitle') || '');
|
||||
const [searchType] = useState(searchParams.get('stype') || '');
|
||||
@@ -1289,6 +1314,110 @@ function PlayPageClient() {
|
||||
// 视频源代理模式状态
|
||||
const [sourceProxyMode, setSourceProxyMode] = useState(false);
|
||||
|
||||
const resolveCurrentExternalPlaybackUrl = async () => {
|
||||
let urlToUse = videoUrl;
|
||||
if (sourceProxyMode && detail?.episodes && currentEpisodeIndex < detail.episodes.length) {
|
||||
urlToUse = detail.episodes[currentEpisodeIndex];
|
||||
}
|
||||
|
||||
if (!urlToUse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildAbsoluteUrl(urlToUse);
|
||||
};
|
||||
|
||||
const handleCreateTranscodeSession = async () => {
|
||||
if (isTranscoding) return;
|
||||
|
||||
try {
|
||||
setIsTranscoding(true);
|
||||
const currentPlayTime = artPlayerRef.current?.currentTime || 0;
|
||||
|
||||
const sourceUrl = await resolveCurrentExternalPlaybackUrl();
|
||||
if (!sourceUrl) {
|
||||
throw new Error('当前没有可转码的播放链接');
|
||||
}
|
||||
|
||||
const requestHeaders: Record<string, string> = {};
|
||||
if (sourceUrl.startsWith(window.location.origin)) {
|
||||
if (document.cookie) {
|
||||
requestHeaders.Cookie = document.cookie;
|
||||
}
|
||||
requestHeaders.Referer = `${window.location.origin}/`;
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${LOCAL_TRANSCODER_BASE_URL}/v1/sessions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: sourceUrl,
|
||||
headers: Object.keys(requestHeaders).length > 0 ? requestHeaders : undefined,
|
||||
subtitle: {
|
||||
mode: 'burn_embedded',
|
||||
stream: 'auto',
|
||||
},
|
||||
refresh: false,
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
throw new Error('转码服务连接失败');
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.error || data?.message || `转码请求失败 (${response.status})`);
|
||||
}
|
||||
|
||||
const playUrl = data?.playlist_url || data?.play_url;
|
||||
if (!playUrl) {
|
||||
throw new Error('转码器未返回播放地址');
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
currentXiaoyaUrlRef.current = '';
|
||||
proxyAttemptedRef.current = false;
|
||||
resumeTimeRef.current = currentPlayTime > 0 ? currentPlayTime : null;
|
||||
setVideoQualities([]);
|
||||
setVideoError(null);
|
||||
setCorsFailedUrl(null);
|
||||
setIsVideoLoading(true);
|
||||
setVideoLoadingStage('sourceChanging');
|
||||
setVideoUrl(playUrl);
|
||||
setToast({
|
||||
message: '转码任务已创建,等待 3 秒后已切换到转码地址',
|
||||
type: 'success',
|
||||
onClose: () => setToast(null),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建转码任务失败:', error);
|
||||
setToast({
|
||||
message: error instanceof Error ? error.message : '创建转码任务失败',
|
||||
type: 'error',
|
||||
onClose: () => setToast(null),
|
||||
});
|
||||
} finally {
|
||||
setIsTranscoding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const showExternalTranscodeButton = Boolean(
|
||||
detail &&
|
||||
videoUrl &&
|
||||
!videoUrl.startsWith('blob:') &&
|
||||
!isM3u8LikeUrl(videoUrl) &&
|
||||
(
|
||||
detail.source === 'openlist' ||
|
||||
detail.source === 'xiaoya' ||
|
||||
detail.source.startsWith('emby')
|
||||
)
|
||||
);
|
||||
|
||||
// 总集数
|
||||
const totalEpisodes = detail?.episodes?.length || 0;
|
||||
const directEpisodeLabel = detail?.episodes_titles?.[currentEpisodeIndex] || '直链';
|
||||
@@ -1357,13 +1486,15 @@ function PlayPageClient() {
|
||||
// 换源加载状态
|
||||
const [isVideoLoading, setIsVideoLoading] = useState(true);
|
||||
const [videoLoadingStage, setVideoLoadingStage] = useState<
|
||||
'initing' | 'sourceChanging'
|
||||
'initing' | 'sourceChanging' | 'episodeChanging'
|
||||
>('initing');
|
||||
const [videoError, setVideoError] = useState<string | null>(null);
|
||||
// 直链播放时 CORS 失败的原始 URL,用于显示"使用代理播放"按钮
|
||||
const [corsFailedUrl, setCorsFailedUrl] = useState<string | null>(null);
|
||||
// 标记当前视频是否已经尝试过代理(防止 415→直连→失败→代理 的无限循环)
|
||||
const proxyAttemptedRef = useRef(false);
|
||||
const videoUrlRequestSeqRef = useRef(0);
|
||||
const lastVideoRequestKeyRef = useRef<string | null>(null);
|
||||
|
||||
// 直链代理域名记忆:检查某个域名是否需要代理
|
||||
const isDirectplayDomainProxied = (url: string): boolean => {
|
||||
@@ -1504,22 +1635,6 @@ function PlayPageClient() {
|
||||
): Promise<SearchResult> => {
|
||||
if (sources.length === 1) return sources[0];
|
||||
|
||||
// 获取配置以获取权重信息
|
||||
let weightMap = new Map<string, number>();
|
||||
try {
|
||||
const configResponse = await fetch('/api/admin/config');
|
||||
if (configResponse.ok) {
|
||||
const configData = await configResponse.json();
|
||||
if (configData.Config?.SourceConfig) {
|
||||
configData.Config.SourceConfig.forEach((source: any) => {
|
||||
weightMap.set(source.key, source.weight ?? 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('获取配置失败,权重将使用默认值0:', error);
|
||||
}
|
||||
|
||||
// 将播放源均分为两批,并发测速各批,避免一次性过多请求
|
||||
const batchSize = Math.ceil(sources.length / 2);
|
||||
const allResults: Array<{
|
||||
@@ -1602,8 +1717,8 @@ function PlayPageClient() {
|
||||
if (successfulResults.length === 0) {
|
||||
console.warn('所有播放源测速都失败,按权重排序');
|
||||
const sortedByWeight = [...sources].sort((a, b) => {
|
||||
const weightA = weightMap.get(a.source) ?? 0;
|
||||
const weightB = weightMap.get(b.source) ?? 0;
|
||||
const weightA = a.weight ?? 0;
|
||||
const weightB = b.weight ?? 0;
|
||||
return weightB - weightA;
|
||||
});
|
||||
return sortedByWeight[0];
|
||||
@@ -1642,7 +1757,7 @@ function PlayPageClient() {
|
||||
maxSpeed,
|
||||
minPing,
|
||||
maxPing,
|
||||
weightMap.get(result.source.source) ?? 0
|
||||
result.source.weight ?? 0
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -1671,7 +1786,7 @@ function PlayPageClient() {
|
||||
maxSpeed: number,
|
||||
minPing: number,
|
||||
maxPing: number,
|
||||
weight: number = 0
|
||||
weight = 0
|
||||
): number => {
|
||||
let score = 0;
|
||||
|
||||
@@ -2122,21 +2237,56 @@ function PlayPageClient() {
|
||||
!detailData.episodes ||
|
||||
episodeIndex >= detailData.episodes.length
|
||||
) {
|
||||
// openlist 和 emby 源的剧集是懒加载的,如果 episodes 为空则跳过
|
||||
if ((detailData?.source === 'openlist' || detailData?.source === 'emby') && (!detailData.episodes || detailData.episodes.length === 0)) {
|
||||
// 这类源统一先走详情懒加载,如果 episodes 为空则跳过
|
||||
if (isLazyDetailSource(detailData?.source) && (!detailData?.episodes || detailData.episodes.length === 0)) {
|
||||
return;
|
||||
}
|
||||
setVideoUrl('');
|
||||
return;
|
||||
}
|
||||
|
||||
const requestKey = `${detailData.source}|${detailData.id}|${episodeIndex}`;
|
||||
const isEpisodeSwitchRequest = lastVideoRequestKeyRef.current !== requestKey;
|
||||
lastVideoRequestKeyRef.current = requestKey;
|
||||
const requestSeq = ++videoUrlRequestSeqRef.current;
|
||||
|
||||
let newUrl = detailData?.episodes[episodeIndex] || '';
|
||||
const isXiaoyaLazyPlayUrl = newUrl.startsWith('/api/xiaoya/play');
|
||||
|
||||
if (isEpisodeSwitchRequest && isXiaoyaLazyPlayUrl) {
|
||||
setVideoLoadingStage('episodeChanging');
|
||||
setIsVideoLoading(true);
|
||||
setVideoError(null);
|
||||
setCorsFailedUrl(null);
|
||||
|
||||
if (artPlayerRef.current?.video) {
|
||||
try {
|
||||
const video = artPlayerRef.current.video as HTMLVideoElement;
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
} catch (error) {
|
||||
console.warn('切集时清空旧视频源失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (videoUrl) {
|
||||
setVideoUrl('');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是小雅或 openlist 接口,先请求获取真实 URL
|
||||
if (newUrl.startsWith('/api/xiaoya/play') || newUrl.startsWith('/api/openlist/play')) {
|
||||
const isSpecialLazyPlayUrl =
|
||||
isXiaoyaLazyPlayUrl ||
|
||||
newUrl.startsWith('/api/openlist/play') ||
|
||||
newUrl.startsWith('/api/source-script/play');
|
||||
|
||||
if (isSpecialLazyPlayUrl) {
|
||||
try {
|
||||
// 保存原始URL(用于后续刷新)
|
||||
currentXiaoyaUrlRef.current = newUrl;
|
||||
if (newUrl.startsWith('/api/xiaoya/play') || newUrl.startsWith('/api/openlist/play')) {
|
||||
currentXiaoyaUrlRef.current = newUrl;
|
||||
}
|
||||
|
||||
// 添加 format=json 参数
|
||||
const separator = newUrl.includes('?') ? '&' : '?';
|
||||
@@ -2144,6 +2294,9 @@ function PlayPageClient() {
|
||||
|
||||
const response = await fetch(fetchUrl);
|
||||
const data = await response.json();
|
||||
if (requestSeq !== videoUrlRequestSeqRef.current) {
|
||||
return;
|
||||
}
|
||||
if (data.url) {
|
||||
newUrl = data.url;
|
||||
// 保存清晰度列表
|
||||
@@ -2154,6 +2307,9 @@ function PlayPageClient() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestSeq !== videoUrlRequestSeqRef.current) {
|
||||
return;
|
||||
}
|
||||
console.error('获取播放链接失败:', error);
|
||||
setVideoQualities([]);
|
||||
currentXiaoyaUrlRef.current = ''; // 获取失败,清空
|
||||
@@ -2171,6 +2327,9 @@ function PlayPageClient() {
|
||||
currentId || undefined,
|
||||
episodeIndex
|
||||
);
|
||||
if (requestSeq !== videoUrlRequestSeqRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileSystemCheck.hasLocal && fileSystemCheck.dirHandle) {
|
||||
// 使用本地文件播放
|
||||
@@ -2178,7 +2337,7 @@ function PlayPageClient() {
|
||||
// 读取 m3u8 文件
|
||||
const fileHandle = await fileSystemCheck.dirHandle.getFileHandle('playlist.m3u8', { create: false });
|
||||
const file = await fileHandle.getFile();
|
||||
let content = await file.text();
|
||||
const content = await file.text();
|
||||
|
||||
// 解析 m3u8 文件,为每个 ts 文件创建 Blob URL
|
||||
const lines = content.split('\n');
|
||||
@@ -2241,6 +2400,9 @@ function PlayPageClient() {
|
||||
// 如果没有 File System API 本地文件,检查服务器端本地下载
|
||||
if (!fileSystemCheck.hasLocal) {
|
||||
const hasLocalFile = await checkLocalDownload(currentSource, currentId, episodeIndex);
|
||||
if (requestSeq !== videoUrlRequestSeqRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasLocalFile) {
|
||||
// 使用本地代理接口,URL以.m3u8结尾以便Artplayer自动识别
|
||||
@@ -2268,7 +2430,10 @@ function PlayPageClient() {
|
||||
}
|
||||
}
|
||||
|
||||
if (newUrl !== videoUrl) {
|
||||
if (isEpisodeSwitchRequest || newUrl !== videoUrl) {
|
||||
if (requestSeq !== videoUrlRequestSeqRef.current) {
|
||||
return;
|
||||
}
|
||||
setVideoUrl(newUrl);
|
||||
}
|
||||
};
|
||||
@@ -3253,6 +3418,42 @@ function PlayPageClient() {
|
||||
}
|
||||
};
|
||||
|
||||
const getCachedSourcesData = (query: string): SearchResult[] => {
|
||||
if (typeof window === 'undefined' || !query.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheKey = `search_cache_${query.trim()}`;
|
||||
const cached = sessionStorage.getItem(cacheKey);
|
||||
if (!cached) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cachedData = JSON.parse(cached);
|
||||
const results = cachedData.filter(
|
||||
(result: SearchResult) =>
|
||||
normalizeTitle(result.title).toLowerCase() ===
|
||||
normalizeTitle(videoTitleRef.current).toLowerCase() &&
|
||||
(videoYearRef.current
|
||||
? result.year.toLowerCase() === videoYearRef.current.toLowerCase() ||
|
||||
!result.year ||
|
||||
result.year.trim() === '' ||
|
||||
result.year === 'unknown' ||
|
||||
!/^\d{4}$/.test(result.year)
|
||||
: true) &&
|
||||
(searchType
|
||||
? getType(result) === searchType
|
||||
: true)
|
||||
);
|
||||
|
||||
return applyCorrectionsToSources(results);
|
||||
} catch (error) {
|
||||
console.error('[Play] 读取缓存失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const initAll = async () => {
|
||||
if (currentSource === 'directplay') {
|
||||
if (!currentId) {
|
||||
@@ -3339,22 +3540,34 @@ function PlayPageClient() {
|
||||
let sourcesInfo: SearchResult[] = [];
|
||||
|
||||
if (currentSource && currentId) {
|
||||
// 先快速获取当前源的详情
|
||||
try {
|
||||
// currentSource 已经是完整格式(如 'emby_wumei')
|
||||
// 如果是小雅源且有fileName参数,传递给API
|
||||
const currentSourceDetail = await fetchSourceDetail(
|
||||
currentSource,
|
||||
currentId,
|
||||
searchTitle || videoTitle,
|
||||
currentSource === 'xiaoya' ? fileName : undefined
|
||||
);
|
||||
if (currentSourceDetail.length > 0) {
|
||||
detailData = currentSourceDetail[0];
|
||||
sourcesInfo = currentSourceDetail;
|
||||
const cachedSources = getCachedSourcesData(searchTitle || videoTitle);
|
||||
const cachedTarget = cachedSources.find(
|
||||
(source) => source.source === currentSource && source.id === currentId
|
||||
);
|
||||
|
||||
if (cachedTarget?.episodes?.length) {
|
||||
detailData = cachedTarget;
|
||||
sourcesInfo = cachedSources;
|
||||
setAvailableSources(cachedSources);
|
||||
setSourceSearchLoading(false);
|
||||
} else {
|
||||
// 先快速获取当前源的详情
|
||||
try {
|
||||
// currentSource 已经是完整格式(如 'emby_wumei')
|
||||
// 如果是小雅源且有fileName参数,传递给API
|
||||
const currentSourceDetail = await fetchSourceDetail(
|
||||
currentSource,
|
||||
currentId,
|
||||
searchTitle || videoTitle,
|
||||
currentSource === 'xiaoya' ? fileName : undefined
|
||||
);
|
||||
if (currentSourceDetail.length > 0) {
|
||||
detailData = currentSourceDetail[0];
|
||||
sourcesInfo = currentSourceDetail;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取当前源详情失败:', err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取当前源详情失败:', err);
|
||||
}
|
||||
|
||||
// 异步获取其他源信息,不阻塞播放
|
||||
@@ -3396,9 +3609,9 @@ function PlayPageClient() {
|
||||
if (target) {
|
||||
detailData = target;
|
||||
|
||||
// 如果是 openlist 或 emby 源且 episodes 为空,需要调用 detail 接口获取完整信息
|
||||
if ((detailData.source === 'openlist' || detailData.source === 'emby' || detailData.source.startsWith('emby_')) && (!detailData.episodes || detailData.episodes.length === 0)) {
|
||||
console.log('[Play] OpenList/Emby source has no episodes, fetching detail...');
|
||||
// 这类源统一通过详情接口补全播放数据
|
||||
if (isLazyDetailSource(detailData.source) && (!detailData.episodes || detailData.episodes.length === 0)) {
|
||||
console.log('[Play] Fetching lazy detail for selected source...');
|
||||
// currentSource 已经是完整格式
|
||||
const detailSources = await fetchSourceDetail(currentSource, currentId, searchTitle || videoTitle);
|
||||
if (detailSources.length > 0) {
|
||||
@@ -3431,6 +3644,9 @@ function PlayPageClient() {
|
||||
// 检查是否为 xiaoya 源
|
||||
if (s.source === 'xiaoya') return false;
|
||||
|
||||
// 脚本源详情懒加载,不参与测速
|
||||
if (s.source.startsWith('script:')) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -3438,13 +3654,14 @@ function PlayPageClient() {
|
||||
s.source === 'openlist' ||
|
||||
s.source === 'emby' ||
|
||||
s.source.startsWith('emby_') ||
|
||||
s.source === 'xiaoya'
|
||||
s.source === 'xiaoya' ||
|
||||
s.source.startsWith('script:')
|
||||
);
|
||||
|
||||
if (sourcesToTest.length > 0) {
|
||||
detailData = await preferBestSource(sourcesToTest);
|
||||
} else if (excludedSources.length > 0) {
|
||||
// 如果只有 openlist/emby/xiaoya 源,直接使用第一个
|
||||
// 如果只有懒加载详情的源,直接使用第一个
|
||||
detailData = excludedSources[0];
|
||||
} else {
|
||||
detailData = sourcesInfo[0];
|
||||
@@ -3453,9 +3670,9 @@ function PlayPageClient() {
|
||||
|
||||
console.log(detailData.source, detailData.id);
|
||||
|
||||
// 如果是 openlist 或 emby 源且 episodes 为空,需要调用 detail 接口获取完整信息
|
||||
if ((detailData.source === 'openlist' || detailData.source === 'emby') && (!detailData.episodes || detailData.episodes.length === 0)) {
|
||||
console.log('[Play] OpenList/Emby source has no episodes after selection, fetching detail...');
|
||||
// 这类源统一通过详情接口补全播放数据
|
||||
if (isLazyDetailSource(detailData.source) && (!detailData.episodes || detailData.episodes.length === 0)) {
|
||||
console.log('[Play] Fetching lazy detail after source selection...');
|
||||
const detailSources = await fetchSourceDetail(detailData.source, detailData.id, detailData.title || videoTitleRef.current);
|
||||
if (detailSources.length > 0) {
|
||||
detailData = detailSources[0];
|
||||
@@ -3770,8 +3987,8 @@ function PlayPageClient() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是 openlist 或 emby 源且 episodes 为空,需要调用 detail 接口获取完整信息
|
||||
if ((newDetail.source === 'openlist' || newDetail.source === 'emby' || newDetail.source.startsWith('emby_')) && (!newDetail.episodes || newDetail.episodes.length === 0)) {
|
||||
// 这类源统一通过详情接口补全播放数据
|
||||
if (isLazyDetailSource(newDetail.source) && (!newDetail.episodes || newDetail.episodes.length === 0)) {
|
||||
try {
|
||||
const detailResponse = await fetch(`/api/source-detail?source=${newSource}&id=${newId}&title=${encodeURIComponent(newTitle)}`);
|
||||
if (detailResponse.ok) {
|
||||
@@ -3781,10 +3998,10 @@ function PlayPageClient() {
|
||||
}
|
||||
newDetail = detailData;
|
||||
} else {
|
||||
throw new Error('获取 openlist 详情失败');
|
||||
throw new Error('获取视频详情失败');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取 openlist 详情失败:', err);
|
||||
console.error('获取视频详情失败:', err);
|
||||
setIsVideoLoading(false);
|
||||
setError('获取视频详情失败,请重试');
|
||||
return;
|
||||
@@ -3881,6 +4098,9 @@ function PlayPageClient() {
|
||||
if (artPlayerRef.current && artPlayerRef.current.paused) {
|
||||
saveCurrentPlayProgress();
|
||||
}
|
||||
setVideoLoadingStage('episodeChanging');
|
||||
setIsVideoLoading(true);
|
||||
setVideoError(null);
|
||||
setCurrentEpisodeIndex(episodeNumber);
|
||||
}
|
||||
};
|
||||
@@ -3892,6 +4112,9 @@ function PlayPageClient() {
|
||||
if (artPlayerRef.current && !artPlayerRef.current.paused) {
|
||||
saveCurrentPlayProgress();
|
||||
}
|
||||
setVideoLoadingStage('episodeChanging');
|
||||
setIsVideoLoading(true);
|
||||
setVideoError(null);
|
||||
setCurrentEpisodeIndex(idx - 1);
|
||||
}
|
||||
};
|
||||
@@ -3940,6 +4163,9 @@ function PlayPageClient() {
|
||||
const isFiltered = episodeTitle && isEpisodeFilteredByTitle(episodeTitle);
|
||||
|
||||
if (!isFiltered) {
|
||||
setVideoLoadingStage('episodeChanging');
|
||||
setIsVideoLoading(true);
|
||||
setVideoError(null);
|
||||
setCurrentEpisodeIndex(nextIdx);
|
||||
return;
|
||||
}
|
||||
@@ -4078,6 +4304,7 @@ function PlayPageClient() {
|
||||
episodeTitle?: string;
|
||||
searchKeyword?: string;
|
||||
danmakuCount?: number;
|
||||
bypassCache?: boolean;
|
||||
}) => {
|
||||
if (!danmakuPluginRef.current) {
|
||||
console.warn('弹幕插件未初始化');
|
||||
@@ -4107,7 +4334,13 @@ function PlayPageClient() {
|
||||
|
||||
console.log(`[弹幕加载] episodeId=${episodeId}, title="${title}", episodeIndex=${episodeIndex}`);
|
||||
|
||||
const comments = await getDanmakuById(episodeId, title, episodeIndex, metadata);
|
||||
const comments = await getDanmakuById(
|
||||
episodeId,
|
||||
title,
|
||||
episodeIndex,
|
||||
{ bypassCache: metadata?.bypassCache === true },
|
||||
metadata
|
||||
);
|
||||
|
||||
if (comments.length === 0) {
|
||||
console.warn('未获取到弹幕数据');
|
||||
@@ -4270,6 +4503,7 @@ function PlayPageClient() {
|
||||
episode.episodeId,
|
||||
title,
|
||||
nextEpisodeIndex,
|
||||
undefined,
|
||||
{
|
||||
animeId: savedAnimeId,
|
||||
animeTitle: episodesResult.bangumi.animeTitle,
|
||||
@@ -4321,6 +4555,7 @@ function PlayPageClient() {
|
||||
episode.episodeId,
|
||||
title,
|
||||
nextEpisodeIndex,
|
||||
undefined,
|
||||
{
|
||||
animeId: selectedAnime.animeId,
|
||||
animeTitle: selectedAnime.animeTitle,
|
||||
@@ -4462,6 +4697,7 @@ function PlayPageClient() {
|
||||
episodeTitle: selection.episodeTitle,
|
||||
searchKeyword: selection.searchKeyword,
|
||||
danmakuCount: selection.danmakuCount,
|
||||
bypassCache: isManual,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5079,8 +5315,8 @@ function PlayPageClient() {
|
||||
return;
|
||||
}
|
||||
|
||||
// openlist 和 emby 源的剧集是懒加载的,如果 episodes 为空则跳过检查
|
||||
if ((currentSource === 'openlist' || currentSource === 'emby' || detail?.source === 'openlist' || detail?.source === 'emby') && (!detail || !detail.episodes || detail.episodes.length === 0)) {
|
||||
// 这类源会先异步补全详情,如果 episodes 为空则跳过
|
||||
if (isLazyDetailSource(currentSource || detail?.source) && (!detail || !detail.episodes || detail.episodes.length === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7084,6 +7320,11 @@ function PlayPageClient() {
|
||||
// 条件:当前播放时间 < 10秒 且 播放记录时间 > 10秒
|
||||
const checkPlayRecordJump = async () => {
|
||||
try {
|
||||
// 仅在进入播放后的首次检查时处理,避免本次会话新生成的记录触发恢复按钮
|
||||
if (!playRecordJumpInitialCheckRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果用户已经关闭过跳转按钮,不再显示
|
||||
if (playRecordJumpDismissedRef.current) {
|
||||
return;
|
||||
@@ -8182,6 +8423,31 @@ function PlayPageClient() {
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showExternalTranscodeButton && (
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
await handleCreateTranscodeSession();
|
||||
}}
|
||||
disabled={isTranscoding}
|
||||
className={`group relative flex items-center justify-center gap-1 w-8 h-8 lg:w-auto lg:h-auto lg:px-2 lg:py-1.5 text-xs font-medium rounded-md transition-all duration-200 shadow-sm hover:shadow-md overflow-hidden border flex-shrink-0 ${
|
||||
isTranscoding
|
||||
? 'bg-amber-400 text-white border-amber-400 cursor-wait'
|
||||
: 'bg-amber-500 hover:bg-amber-600 text-white border-amber-500 cursor-pointer'
|
||||
}`}
|
||||
title='转码播放'
|
||||
>
|
||||
{isTranscoding ? (
|
||||
<Loader2 className='w-4 h-4 flex-shrink-0 text-white animate-spin' />
|
||||
) : (
|
||||
<Router className='w-4 h-4 flex-shrink-0 text-white' />
|
||||
)}
|
||||
<span className='hidden lg:inline max-w-0 group-hover:max-w-[100px] overflow-hidden whitespace-nowrap transition-all duration-200 ease-in-out text-white'>
|
||||
{isTranscoding ? '转码中' : '转码'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* PotPlayer */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
||||
+211
-64
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { getEpisodes, searchAnime } from '@/lib/danmaku/api';
|
||||
import type {
|
||||
@@ -35,6 +35,10 @@ export default function DanmakuPanel({
|
||||
const [searchError, setSearchError] = useState<string | null>(null);
|
||||
const initializedRef = useRef(false); // 标记是否已初始化过
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [episodeGroupIndex, setEpisodeGroupIndex] = useState(0);
|
||||
const [episodeDescending, setEpisodeDescending] = useState(false);
|
||||
const [episodeViewMode, setEpisodeViewMode] = useState<'list' | 'grid'>('list');
|
||||
const episodesPerGroup = 50;
|
||||
|
||||
// 搜索弹幕
|
||||
const handleSearch = useCallback(async (keyword: string) => {
|
||||
@@ -112,6 +116,7 @@ export default function DanmakuPanel({
|
||||
const handleBackToResults = useCallback(() => {
|
||||
setSelectedAnime(null);
|
||||
setEpisodes([]);
|
||||
setEpisodeGroupIndex(0);
|
||||
}, []);
|
||||
|
||||
// 判断当前剧集是否已选中
|
||||
@@ -162,6 +167,69 @@ export default function DanmakuPanel({
|
||||
}
|
||||
}, [videoTitle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (episodes.length > 0) {
|
||||
setEpisodeGroupIndex(Math.floor(currentEpisodeIndex / episodesPerGroup));
|
||||
} else {
|
||||
setEpisodeGroupIndex(0);
|
||||
}
|
||||
}, [episodes, currentEpisodeIndex]);
|
||||
|
||||
const episodeGroupCount = Math.ceil(episodes.length / episodesPerGroup);
|
||||
|
||||
const episodeGroups = useMemo(() => {
|
||||
return Array.from({ length: episodeGroupCount }, (_, idx) => {
|
||||
const start = idx * episodesPerGroup + 1;
|
||||
const end = Math.min((idx + 1) * episodesPerGroup, episodes.length);
|
||||
return `${start}-${end}`;
|
||||
});
|
||||
}, [episodeGroupCount, episodes.length]);
|
||||
|
||||
const displayEpisodeGroupIndex = useMemo(() => {
|
||||
if (episodeDescending) {
|
||||
return episodeGroupCount - 1 - episodeGroupIndex;
|
||||
}
|
||||
return episodeGroupIndex;
|
||||
}, [episodeDescending, episodeGroupCount, episodeGroupIndex]);
|
||||
|
||||
const currentGroupEpisodes = useMemo(() => {
|
||||
if (episodes.length === 0) return [];
|
||||
|
||||
const start = episodeGroupIndex * episodesPerGroup;
|
||||
const end = Math.min(start + episodesPerGroup, episodes.length);
|
||||
const groupEpisodes = episodes.slice(start, end);
|
||||
const withEpisodeNumber = groupEpisodes.map((episode, index) => ({
|
||||
...episode,
|
||||
episodeNumber: start + index + 1,
|
||||
}));
|
||||
|
||||
return episodeDescending ? [...withEpisodeNumber].reverse() : withEpisodeNumber;
|
||||
}, [episodes, episodeDescending, episodeGroupIndex]);
|
||||
|
||||
const getEpisodeDisplayLabel = useCallback((episodeTitle: string, episodeNumber: number) => {
|
||||
if (!episodeTitle) {
|
||||
return String(episodeNumber);
|
||||
}
|
||||
|
||||
if (episodeTitle.match(/^OVA\s+\d+/i)) {
|
||||
return episodeTitle;
|
||||
}
|
||||
|
||||
const sxxexxMatch = episodeTitle.match(/[Ss](\d+)[Ee](\d{1,4}(?:\.\d+)?)/);
|
||||
if (sxxexxMatch) {
|
||||
const season = sxxexxMatch[1].padStart(2, '0');
|
||||
const episode = sxxexxMatch[2];
|
||||
return `S${season}E${episode}`;
|
||||
}
|
||||
|
||||
const match = episodeTitle.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
return String(episodeNumber);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col overflow-hidden'>
|
||||
{/* 搜索区域 - 固定在顶部 */}
|
||||
@@ -278,72 +346,151 @@ export default function DanmakuPanel({
|
||||
|
||||
{/* 剧集列表 */}
|
||||
{!isLoadingEpisodes && episodes.length > 0 && (
|
||||
<div className='space-y-2 pb-4'>
|
||||
{episodes.map((episode, index) => {
|
||||
const isSelected = isEpisodeSelected(episode.episodeId);
|
||||
return (
|
||||
<button
|
||||
key={episode.episodeId}
|
||||
onClick={() => handleEpisodeSelect(episode)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg text-left
|
||||
transition-all duration-200 group border
|
||||
${
|
||||
isSelected
|
||||
? 'bg-green-500 text-white border-green-600 shadow-md'
|
||||
: 'bg-gray-100 hover:bg-gray-200 border-gray-200 ' +
|
||||
'dark:bg-gray-800 dark:hover:bg-gray-700 dark:border-gray-700 ' +
|
||||
'hover:border-green-500/50 hover:shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{/* 序号徽章 */}
|
||||
<div className={`flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm
|
||||
${
|
||||
isSelected
|
||||
? 'bg-white/20 text-white'
|
||||
: 'bg-green-500 text-white group-hover:bg-green-600'
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
{/* 标题和信息 */}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-semibold text-sm mb-1 truncate'>
|
||||
{episode.episodeTitle}
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 text-xs
|
||||
${
|
||||
isSelected
|
||||
? 'text-white/80'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
<div className='pb-4'>
|
||||
<div className='mb-4 border-b border-gray-300 dark:border-gray-700'>
|
||||
<div className='flex items-center gap-4 overflow-x-auto pb-3'>
|
||||
{episodeGroups.map((label, idx) => {
|
||||
const isActive = idx === displayEpisodeGroupIndex;
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
onClick={() =>
|
||||
setEpisodeGroupIndex(
|
||||
episodeDescending ? episodeGroupCount - 1 - idx : idx
|
||||
)
|
||||
}
|
||||
className={`relative w-20 py-2 text-sm font-medium transition-colors whitespace-nowrap flex-shrink-0 text-center ${
|
||||
isActive
|
||||
? 'text-green-500 dark:text-green-400'
|
||||
: 'text-gray-700 hover:text-green-600 dark:text-gray-300 dark:hover:text-green-400'
|
||||
}`}
|
||||
>
|
||||
<span className='flex items-center gap-1'>
|
||||
🆔 ID: {episode.episodeId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 选中标记 */}
|
||||
{isSelected && (
|
||||
<div className='flex-shrink-0'>
|
||||
<svg className='w-6 h-6 text-white' fill='currentColor' viewBox='0 0 20 20'>
|
||||
<path fillRule='evenodd' d='M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z' clipRule='evenodd' />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 未选中时的箭头 */}
|
||||
{!isSelected && (
|
||||
<div className='flex-shrink-0'>
|
||||
<svg className='w-5 h-5 text-gray-400 group-hover:text-green-500 transition-colors' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M9 5l7 7-7 7' />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{label}
|
||||
{isActive && (
|
||||
<div className='absolute bottom-0 left-0 right-0 h-0.5 bg-green-500 dark:bg-green-400' />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
onClick={() => setEpisodeDescending((prev) => !prev)}
|
||||
className='flex-shrink-0 rounded-md p-2 text-gray-700 hover:bg-gray-100 hover:text-green-600 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-400'
|
||||
title={episodeDescending ? '切换正序' : '切换倒序'}
|
||||
>
|
||||
<svg className='h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4' />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className='ml-auto flex items-center gap-1 rounded-md bg-gray-100 p-1 dark:bg-gray-800'>
|
||||
<button
|
||||
onClick={() => setEpisodeViewMode('list')}
|
||||
title='列表视图'
|
||||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
episodeViewMode === 'list'
|
||||
? 'bg-white text-green-600 shadow-sm dark:bg-gray-700 dark:text-green-400'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<svg className='h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01' />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEpisodeViewMode('grid')}
|
||||
title='格子视图'
|
||||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
episodeViewMode === 'grid'
|
||||
? 'bg-white text-green-600 shadow-sm dark:bg-gray-700 dark:text-green-400'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<svg className='h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M4 4h6v6H4V4zm10 0h6v6h-6V4zM4 14h6v6H4v-6zm10 0h6v6h-6v-6z' />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{episodeViewMode === 'grid' ? (
|
||||
<div className='grid grid-cols-3 gap-2 sm:grid-cols-4'>
|
||||
{currentGroupEpisodes.map((episode) => {
|
||||
const isSelected = isEpisodeSelected(episode.episodeId);
|
||||
return (
|
||||
<button
|
||||
key={episode.episodeId}
|
||||
onClick={() => handleEpisodeSelect(episode)}
|
||||
className={`rounded-lg px-3 py-2 text-sm font-medium transition-all ${
|
||||
isSelected
|
||||
? 'bg-green-500 text-white shadow-md'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={episode.episodeTitle}
|
||||
>
|
||||
<div className='truncate'>
|
||||
{getEpisodeDisplayLabel(episode.episodeTitle, episode.episodeNumber)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-2'>
|
||||
{currentGroupEpisodes.map((episode) => {
|
||||
const isSelected = isEpisodeSelected(episode.episodeId);
|
||||
return (
|
||||
<button
|
||||
key={episode.episodeId}
|
||||
onClick={() => handleEpisodeSelect(episode)}
|
||||
className={`w-full flex items-center gap-3 p-3 rounded-lg text-left transition-all duration-200 group border ${
|
||||
isSelected
|
||||
? 'bg-green-500 text-white border-green-600 shadow-md'
|
||||
: 'bg-gray-100 hover:bg-gray-200 border-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 dark:border-gray-700 hover:border-green-500/50 hover:shadow-sm'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm ${
|
||||
isSelected
|
||||
? 'bg-white/20 text-white'
|
||||
: 'bg-green-500 text-white group-hover:bg-green-600'
|
||||
}`}
|
||||
>
|
||||
{episode.episodeNumber}
|
||||
</div>
|
||||
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='font-semibold text-sm mb-1 truncate'>
|
||||
{episode.episodeTitle}
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center gap-2 text-xs ${
|
||||
isSelected ? 'text-white/80' : 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<span className='flex items-center gap-1'>
|
||||
🆔 ID: {episode.episodeId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSelected ? (
|
||||
<div className='flex-shrink-0'>
|
||||
<svg className='w-6 h-6 text-white' fill='currentColor' viewBox='0 0 20 20'>
|
||||
<path fillRule='evenodd' d='M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z' clipRule='evenodd' />
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex-shrink-0'>
|
||||
<svg className='w-5 h-5 text-gray-400 group-hover:text-green-500 transition-colors' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
|
||||
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M9 5l7 7-7 7' />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Calendar, Clock, Film,Globe, Star, Tag, Users, X } from 'lucide-react';
|
||||
import { Calendar, Clock, ExternalLink, Film,Globe, Star, Tag, Users, X } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
@@ -101,12 +101,38 @@ const DetailPanel: React.FC<DetailPanelProps> = ({
|
||||
const [showImageViewer, setShowImageViewer] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<string>('');
|
||||
|
||||
|
||||
// 数据源状态管理
|
||||
const [currentSource, setCurrentSource] = useState<'douban' | 'bangumi' | 'cms' | 'tmdb'>('tmdb');
|
||||
const [originalSource, setOriginalSource] = useState<'douban' | 'bangumi' | 'cms' | 'tmdb'>('tmdb');
|
||||
const [isUsingTmdb, setIsUsingTmdb] = useState(false);
|
||||
const [originalDetailData, setOriginalDetailData] = useState<DetailData | null>(null);
|
||||
|
||||
const getExternalUrl = () => {
|
||||
if (currentSource === 'douban' && doubanId) {
|
||||
return `https://movie.douban.com/subject/${doubanId}`;
|
||||
}
|
||||
|
||||
if (currentSource === 'bangumi') {
|
||||
const actualBangumiId = bangumiId || doubanId;
|
||||
if (actualBangumiId) {
|
||||
return `https://bgm.tv/subject/${actualBangumiId}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSource === 'tmdb') {
|
||||
const actualTmdbId = detailData?.tmdbId || tmdbId;
|
||||
const actualMediaType = detailData?.mediaType || type;
|
||||
if (actualTmdbId) {
|
||||
return `https://www.themoviedb.org/${actualMediaType}/${actualTmdbId}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const externalUrl = getExternalUrl();
|
||||
|
||||
// 拖动滚动状态
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
@@ -874,12 +900,26 @@ const DetailPanel: React.FC<DetailPanelProps> = ({
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-100 dark:border-gray-800 sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">详情</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
|
||||
>
|
||||
<X size={20} className="text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{externalUrl && (
|
||||
<button
|
||||
onClick={() => window.open(externalUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
|
||||
title="打开外部页面"
|
||||
aria-label="打开外部页面"
|
||||
>
|
||||
<ExternalLink size={18} className="text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
|
||||
title="关闭"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<X size={20} className="text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
@@ -1364,12 +1404,26 @@ const DetailPanel: React.FC<DetailPanelProps> = ({
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-100 dark:border-gray-800 sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">详情</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
|
||||
>
|
||||
<X size={20} className="text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{externalUrl && (
|
||||
<button
|
||||
onClick={() => window.open(externalUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
|
||||
title="打开外部页面"
|
||||
aria-label="打开外部页面"
|
||||
>
|
||||
<ExternalLink size={18} className="text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-150"
|
||||
title="关闭"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<X size={20} className="text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface ErrorInfo {
|
||||
id: string;
|
||||
@@ -12,6 +12,42 @@ export function GlobalErrorIndicator() {
|
||||
const [currentError, setCurrentError] = useState<ErrorInfo | null>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isReplacing, setIsReplacing] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const currentErrorRef = useRef<ErrorInfo | null>(null);
|
||||
const closeTimerRef = useRef<number | null>(null);
|
||||
const exitTimerRef = useRef<number | null>(null);
|
||||
|
||||
const clearCloseTimer = useCallback(() => {
|
||||
if (closeTimerRef.current !== null) {
|
||||
window.clearTimeout(closeTimerRef.current);
|
||||
closeTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearExitTimer = useCallback(() => {
|
||||
if (exitTimerRef.current !== null) {
|
||||
window.clearTimeout(exitTimerRef.current);
|
||||
exitTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
clearCloseTimer();
|
||||
clearExitTimer();
|
||||
setIsClosing(true);
|
||||
setIsReplacing(false);
|
||||
|
||||
exitTimerRef.current = window.setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
setCurrentError(null);
|
||||
setIsClosing(false);
|
||||
exitTimerRef.current = null;
|
||||
}, 300);
|
||||
}, [clearCloseTimer, clearExitTimer]);
|
||||
|
||||
useEffect(() => {
|
||||
currentErrorRef.current = currentError;
|
||||
}, [currentError]);
|
||||
|
||||
useEffect(() => {
|
||||
// 监听自定义错误事件
|
||||
@@ -23,8 +59,13 @@ export function GlobalErrorIndicator() {
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
clearCloseTimer();
|
||||
clearExitTimer();
|
||||
setIsClosing(false);
|
||||
setIsVisible(true);
|
||||
|
||||
// 如果已有错误,开始替换动画
|
||||
if (currentError) {
|
||||
if (currentErrorRef.current) {
|
||||
setCurrentError(newError);
|
||||
setIsReplacing(true);
|
||||
|
||||
@@ -36,23 +77,32 @@ export function GlobalErrorIndicator() {
|
||||
// 第一次显示错误
|
||||
setCurrentError(newError);
|
||||
}
|
||||
|
||||
setIsVisible(true);
|
||||
};
|
||||
|
||||
// 监听错误事件
|
||||
window.addEventListener('globalError', handleError as EventListener);
|
||||
|
||||
return () => {
|
||||
clearCloseTimer();
|
||||
clearExitTimer();
|
||||
window.removeEventListener('globalError', handleError as EventListener);
|
||||
};
|
||||
}, [currentError]);
|
||||
}, [clearCloseTimer, clearExitTimer]);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
setCurrentError(null);
|
||||
setIsReplacing(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!currentError || isClosing) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearCloseTimer();
|
||||
closeTimerRef.current = window.setTimeout(() => {
|
||||
handleClose();
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearCloseTimer();
|
||||
};
|
||||
}, [currentError, handleClose, isClosing, clearCloseTimer]);
|
||||
|
||||
if (!isVisible || !currentError) {
|
||||
return null;
|
||||
@@ -63,6 +113,10 @@ export function GlobalErrorIndicator() {
|
||||
{/* 错误卡片 */}
|
||||
<div
|
||||
className={`bg-red-500 text-white px-4 py-3 rounded-lg shadow-lg flex items-center justify-between min-w-[300px] max-w-[400px] transition-all duration-300 ${
|
||||
isClosing
|
||||
? '-translate-y-4 opacity-0'
|
||||
: 'translate-y-0 opacity-100'
|
||||
} ${
|
||||
isReplacing ? 'scale-105 bg-red-400' : 'scale-100 bg-red-500'
|
||||
} animate-fade-in`}
|
||||
>
|
||||
|
||||
@@ -21,6 +21,7 @@ interface MobileActionSheetProps {
|
||||
sources?: string[]; // 播放源信息
|
||||
isAggregate?: boolean; // 是否为聚合内容
|
||||
sourceName?: string; // 播放源名称
|
||||
directLinkUrl?: string; // 直链播放完整链接
|
||||
currentEpisode?: number; // 当前集数
|
||||
totalEpisodes?: number; // 总集数
|
||||
origin?: 'vod' | 'live';
|
||||
@@ -36,6 +37,7 @@ const MobileActionSheet: React.FC<MobileActionSheetProps> = ({
|
||||
sources,
|
||||
isAggregate,
|
||||
sourceName,
|
||||
directLinkUrl,
|
||||
currentEpisode,
|
||||
totalEpisodes,
|
||||
origin = 'vod',
|
||||
@@ -253,6 +255,11 @@ const MobileActionSheet: React.FC<MobileActionSheetProps> = ({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{directLinkUrl && (
|
||||
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400 break-all">
|
||||
{directLinkUrl}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
选择操作
|
||||
</p>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { Cat, Clover, Film, FolderOpen, Globe, Home, Star, Tv, Users } from 'lucide-react';
|
||||
import { Blend, Cat, Clover, Container, Film, Globe, Home, Star, Tv, TvMinimalPlay, Users } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -51,7 +51,7 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
href: '/douban?type=show',
|
||||
},
|
||||
{
|
||||
icon: Tv,
|
||||
icon: TvMinimalPlay,
|
||||
label: '电视直播',
|
||||
href: '/live',
|
||||
},
|
||||
@@ -89,7 +89,7 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
href: '/douban?type=show',
|
||||
},
|
||||
{
|
||||
icon: Tv,
|
||||
icon: TvMinimalPlay,
|
||||
label: '电视直播',
|
||||
href: '/live',
|
||||
},
|
||||
@@ -107,12 +107,20 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
// 如果配置了 OpenList 或 Emby,添加私人影库入口
|
||||
if (runtimeConfig?.PRIVATE_LIBRARY_ENABLED) {
|
||||
items.push({
|
||||
icon: FolderOpen,
|
||||
icon: Container,
|
||||
label: '私人影库',
|
||||
href: '/private-library',
|
||||
});
|
||||
}
|
||||
|
||||
if (runtimeConfig?.ADVANCED_RECOMMENDATION_ENABLED) {
|
||||
items.push({
|
||||
icon: Blend,
|
||||
label: '高级推荐',
|
||||
href: '/advanced-recommendation',
|
||||
});
|
||||
}
|
||||
|
||||
// 如果启用观影室,添加观影室入口
|
||||
if (watchRoomContext?.isEnabled) {
|
||||
items.push({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { Cat, Clover, Film, FolderOpen, Globe, Home, Menu, Search, Star, Tv, Users } from 'lucide-react';
|
||||
import { Blend, Cat, Clover, Container, Film, Globe, Home, Menu, Search, Star, Tv, TvMinimalPlay, Users } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import {
|
||||
@@ -143,7 +143,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
href: '/douban?type=show',
|
||||
},
|
||||
{
|
||||
icon: Tv,
|
||||
icon: TvMinimalPlay,
|
||||
label: '电视直播',
|
||||
href: '/live',
|
||||
},
|
||||
@@ -180,7 +180,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
href: '/douban?type=show',
|
||||
},
|
||||
{
|
||||
icon: Tv,
|
||||
icon: TvMinimalPlay,
|
||||
label: '电视直播',
|
||||
href: '/live',
|
||||
},
|
||||
@@ -198,12 +198,20 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
// 如果配置了 OpenList 或 Emby,添加私人影库入口
|
||||
if (runtimeConfig?.PRIVATE_LIBRARY_ENABLED) {
|
||||
items.push({
|
||||
icon: FolderOpen,
|
||||
icon: Container,
|
||||
label: '私人影库',
|
||||
href: '/private-library',
|
||||
});
|
||||
}
|
||||
|
||||
if (runtimeConfig?.ADVANCED_RECOMMENDATION_ENABLED) {
|
||||
items.push({
|
||||
icon: Blend,
|
||||
label: '高级推荐',
|
||||
href: '/advanced-recommendation',
|
||||
});
|
||||
}
|
||||
|
||||
// 如果启用观影室,添加观影室入口
|
||||
if (watchRoomContext?.isEnabled) {
|
||||
items.push({
|
||||
|
||||
+111
-64
@@ -23,6 +23,7 @@ import {
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
Package,
|
||||
Router as RouterIcon,
|
||||
Rss,
|
||||
Settings,
|
||||
Shield,
|
||||
@@ -78,12 +79,12 @@ export const UserMenu: React.FC = () => {
|
||||
// 订阅相关状态
|
||||
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
|
||||
const [subscribeUrl, setSubscribeUrl] = useState('');
|
||||
const [subscribeUrlWithAdFilter, setSubscribeUrlWithAdFilter] = useState('');
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const [copySuccessAdFilter, setCopySuccessAdFilter] = useState(false);
|
||||
const [tvboxToken, setTvboxToken] = useState('');
|
||||
const [isResettingToken, setIsResettingToken] = useState(false);
|
||||
const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false);
|
||||
const [subscribeAdFilterEnabled, setSubscribeAdFilterEnabled] = useState(false);
|
||||
const [subscribeYellowFilterEnabled, setSubscribeYellowFilterEnabled] = useState(false);
|
||||
|
||||
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
||||
useEffect(() => {
|
||||
@@ -156,7 +157,7 @@ export const UserMenu: React.FC = () => {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
onConfirm: () => {},
|
||||
onConfirm: () => undefined,
|
||||
});
|
||||
|
||||
// 折叠面板状态
|
||||
@@ -323,13 +324,7 @@ export const UserMenu: React.FC = () => {
|
||||
const token = data.token;
|
||||
setTvboxToken(token);
|
||||
|
||||
// 前端拼接订阅链接
|
||||
const currentOrigin = window.location.origin;
|
||||
const standardUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}`;
|
||||
const adFilterUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}&adFilter=true`;
|
||||
|
||||
setSubscribeUrl(standardUrl);
|
||||
setSubscribeUrlWithAdFilter(adFilterUrl);
|
||||
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订阅URL失败:', error);
|
||||
@@ -359,13 +354,7 @@ export const UserMenu: React.FC = () => {
|
||||
const token = data.token;
|
||||
setTvboxToken(token);
|
||||
|
||||
// 更新订阅链接
|
||||
const currentOrigin = window.location.origin;
|
||||
const standardUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}`;
|
||||
const adFilterUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}&adFilter=true`;
|
||||
|
||||
setSubscribeUrl(standardUrl);
|
||||
setSubscribeUrlWithAdFilter(adFilterUrl);
|
||||
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||
|
||||
if (messageEl) {
|
||||
messageEl.textContent = '订阅token已重置!';
|
||||
@@ -398,6 +387,19 @@ export const UserMenu: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const buildSubscribeUrl = (token: string, adFilter: boolean, yellowFilter: boolean) => {
|
||||
const currentOrigin = window.location.origin;
|
||||
const url = new URL('/api/tvbox/subscribe', currentOrigin);
|
||||
url.searchParams.set('token', token);
|
||||
if (adFilter) {
|
||||
url.searchParams.set('adFilter', 'true');
|
||||
}
|
||||
if (yellowFilter) {
|
||||
url.searchParams.set('yellowFilter', 'true');
|
||||
}
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
// 获取认证信息和存储类型
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -819,7 +821,6 @@ export const UserMenu: React.FC = () => {
|
||||
const handleCloseSubscribe = () => {
|
||||
setIsSubscribeOpen(false);
|
||||
setCopySuccess(false);
|
||||
setCopySuccessAdFilter(false);
|
||||
};
|
||||
|
||||
const handleCopySubscribeUrl = async () => {
|
||||
@@ -833,18 +834,11 @@ export const UserMenu: React.FC = () => {
|
||||
console.error('复制失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopySubscribeUrlWithAdFilter = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(subscribeUrlWithAdFilter);
|
||||
setCopySuccessAdFilter(true);
|
||||
setTimeout(() => {
|
||||
setCopySuccessAdFilter(false);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!tvboxToken || !isSubscribeOpen) return;
|
||||
setSubscribeUrl(buildSubscribeUrl(tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||
}, [tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled, isSubscribeOpen]);
|
||||
|
||||
const handleSubmitChangePassword = async () => {
|
||||
setPasswordError('');
|
||||
@@ -2833,18 +2827,18 @@ export const UserMenu: React.FC = () => {
|
||||
<div className='space-y-4'>
|
||||
{isLoadingSubscribeUrl ? (
|
||||
<>
|
||||
{/* 加载骨架 - 订阅链接(标准) */}
|
||||
{/* 加载骨架 - 开关 */}
|
||||
<div>
|
||||
<div className='h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded mb-2 animate-pulse'></div>
|
||||
<div className='flex gap-2'>
|
||||
<div className='flex-1 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
|
||||
<div className='w-20 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
|
||||
<div className='h-5 w-24 bg-gray-200 dark:bg-gray-700 rounded mb-3 animate-pulse'></div>
|
||||
<div className='space-y-2'>
|
||||
<div className='h-14 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
|
||||
<div className='h-14 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 加载骨架 - 订阅链接(去广告) */}
|
||||
{/* 加载骨架 - 订阅链接 */}
|
||||
<div>
|
||||
<div className='h-5 w-36 bg-gray-200 dark:bg-gray-700 rounded mb-2 animate-pulse'></div>
|
||||
<div className='h-5 w-28 bg-gray-200 dark:bg-gray-700 rounded mb-2 animate-pulse'></div>
|
||||
<div className='flex gap-2'>
|
||||
<div className='flex-1 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
|
||||
<div className='w-20 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
|
||||
@@ -2860,10 +2854,51 @@ export const UserMenu: React.FC = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* 订阅链接(标准) */}
|
||||
<div className='space-y-3'>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
订阅选项
|
||||
</h4>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setSubscribeAdFilterEnabled((prev) => !prev)}
|
||||
className='w-full flex items-center justify-between rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3 text-left bg-gray-50 dark:bg-gray-800/70'
|
||||
>
|
||||
<div>
|
||||
<div className='text-sm font-medium text-gray-800 dark:text-gray-200'>
|
||||
去广告
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
开启后通过代理处理播放链接,兼容性可能略低
|
||||
</div>
|
||||
</div>
|
||||
<div className={`relative h-6 w-11 rounded-full transition-colors ${subscribeAdFilterEnabled ? 'bg-green-500' : 'bg-gray-300 dark:bg-gray-600'}`}>
|
||||
<div className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${subscribeAdFilterEnabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setSubscribeYellowFilterEnabled((prev) => !prev)}
|
||||
className='w-full flex items-center justify-between rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3 text-left bg-gray-50 dark:bg-gray-800/70'
|
||||
>
|
||||
<div>
|
||||
<div className='text-sm font-medium text-gray-800 dark:text-gray-200'>
|
||||
黄色过滤
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
开启后同样走代理,并在代理搜索时过滤黄色内容
|
||||
</div>
|
||||
</div>
|
||||
<div className={`relative h-6 w-11 rounded-full transition-colors ${subscribeYellowFilterEnabled ? 'bg-yellow-500' : 'bg-gray-300 dark:bg-gray-600'}`}>
|
||||
<div className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${subscribeYellowFilterEnabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
订阅链接(标准)
|
||||
订阅链接
|
||||
</h4>
|
||||
<div className='flex gap-2'>
|
||||
<input
|
||||
@@ -2880,31 +2915,11 @@ export const UserMenu: React.FC = () => {
|
||||
{copySuccess ? '已复制' : '复制'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 订阅链接(去广告) */}
|
||||
<div>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||
订阅链接(去广告)
|
||||
</h4>
|
||||
<div className='flex gap-2'>
|
||||
<input
|
||||
type='text'
|
||||
className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||
value={subscribeUrlWithAdFilter}
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopySubscribeUrlWithAdFilter}
|
||||
className='px-4 py-2 bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600 text-white text-sm font-medium rounded-md transition-colors flex items-center gap-2 whitespace-nowrap'
|
||||
>
|
||||
<Copy className='w-4 h-4' />
|
||||
{copySuccessAdFilter ? '已复制' : '复制'}
|
||||
</button>
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
💡 去广告需要经过服务器代理,某些源可能因为区域或兼容问题无法播放
|
||||
</p>
|
||||
{(subscribeAdFilterEnabled || subscribeYellowFilterEnabled) && (
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
💡 代理模式已开启,某些源可能因为区域或兼容问题无法播放
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 重置Token按钮 */}
|
||||
@@ -3566,6 +3581,38 @@ export const UserMenu: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 私人影库转码器 */}
|
||||
<div className='bg-gray-50 dark:bg-gray-800 rounded-lg p-5 border border-gray-200 dark:border-gray-700'>
|
||||
<div className='flex items-start gap-4'>
|
||||
<div className='flex-shrink-0 relative'>
|
||||
<div className='w-16 h-16 rounded-xl bg-amber-500 flex items-center justify-center shadow-sm'>
|
||||
<RouterIcon className='w-8 h-8 text-white' />
|
||||
</div>
|
||||
<span className='absolute -top-1 -right-1 px-1.5 py-0.5 bg-red-500 text-white text-[10px] font-bold rounded'>
|
||||
MKV转码
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<h4 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2'>
|
||||
私人影库转码器
|
||||
</h4>
|
||||
<p className='text-sm text-gray-600 dark:text-gray-400 mb-3'>
|
||||
为私人影库中的 MKV 视频提供转码播放能力,可解析内封字幕并解决部分视频无音频问题,但通常需要较高的本机性能配置。
|
||||
</p>
|
||||
<a
|
||||
href='https://github.com/mtvpls/moontvplus-transcoder/tags'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='inline-flex items-center gap-2 px-4 py-2 bg-amber-500 hover:bg-amber-600 text-white text-sm font-medium rounded-lg transition-colors'
|
||||
>
|
||||
<Download className='w-4 h-4' />
|
||||
下载
|
||||
<ExternalLink className='w-3 h-3' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
saveFavorite,
|
||||
subscribeToDataUpdates,
|
||||
} from '@/lib/db.client';
|
||||
import { processImageUrl } from '@/lib/utils';
|
||||
import { processImageUrl, base58Decode } from '@/lib/utils';
|
||||
import { useLongPress } from '@/hooks/useLongPress';
|
||||
|
||||
import AIChatPanel from '@/components/AIChatPanel';
|
||||
@@ -172,6 +172,14 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
const actualQuery = query || '';
|
||||
const actualSearchType = type;
|
||||
const isDirectPlaySource = actualSource === 'directplay';
|
||||
const directLinkUrl = useMemo(() => {
|
||||
if (!isDirectPlaySource || !actualId) return '';
|
||||
try {
|
||||
return base58Decode(actualId);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [isDirectPlaySource, actualId]);
|
||||
const displayYear = useMemo(() => {
|
||||
if (!actualYear) return '';
|
||||
const normalized = actualYear.trim();
|
||||
@@ -938,6 +946,38 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 竖向模式:顶部直链地址显示 */}
|
||||
{orientation === 'vertical' && isDirectPlaySource && directLinkUrl && (
|
||||
<div
|
||||
className='absolute top-1 left-1 right-1 sm:top-2 sm:left-2 sm:right-2 pt-1 px-1 sm:pt-2 sm:px-2'
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
} as React.CSSProperties}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='text-[9px] sm:text-[10px] text-yellow-400 line-clamp-2 break-all'
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
} as React.CSSProperties}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
title={directLinkUrl}
|
||||
>
|
||||
{directLinkUrl}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actualEpisodes && actualEpisodes > 1 && orientation === 'vertical' && (
|
||||
<div
|
||||
className='absolute top-1 right-1 sm:top-2 sm:right-2 flex flex-col gap-0.5 sm:gap-1.5'
|
||||
@@ -1249,6 +1289,25 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
第{currentEpisode}集 · 共{actualEpisodes}集
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 直链地址 */}
|
||||
{isDirectPlaySource && directLinkUrl && (
|
||||
<div
|
||||
className='text-[10px] text-white/75 truncate'
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
WebkitTouchCallout: 'none',
|
||||
} as React.CSSProperties}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
title={directLinkUrl}
|
||||
>
|
||||
{directLinkUrl}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部渐变遮罩 - 用于进度条背景 */}
|
||||
@@ -1487,6 +1546,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
sources={isAggregate && dynamicSourceNames ? Array.from(new Set(dynamicSourceNames)) : undefined}
|
||||
isAggregate={isAggregate}
|
||||
sourceName={cmsData ? undefined : source_name}
|
||||
directLinkUrl={directLinkUrl || undefined}
|
||||
currentEpisode={currentEpisode}
|
||||
totalEpisodes={actualEpisodes}
|
||||
origin={origin}
|
||||
|
||||
@@ -30,6 +30,11 @@ export interface AdminConfig {
|
||||
PansouUsername?: string;
|
||||
PansouPassword?: string;
|
||||
PansouKeywordBlocklist?: string;
|
||||
// 磁链配置
|
||||
MagnetProxy?: string;
|
||||
MagnetMikanReverseProxy?: string;
|
||||
MagnetDmhyReverseProxy?: string;
|
||||
MagnetAcgripReverseProxy?: string;
|
||||
// 评论功能开关
|
||||
EnableComments: boolean;
|
||||
// 自定义去广告代码
|
||||
|
||||
@@ -10,6 +10,31 @@ export interface ChangelogEntry {
|
||||
}
|
||||
|
||||
export const changelog: ChangelogEntry[] = [
|
||||
{
|
||||
version: "216.0.0",
|
||||
date: "2026-03-30",
|
||||
added: [
|
||||
"新增视频源脚本",
|
||||
"私人影库增加本机转码功能",
|
||||
"tvbox订阅增加黄色过滤",
|
||||
"播放记录显示直链播放的链接",
|
||||
"磁链增加代理配置",
|
||||
"弹幕选集面板增强",
|
||||
"电视直播聚合同名节目",
|
||||
"douban页面大屏自动预加载第二页",
|
||||
"增加docker lite镜像",
|
||||
"详情面板增加外部跳转"
|
||||
],
|
||||
changed: [
|
||||
"GlobalError自动消失",
|
||||
"站点配置子服务配置项折叠"
|
||||
],
|
||||
fixed: [
|
||||
"修复手动选择弹幕因缓存问题无法变更弹幕集数",
|
||||
"修复当前集拉回开头显示恢复进度按钮",
|
||||
"修复视频源权重的一些问题"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "215.0.0",
|
||||
date: "2026-03-20",
|
||||
|
||||
@@ -256,6 +256,11 @@ async function getInitConfig(configFile: string, subConfig: {
|
||||
PansouUsername: '',
|
||||
PansouPassword: '',
|
||||
PansouKeywordBlocklist: '',
|
||||
// 磁链配置
|
||||
MagnetProxy: '',
|
||||
MagnetMikanReverseProxy: '',
|
||||
MagnetDmhyReverseProxy: '',
|
||||
MagnetAcgripReverseProxy: '',
|
||||
// 评论功能开关
|
||||
EnableComments: false,
|
||||
},
|
||||
@@ -432,6 +437,10 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
PansouUsername: '',
|
||||
PansouPassword: '',
|
||||
PansouKeywordBlocklist: '',
|
||||
MagnetProxy: '',
|
||||
MagnetMikanReverseProxy: '',
|
||||
MagnetDmhyReverseProxy: '',
|
||||
MagnetAcgripReverseProxy: '',
|
||||
EnableComments: false,
|
||||
};
|
||||
}
|
||||
@@ -449,6 +458,18 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (adminConfig.SiteConfig.PansouKeywordBlocklist === undefined) {
|
||||
adminConfig.SiteConfig.PansouKeywordBlocklist = '';
|
||||
}
|
||||
if (adminConfig.SiteConfig.MagnetProxy === undefined) {
|
||||
adminConfig.SiteConfig.MagnetProxy = '';
|
||||
}
|
||||
if (adminConfig.SiteConfig.MagnetMikanReverseProxy === undefined) {
|
||||
adminConfig.SiteConfig.MagnetMikanReverseProxy = '';
|
||||
}
|
||||
if (adminConfig.SiteConfig.MagnetDmhyReverseProxy === undefined) {
|
||||
adminConfig.SiteConfig.MagnetDmhyReverseProxy = '';
|
||||
}
|
||||
if (adminConfig.SiteConfig.MagnetAcgripReverseProxy === undefined) {
|
||||
adminConfig.SiteConfig.MagnetAcgripReverseProxy = '';
|
||||
}
|
||||
if (!adminConfig.UserConfig) {
|
||||
adminConfig.UserConfig = { Users: [] };
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ export async function getDanmakuById(
|
||||
episodeId: number,
|
||||
title?: string,
|
||||
episodeIndex?: number,
|
||||
options?: {
|
||||
bypassCache?: boolean;
|
||||
},
|
||||
metadata?: {
|
||||
animeId?: number;
|
||||
animeTitle?: string;
|
||||
@@ -152,13 +155,15 @@ export async function getDanmakuById(
|
||||
): Promise<DanmakuComment[]> {
|
||||
try {
|
||||
// 1. 如果提供了 title 和 episodeIndex,先尝试从缓存读取
|
||||
if (title && episodeIndex !== undefined) {
|
||||
if (title && episodeIndex !== undefined && !options?.bypassCache) {
|
||||
const cachedData = await getDanmakuFromCache(title, episodeIndex);
|
||||
if (cachedData) {
|
||||
console.log(`[弹幕缓存] 使用缓存: title=${title}, episodeIndex=${episodeIndex}, 数量=${cachedData.comments.length}`);
|
||||
return cachedData.comments;
|
||||
}
|
||||
console.log(`[弹幕缓存] 缓存未命中,从 API 获取: title=${title}, episodeIndex=${episodeIndex}`);
|
||||
} else if (title && episodeIndex !== undefined && options?.bypassCache) {
|
||||
console.log(`[弹幕缓存] 手动选择,跳过缓存读取: title=${title}, episodeIndex=${episodeIndex}, episodeId=${episodeId}`);
|
||||
} else {
|
||||
console.log(`[弹幕缓存] 未提供 title/episodeIndex,跳过缓存: episodeId=${episodeId}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as cheerio from 'cheerio/slim';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import nodeFetch from 'node-fetch';
|
||||
|
||||
function isCloudflareEnvironment(): boolean {
|
||||
return process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare';
|
||||
}
|
||||
|
||||
export function getMagnetBaseUrl(defaultBaseUrl: string, reverseProxyBaseUrl?: string): string {
|
||||
return (reverseProxyBaseUrl || defaultBaseUrl).replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export async function universalMagnetFetch(
|
||||
url: string,
|
||||
proxy?: string,
|
||||
init?: RequestInit
|
||||
): Promise<Response> {
|
||||
if (isCloudflareEnvironment()) {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
return response as unknown as Response;
|
||||
}
|
||||
|
||||
const fetchOptions: any = proxy
|
||||
? {
|
||||
...init,
|
||||
agent: new HttpsProxyAgent(proxy, {
|
||||
timeout: 30000,
|
||||
keepAlive: false,
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
: {
|
||||
...init,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
};
|
||||
|
||||
return nodeFetch(url, fetchOptions) as unknown as Response;
|
||||
}
|
||||
@@ -0,0 +1,1041 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import * as cheerio from 'cheerio/slim';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
const SOURCE_SCRIPT_REGISTRY_KEY = 'source-script:registry';
|
||||
const DEFAULT_TIMEOUT_MS = 20000;
|
||||
|
||||
// 绕过 webpack 静态分析,获取真正的 Node.js require
|
||||
// eslint-disable-next-line no-eval
|
||||
const _nodeRequire = eval('require') as NodeRequire;
|
||||
|
||||
// ---- 内存缓存 ----
|
||||
let _registryCache: { data: SourceScriptRegistry; ts: number } | null = null;
|
||||
const REGISTRY_CACHE_TTL_MS = 4 * 60 * 60 * 1000;
|
||||
|
||||
const _compiledCache = new Map<string, any>();
|
||||
const MAX_COMPILED_CACHE_SIZE = 50;
|
||||
|
||||
export interface SourceScriptRecord {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
version: string;
|
||||
code: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface SourceScriptImportItem {
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
code: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SourceScriptRegistry {
|
||||
items: SourceScriptRecord[];
|
||||
}
|
||||
|
||||
export interface SourceScriptTestResult {
|
||||
ok: boolean;
|
||||
durationMs: number;
|
||||
logs: string[];
|
||||
meta?: Record<string, any>;
|
||||
result?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type SourceScriptHook =
|
||||
| 'getSources'
|
||||
| 'search'
|
||||
| 'recommend'
|
||||
| 'detail'
|
||||
| 'resolvePlayUrl';
|
||||
|
||||
export interface PublicSourceScriptSummary {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
version: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface ScriptSourceDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const SCRIPT_SOURCE_PREFIX = 'script:';
|
||||
|
||||
const DEFAULT_SCRIPT_TEMPLATE = `return {
|
||||
meta: {
|
||||
name: '示例脚本',
|
||||
author: 'admin'
|
||||
},
|
||||
|
||||
async getSources(ctx) {
|
||||
return [
|
||||
{ id: 'main', name: '主站' },
|
||||
{ id: 'backup', name: '备用站' }
|
||||
];
|
||||
},
|
||||
|
||||
async search(ctx, { keyword, page, sourceId }) {
|
||||
ctx.log.info('search', keyword, page, sourceId);
|
||||
return {
|
||||
sourceId,
|
||||
list: [],
|
||||
page,
|
||||
pageCount: 1,
|
||||
total: 0
|
||||
};
|
||||
},
|
||||
|
||||
async recommend(ctx, { page }) {
|
||||
ctx.log.info('recommend', page);
|
||||
return {
|
||||
list: [],
|
||||
page: page || 1,
|
||||
pageCount: 1,
|
||||
total: 0
|
||||
};
|
||||
},
|
||||
|
||||
async detail(ctx, { id, sourceId }) {
|
||||
ctx.log.info('detail', id, sourceId);
|
||||
return {
|
||||
id,
|
||||
sourceId,
|
||||
title: '',
|
||||
poster: '',
|
||||
year: '',
|
||||
desc: '',
|
||||
playbacks: [
|
||||
{
|
||||
sourceId: sourceId || 'main',
|
||||
sourceName: '主站',
|
||||
episodes: [],
|
||||
episodes_titles: []
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
async resolvePlayUrl(ctx, { playUrl, sourceId, episodeIndex }) {
|
||||
ctx.log.info('resolvePlayUrl', sourceId, episodeIndex, playUrl);
|
||||
return {
|
||||
url: playUrl,
|
||||
type: 'auto',
|
||||
headers: {}
|
||||
};
|
||||
}
|
||||
};`;
|
||||
|
||||
function getNowVersion() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function buildEmptyRegistry(): SourceScriptRegistry {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
async function loadRegistry(): Promise<SourceScriptRegistry> {
|
||||
if (_registryCache && Date.now() - _registryCache.ts < REGISTRY_CACHE_TTL_MS) {
|
||||
return _registryCache.data;
|
||||
}
|
||||
|
||||
const raw = await db.getGlobalValue(SOURCE_SCRIPT_REGISTRY_KEY);
|
||||
if (!raw) {
|
||||
const empty = buildEmptyRegistry();
|
||||
_registryCache = { data: empty, ts: Date.now() };
|
||||
return empty;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as SourceScriptRegistry;
|
||||
if (!parsed || !Array.isArray(parsed.items)) {
|
||||
const empty = buildEmptyRegistry();
|
||||
_registryCache = { data: empty, ts: Date.now() };
|
||||
return empty;
|
||||
}
|
||||
_registryCache = { data: parsed, ts: Date.now() };
|
||||
return parsed;
|
||||
} catch {
|
||||
const empty = buildEmptyRegistry();
|
||||
_registryCache = { data: empty, ts: Date.now() };
|
||||
return empty;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRegistry(registry: SourceScriptRegistry) {
|
||||
_registryCache = null;
|
||||
_compiledCache.clear();
|
||||
await db.setGlobalValue(
|
||||
SOURCE_SCRIPT_REGISTRY_KEY,
|
||||
JSON.stringify(registry)
|
||||
);
|
||||
}
|
||||
|
||||
function assertScriptKey(key: string) {
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
|
||||
throw new Error('脚本 Key 仅支持字母、数字、下划线和中划线');
|
||||
}
|
||||
}
|
||||
|
||||
function createLogCollector() {
|
||||
const logs: string[] = [];
|
||||
|
||||
const push = (level: string, args: any[]) => {
|
||||
const rendered = args
|
||||
.map((arg) => {
|
||||
if (typeof arg === 'string') return arg;
|
||||
try {
|
||||
return JSON.stringify(arg);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
logs.push(`[${level}] ${rendered}`);
|
||||
if (logs.length > 50) {
|
||||
logs.shift();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
logs,
|
||||
log: {
|
||||
info: (...args: any[]) => push('info', args),
|
||||
warn: (...args: any[]) => push('warn', args),
|
||||
error: (...args: any[]) => push('error', args),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`执行超时(${timeoutMs}ms)`)), timeoutMs);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
function createCacheHelpers(scriptId: string) {
|
||||
const prefix = `source-script-cache:${scriptId}:`;
|
||||
|
||||
return {
|
||||
async get(key: string) {
|
||||
const raw = await db.getGlobalValue(`${prefix}${key}`);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { value: string; expiresAt: number };
|
||||
if (parsed.expiresAt && parsed.expiresAt < Date.now()) {
|
||||
await db.deleteGlobalValue(`${prefix}${key}`);
|
||||
return null;
|
||||
}
|
||||
return parsed.value ?? null;
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
},
|
||||
async set(key: string, value: string, ttlSec = 300) {
|
||||
await db.setGlobalValue(
|
||||
`${prefix}${key}`,
|
||||
JSON.stringify({
|
||||
value,
|
||||
expiresAt: Date.now() + ttlSec * 1000,
|
||||
})
|
||||
);
|
||||
},
|
||||
async del(key: string) {
|
||||
await db.deleteGlobalValue(`${prefix}${key}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createUtils() {
|
||||
return {
|
||||
buildUrl(base: string, query?: Record<string, string | number | boolean>) {
|
||||
const url = new URL(base);
|
||||
Object.entries(query || {}).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, String(value));
|
||||
});
|
||||
return url.toString();
|
||||
},
|
||||
joinUrl(base: string, path: string) {
|
||||
return new URL(path, base).toString();
|
||||
},
|
||||
randomUA() {
|
||||
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36';
|
||||
},
|
||||
sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
},
|
||||
base64Encode(value: string) {
|
||||
return Buffer.from(value, 'utf8').toString('base64');
|
||||
},
|
||||
base64Decode(value: string) {
|
||||
return Buffer.from(value, 'base64').toString('utf8');
|
||||
},
|
||||
now() {
|
||||
return Date.now();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createScriptFactory(code: string) {
|
||||
return new Function(
|
||||
'require',
|
||||
`"use strict";\n${code}`
|
||||
) as (req: NodeRequire) => any;
|
||||
}
|
||||
|
||||
async function createScriptContext(script: SourceScriptRecord, configValues?: Record<string, string>) {
|
||||
const { logs, log } = createLogCollector();
|
||||
const cache = createCacheHelpers(script.id);
|
||||
|
||||
const fetcher = async (input: {
|
||||
url: string;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
query?: Record<string, string | number | boolean>;
|
||||
body?: string;
|
||||
json?: unknown;
|
||||
timeoutMs?: number;
|
||||
}) => {
|
||||
const url = new URL(input.url);
|
||||
Object.entries(input.query || {}).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, String(value));
|
||||
});
|
||||
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
throw new Error(`不支持的协议: ${url.protocol}`);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
input.timeoutMs || DEFAULT_TIMEOUT_MS
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: input.method || 'GET',
|
||||
headers: {
|
||||
...(input.json ? { 'Content-Type': 'application/json' } : {}),
|
||||
...(input.headers || {}),
|
||||
},
|
||||
body: input.json !== undefined ? JSON.stringify(input.json) : input.body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
url: response.url,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
text: () => response.text(),
|
||||
json: <T = any>() => response.json() as Promise<T>,
|
||||
arrayBuffer: () => response.arrayBuffer(),
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
ctx: Object.freeze({
|
||||
fetch: fetcher,
|
||||
request: {
|
||||
get: (url: string, options?: Omit<Parameters<typeof fetcher>[0], 'url' | 'method'>) =>
|
||||
fetcher({ url, method: 'GET', ...(options || {}) }),
|
||||
post: (url: string, options?: Omit<Parameters<typeof fetcher>[0], 'url' | 'method'>) =>
|
||||
fetcher({ url, method: 'POST', ...(options || {}) }),
|
||||
async getHtml(url: string, options?: Omit<Parameters<typeof fetcher>[0], 'url' | 'method'>) {
|
||||
const response = await fetcher({ url, method: 'GET', ...(options || {}) });
|
||||
const text = await response.text();
|
||||
return cheerio.load(text);
|
||||
},
|
||||
async getJson<T = any>(url: string, options?: Omit<Parameters<typeof fetcher>[0], 'url' | 'method'>) {
|
||||
const response = await fetcher({ url, method: 'GET', ...(options || {}) });
|
||||
return response.json<T>();
|
||||
},
|
||||
},
|
||||
html: {
|
||||
load: (html: string) => cheerio.load(html),
|
||||
},
|
||||
json: {
|
||||
parse<T = any>(text: string, fallback?: T) {
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
return fallback as T;
|
||||
}
|
||||
},
|
||||
stringify(value: unknown) {
|
||||
return JSON.stringify(value);
|
||||
},
|
||||
},
|
||||
utils: createUtils(),
|
||||
cache,
|
||||
log,
|
||||
config: {
|
||||
get: (key: string) => configValues?.[key],
|
||||
require: (key: string) => {
|
||||
const value = configValues?.[key];
|
||||
if (!value) {
|
||||
throw new Error(`缺少脚本配置: ${key}`);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
all: () => ({ ...(configValues || {}) }),
|
||||
},
|
||||
runtime: {
|
||||
scriptId: script.id,
|
||||
sourceKey: script.key,
|
||||
sourceName: script.name,
|
||||
version: script.version,
|
||||
},
|
||||
}),
|
||||
logs,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeScript(script: any) {
|
||||
if (!script || typeof script !== 'object') {
|
||||
throw new Error('脚本必须返回对象');
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
async function getEnabledSourceScriptByKey(key: string) {
|
||||
const registry = await loadRegistry();
|
||||
const item = registry.items.find((record) => record.key === key);
|
||||
if (!item) {
|
||||
throw new Error('脚本不存在');
|
||||
}
|
||||
if (!item.enabled) {
|
||||
throw new Error('脚本已停用');
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function getOrCompileScript(script: SourceScriptRecord) {
|
||||
const cacheKey = `${script.id}:${script.version}`;
|
||||
const cached = _compiledCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const factory = createScriptFactory(script.code);
|
||||
const compiled = normalizeScript(factory(_nodeRequire));
|
||||
|
||||
if (_compiledCache.size >= MAX_COMPILED_CACHE_SIZE) {
|
||||
const firstKey = _compiledCache.keys().next().value;
|
||||
if (firstKey) _compiledCache.delete(firstKey);
|
||||
}
|
||||
_compiledCache.set(cacheKey, compiled);
|
||||
return compiled;
|
||||
}
|
||||
|
||||
async function compileSourceScript(
|
||||
script: SourceScriptRecord,
|
||||
configValues?: Record<string, string>
|
||||
) {
|
||||
const compiled = getOrCompileScript(script);
|
||||
const context = await createScriptContext(script, configValues);
|
||||
return {
|
||||
compiled,
|
||||
...context,
|
||||
};
|
||||
}
|
||||
|
||||
export async function executeSavedSourceScript(input: {
|
||||
key: string;
|
||||
hook: SourceScriptHook;
|
||||
payload?: Record<string, any>;
|
||||
configValues?: Record<string, string>;
|
||||
}): Promise<SourceScriptTestResult> {
|
||||
const startedAt = Date.now();
|
||||
const script = await getEnabledSourceScriptByKey(input.key);
|
||||
const { compiled, ctx, logs } = await compileSourceScript(
|
||||
script,
|
||||
input.configValues
|
||||
);
|
||||
|
||||
const hook = compiled[input.hook];
|
||||
if (typeof hook !== 'function') {
|
||||
throw new Error(`脚本未实现 ${input.hook} hook`);
|
||||
}
|
||||
|
||||
const result = await withTimeout(
|
||||
Promise.resolve(hook(ctx, input.payload || {})),
|
||||
DEFAULT_TIMEOUT_MS
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
durationMs: Date.now() - startedAt,
|
||||
logs,
|
||||
meta: compiled.meta,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listEnabledSourceScripts(): Promise<PublicSourceScriptSummary[]> {
|
||||
const registry = await loadRegistry();
|
||||
return registry.items
|
||||
.filter((item) => item.enabled)
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
version: item.version,
|
||||
updatedAt: item.updatedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function listSourceScripts() {
|
||||
const registry = await loadRegistry();
|
||||
return registry.items.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
}
|
||||
|
||||
export async function getSourceScript(id: string) {
|
||||
const registry = await loadRegistry();
|
||||
return registry.items.find((item) => item.id === id) || null;
|
||||
}
|
||||
|
||||
export async function saveSourceScript(input: {
|
||||
id?: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
code: string;
|
||||
enabled?: boolean;
|
||||
}) {
|
||||
assertScriptKey(input.key);
|
||||
|
||||
const registry = await loadRegistry();
|
||||
const now = Date.now();
|
||||
const existing = input.id
|
||||
? registry.items.find((item) => item.id === input.id)
|
||||
: undefined;
|
||||
|
||||
if (!existing && registry.items.some((item) => item.key === input.key)) {
|
||||
throw new Error('脚本 Key 已存在');
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
existing.key = input.key;
|
||||
existing.name = input.name;
|
||||
existing.description = input.description || '';
|
||||
existing.code = input.code;
|
||||
existing.enabled = input.enabled ?? existing.enabled;
|
||||
existing.updatedAt = now;
|
||||
existing.version = getNowVersion();
|
||||
await saveRegistry(registry);
|
||||
return existing;
|
||||
}
|
||||
|
||||
const created: SourceScriptRecord = {
|
||||
id: nanoid(),
|
||||
key: input.key,
|
||||
name: input.name,
|
||||
description: input.description || '',
|
||||
code: input.code,
|
||||
enabled: input.enabled ?? true,
|
||||
version: getNowVersion(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
registry.items.unshift(created);
|
||||
await saveRegistry(registry);
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function importSourceScripts(items: SourceScriptImportItem[]) {
|
||||
const registry = await loadRegistry();
|
||||
const now = Date.now();
|
||||
const imported: SourceScriptRecord[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (!item?.key || !item?.name || !item?.code) {
|
||||
throw new Error('导入脚本缺少必要字段: key/name/code');
|
||||
}
|
||||
|
||||
assertScriptKey(item.key);
|
||||
|
||||
const existing = registry.items.find((record) => record.key === item.key);
|
||||
|
||||
if (existing) {
|
||||
existing.name = item.name;
|
||||
existing.description = item.description || '';
|
||||
existing.code = item.code;
|
||||
existing.enabled = item.enabled ?? existing.enabled;
|
||||
existing.updatedAt = now;
|
||||
existing.version = getNowVersion();
|
||||
imported.push(existing);
|
||||
continue;
|
||||
}
|
||||
|
||||
const created: SourceScriptRecord = {
|
||||
id: nanoid(),
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
code: item.code,
|
||||
enabled: item.enabled ?? true,
|
||||
version: getNowVersion(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
registry.items.unshift(created);
|
||||
imported.push(created);
|
||||
}
|
||||
|
||||
await saveRegistry(registry);
|
||||
return imported;
|
||||
}
|
||||
|
||||
export async function deleteSourceScript(id: string) {
|
||||
const registry = await loadRegistry();
|
||||
const nextItems = registry.items.filter((item) => item.id !== id);
|
||||
if (nextItems.length === registry.items.length) {
|
||||
throw new Error('脚本不存在');
|
||||
}
|
||||
registry.items = nextItems;
|
||||
await saveRegistry(registry);
|
||||
}
|
||||
|
||||
export async function toggleSourceScriptEnabled(id: string) {
|
||||
const registry = await loadRegistry();
|
||||
const target = registry.items.find((item) => item.id === id);
|
||||
if (!target) {
|
||||
throw new Error('脚本不存在');
|
||||
}
|
||||
target.enabled = !target.enabled;
|
||||
target.updatedAt = Date.now();
|
||||
await saveRegistry(registry);
|
||||
return target;
|
||||
}
|
||||
|
||||
export async function testSourceScript(input: {
|
||||
code: string;
|
||||
hook: SourceScriptHook;
|
||||
payload: Record<string, any>;
|
||||
name?: string;
|
||||
key?: string;
|
||||
configValues?: Record<string, string>;
|
||||
}): Promise<SourceScriptTestResult> {
|
||||
const startedAt = Date.now();
|
||||
let collectedLogs: string[] = [];
|
||||
try {
|
||||
const tempScript: SourceScriptRecord = {
|
||||
id: 'test-script',
|
||||
key: input.key || 'test-script',
|
||||
name: input.name || '测试脚本',
|
||||
description: '',
|
||||
enabled: true,
|
||||
version: 'test',
|
||||
code: input.code,
|
||||
createdAt: startedAt,
|
||||
updatedAt: startedAt,
|
||||
};
|
||||
|
||||
const factory = createScriptFactory(input.code);
|
||||
const compiled = normalizeScript(factory(_nodeRequire));
|
||||
const hook = compiled[input.hook];
|
||||
if (typeof hook !== 'function') {
|
||||
throw new Error(`脚本未实现 ${input.hook} hook`);
|
||||
}
|
||||
|
||||
const { ctx, logs } = await createScriptContext(tempScript, input.configValues);
|
||||
collectedLogs = logs;
|
||||
const result = await withTimeout(
|
||||
Promise.resolve(hook(ctx, input.payload)),
|
||||
DEFAULT_TIMEOUT_MS
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
durationMs: Date.now() - startedAt,
|
||||
logs,
|
||||
meta: compiled.meta,
|
||||
result,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
durationMs: Date.now() - startedAt,
|
||||
logs: collectedLogs,
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultSourceScriptTemplate() {
|
||||
return DEFAULT_SCRIPT_TEMPLATE;
|
||||
}
|
||||
|
||||
export function buildScriptSourceValue(scriptKey: string, sourceId?: string) {
|
||||
return `${SCRIPT_SOURCE_PREFIX}${scriptKey}:${sourceId || 'default'}`;
|
||||
}
|
||||
|
||||
export function parseScriptSourceValue(source: string) {
|
||||
if (!source.startsWith(SCRIPT_SOURCE_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rest = source.slice(SCRIPT_SOURCE_PREFIX.length);
|
||||
const separatorIndex = rest.indexOf(':');
|
||||
if (separatorIndex === -1) {
|
||||
return {
|
||||
scriptKey: rest,
|
||||
sourceId: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scriptKey: rest.slice(0, separatorIndex),
|
||||
sourceId: rest.slice(separatorIndex + 1) || 'default',
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeScriptSources(result: any): ScriptSourceDescriptor[] {
|
||||
if (!Array.isArray(result)) {
|
||||
return [{ id: 'default', name: '默认源' }];
|
||||
}
|
||||
|
||||
return result
|
||||
.filter((item) => item && item.id)
|
||||
.map((item) => ({
|
||||
id: String(item.id),
|
||||
name: String(item.name || item.id),
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeScriptSearchResults(input: {
|
||||
scriptKey: string;
|
||||
scriptName: string;
|
||||
sourceId: string;
|
||||
sourceName: string;
|
||||
result: any;
|
||||
}) {
|
||||
const list = Array.isArray(input.result?.list) ? input.result.list : [];
|
||||
return list.map((item: any) => {
|
||||
const titles = Array.isArray(item.episodes_titles) ? item.episodes_titles : [];
|
||||
const episodes = Array.isArray(item.episodes)
|
||||
? item.episodes.map((episode: any, index: number) => {
|
||||
const playUrl =
|
||||
typeof episode === 'string'
|
||||
? episode
|
||||
: String(episode?.playUrl || episode?.url || '');
|
||||
const needResolve =
|
||||
typeof episode === 'object' && episode
|
||||
? episode.needResolve !== false
|
||||
: true;
|
||||
|
||||
return needResolve
|
||||
? buildScriptPlayUrl({
|
||||
scriptKey: input.scriptKey,
|
||||
sourceId: input.sourceId,
|
||||
episodeIndex: index,
|
||||
playUrl,
|
||||
})
|
||||
: playUrl;
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
id: String(item.id),
|
||||
title: String(item.title || ''),
|
||||
poster: item.poster || '',
|
||||
episodes,
|
||||
episodes_titles: titles,
|
||||
source: buildScriptSourceValue(input.scriptKey, input.sourceId),
|
||||
source_name: `${input.scriptName} / ${input.sourceName}`,
|
||||
year: item.year || '',
|
||||
desc: item.desc || '',
|
||||
type_name: item.type_name || '',
|
||||
douban_id: item.douban_id || 0,
|
||||
vod_remarks: item.vod_remarks,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeScriptRecommendResults(input: {
|
||||
scriptKey: string;
|
||||
scriptName: string;
|
||||
result: any;
|
||||
sources?: ScriptSourceDescriptor[];
|
||||
defaultSourceId?: string;
|
||||
}) {
|
||||
const list = Array.isArray(input.result?.list) ? input.result.list : [];
|
||||
const sourceMap = new Map(
|
||||
(input.sources || []).map((item) => [String(item.id), String(item.name)])
|
||||
);
|
||||
const fallbackSourceId = input.defaultSourceId || 'default';
|
||||
|
||||
return list.map((item: any) => {
|
||||
const sourceId = String(
|
||||
item?.sourceId || item?.source_id || item?.source || fallbackSourceId
|
||||
);
|
||||
const sourceName = String(
|
||||
item?.sourceName ||
|
||||
item?.source_name ||
|
||||
sourceMap.get(sourceId) ||
|
||||
sourceId
|
||||
);
|
||||
|
||||
return {
|
||||
id: String(item?.id || ''),
|
||||
title: String(item?.title || ''),
|
||||
poster: item?.poster || '',
|
||||
episodes: Array.isArray(item?.episodes) ? item.episodes : [],
|
||||
episodes_titles: Array.isArray(item?.episodes_titles)
|
||||
? item.episodes_titles
|
||||
: [],
|
||||
source: buildScriptSourceValue(input.scriptKey, sourceId),
|
||||
source_name: `${input.scriptName} / ${sourceName}`,
|
||||
year: item?.year || '',
|
||||
desc: item?.desc || '',
|
||||
type_name: item?.type_name || '',
|
||||
douban_id: item?.douban_id || 0,
|
||||
vod_remarks: item?.vod_remarks,
|
||||
vod_total: item?.vod_total,
|
||||
tmdb_id: item?.tmdb_id,
|
||||
rating: item?.rating,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeScriptDetailResult(input: {
|
||||
source: string;
|
||||
scriptKey: string;
|
||||
scriptName: string;
|
||||
sourceId: string;
|
||||
sourceName: string;
|
||||
detailId: string;
|
||||
result: any;
|
||||
}) {
|
||||
const playbacks = Array.isArray(input.result?.playbacks)
|
||||
? input.result.playbacks
|
||||
: [
|
||||
{
|
||||
sourceId: input.sourceId,
|
||||
sourceName: input.sourceName,
|
||||
episodes: input.result?.episodes || [],
|
||||
episodes_titles: input.result?.episodes_titles || [],
|
||||
},
|
||||
];
|
||||
|
||||
const flattenedEpisodes: string[] = [];
|
||||
const flattenedTitles: string[] = [];
|
||||
|
||||
playbacks.forEach((playback: any) => {
|
||||
const playbackSourceName = String(playback.sourceName || input.sourceName);
|
||||
const titles = Array.isArray(playback.episodes_titles)
|
||||
? playback.episodes_titles
|
||||
: [];
|
||||
const episodes = Array.isArray(playback.episodes) ? playback.episodes : [];
|
||||
|
||||
episodes.forEach((episode: any, index: number) => {
|
||||
const rawPlayUrl =
|
||||
typeof episode === 'string'
|
||||
? episode
|
||||
: String(episode?.playUrl || episode?.url || '');
|
||||
const episodeTitle =
|
||||
typeof episode === 'object' && episode?.title
|
||||
? String(episode.title)
|
||||
: String(titles[index] || `第${index + 1}集`);
|
||||
|
||||
const playbackSourceId = String(playback.sourceId || input.sourceId);
|
||||
const needResolve =
|
||||
typeof episode === 'object' && episode
|
||||
? episode.needResolve !== false
|
||||
: true;
|
||||
const playUrl = needResolve
|
||||
? buildScriptPlayUrl({
|
||||
scriptKey: input.scriptKey,
|
||||
sourceId: playbackSourceId,
|
||||
episodeIndex: index,
|
||||
playUrl: rawPlayUrl,
|
||||
})
|
||||
: rawPlayUrl;
|
||||
|
||||
flattenedEpisodes.push(playUrl);
|
||||
flattenedTitles.push(`${playbackSourceName} / ${episodeTitle}`);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
id: input.detailId,
|
||||
title: String(input.result?.title || ''),
|
||||
poster: input.result?.poster || '',
|
||||
episodes: flattenedEpisodes,
|
||||
episodes_titles: flattenedTitles,
|
||||
source: input.source,
|
||||
source_name: `${input.scriptName} / ${input.sourceName}`,
|
||||
class: input.result?.class,
|
||||
year: input.result?.year || '',
|
||||
desc: input.result?.desc || '',
|
||||
type_name: input.result?.type_name || '',
|
||||
douban_id: input.result?.douban_id || 0,
|
||||
vod_remarks: input.result?.vod_remarks,
|
||||
vod_total: input.result?.vod_total,
|
||||
proxyMode: false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveScriptDetailPlaybacks(input: {
|
||||
scriptKey: string;
|
||||
sourceId: string;
|
||||
result: any;
|
||||
}) {
|
||||
const playbacks = Array.isArray(input.result?.playbacks)
|
||||
? input.result.playbacks
|
||||
: [
|
||||
{
|
||||
sourceId: input.sourceId,
|
||||
sourceName: input.sourceId,
|
||||
episodes: input.result?.episodes || [],
|
||||
episodes_titles: input.result?.episodes_titles || [],
|
||||
},
|
||||
];
|
||||
|
||||
// 预检查:编译一次脚本,判断是否实现了 resolvePlayUrl
|
||||
const script = await getEnabledSourceScriptByKey(input.scriptKey);
|
||||
const compiled = getOrCompileScript(script);
|
||||
|
||||
if (typeof compiled.resolvePlayUrl !== 'function') {
|
||||
return input.result;
|
||||
}
|
||||
|
||||
// 已实现 resolvePlayUrl,创建一个 context 复用
|
||||
const { ctx } = await createScriptContext(script);
|
||||
|
||||
const resolvedPlaybacks = await Promise.all(
|
||||
playbacks.map(async (playback: any) => {
|
||||
const playbackSourceId = String(playback.sourceId || input.sourceId);
|
||||
const episodes = Array.isArray(playback.episodes) ? playback.episodes : [];
|
||||
|
||||
const resolvedEpisodes = await Promise.all(
|
||||
episodes.map(async (episode: any, index: number) => {
|
||||
const playUrl =
|
||||
typeof episode === 'string'
|
||||
? episode
|
||||
: String(episode?.playUrl || episode?.url || '');
|
||||
|
||||
try {
|
||||
const result = await withTimeout(
|
||||
Promise.resolve(
|
||||
compiled.resolvePlayUrl(ctx, {
|
||||
playUrl,
|
||||
sourceId: playbackSourceId,
|
||||
episodeIndex: index,
|
||||
})
|
||||
),
|
||||
DEFAULT_TIMEOUT_MS
|
||||
);
|
||||
return result?.url || playUrl;
|
||||
} catch {
|
||||
return playUrl;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...playback,
|
||||
episodes: resolvedEpisodes,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...input.result,
|
||||
playbacks: resolvedPlaybacks,
|
||||
};
|
||||
}
|
||||
|
||||
function encodeBase64Url(value: string) {
|
||||
return Buffer.from(value, 'utf8')
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function decodeBase64Url(value: string) {
|
||||
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padding = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4));
|
||||
return Buffer.from(`${normalized}${padding}`, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
export function buildScriptPlayUrl(input: {
|
||||
scriptKey: string;
|
||||
sourceId: string;
|
||||
episodeIndex: number;
|
||||
playUrl: string;
|
||||
}) {
|
||||
const searchParams = new URLSearchParams({
|
||||
key: input.scriptKey,
|
||||
sourceId: input.sourceId,
|
||||
episodeIndex: String(input.episodeIndex),
|
||||
playUrl: encodeBase64Url(input.playUrl),
|
||||
});
|
||||
return `/api/source-script/play?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
export function parseScriptPlayUrlValue(value: string) {
|
||||
return decodeBase64Url(value);
|
||||
}
|
||||
|
||||
export async function resolveSavedScriptPlayUrl(input: {
|
||||
key: string;
|
||||
sourceId: string;
|
||||
episodeIndex: number;
|
||||
playUrl: string;
|
||||
configValues?: Record<string, string>;
|
||||
}) {
|
||||
const script = await getEnabledSourceScriptByKey(input.key);
|
||||
const { compiled, ctx } = await compileSourceScript(script, input.configValues);
|
||||
|
||||
if (typeof compiled.resolvePlayUrl !== 'function') {
|
||||
return {
|
||||
url: input.playUrl,
|
||||
type: 'auto',
|
||||
headers: {},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await withTimeout(
|
||||
Promise.resolve(
|
||||
compiled.resolvePlayUrl(ctx, {
|
||||
playUrl: input.playUrl,
|
||||
sourceId: input.sourceId,
|
||||
episodeIndex: input.episodeIndex,
|
||||
})
|
||||
),
|
||||
DEFAULT_TIMEOUT_MS
|
||||
);
|
||||
|
||||
return {
|
||||
url: result?.url || input.playUrl,
|
||||
type: result?.type || 'auto',
|
||||
headers: result?.headers || {},
|
||||
};
|
||||
}
|
||||
@@ -177,6 +177,7 @@ export interface SearchResult {
|
||||
episodes_titles: string[];
|
||||
source: string;
|
||||
source_name: string;
|
||||
weight?: number; // 播放源权重(来自后台配置,用于排序和优选评分)
|
||||
class?: string;
|
||||
year: string;
|
||||
desc?: string;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const CURRENT_VERSION = '215.0.0';
|
||||
const CURRENT_VERSION = '216.0.0';
|
||||
|
||||
// 导出当前版本号供其他地方使用
|
||||
export { CURRENT_VERSION };
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare module 'cheerio/slim' {
|
||||
export * from 'cheerio';
|
||||
}
|
||||
Reference in New Issue
Block a user