@rexnow/libs 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/env/common.d.ts +55 -0
- package/dist/env/danmu.d.ts +36 -0
- package/dist/env/index.d.ts +7 -0
- package/dist/env/metadata.d.ts +119 -0
- package/dist/env/stream.d.ts +8 -0
- package/dist/env/subtitle.d.ts +34 -0
- package/dist/env/video.d.ts +91 -0
- package/dist/env/widget.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/env.zod/index.ts +371 -0
- package/dist/widget-adaptor.d.ts +51 -0
- package/dist/widget-adaptor.js +135 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Baran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @rexnow/libs
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@rexnow/libs)
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
> Rex Widget 开发工具库
|
|
7
|
+
|
|
8
|
+
## 🚀 简介
|
|
9
|
+
|
|
10
|
+
`@rexnow/libs` 是一个专为 Rex Widget 开发者设计的工具库,提供了完整的类型定义和测试工具,帮助开发者更高效地构建和测试 Widget 应用。
|
|
11
|
+
|
|
12
|
+
## 📦 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @rexnow/libs
|
|
16
|
+
# 或
|
|
17
|
+
yarn add @rexnow/libs
|
|
18
|
+
# 或
|
|
19
|
+
pnpm add @rexnow/libs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 🛠️ 使用方法
|
|
23
|
+
|
|
24
|
+
### 环境类型定义
|
|
25
|
+
|
|
26
|
+
#### TypeScript 项目
|
|
27
|
+
|
|
28
|
+
在你的项目中创建一个 `.d.ts` 文件,引入类型定义:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// rex-widget-env.d.ts
|
|
32
|
+
/// <reference types="@rexnow/libs/env" />
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
这样你就可以在 TypeScript 代码中获得完整的类型支持:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// 现在可以使用 Widget 相关的类型和全局变量
|
|
39
|
+
WidgetMetadata = {
|
|
40
|
+
name: "My Widget",
|
|
41
|
+
version: "1.0.0",
|
|
42
|
+
// ... 其他配置
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
async function loadDetail(url: string) {
|
|
46
|
+
const resp = await Widget.http.get('https://api.example.com/data')
|
|
47
|
+
|
|
48
|
+
const result: VideoItem = {
|
|
49
|
+
// ...
|
|
50
|
+
}
|
|
51
|
+
// ...
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### JavaScript 项目
|
|
57
|
+
|
|
58
|
+
对于 JavaScript 项目,你可以使用 JSDoc 来获得类型提示:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
/**
|
|
62
|
+
* @type {import('@rexnow/libs/env')}
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
WidgetMetadata = {
|
|
67
|
+
name: "My Widget",
|
|
68
|
+
version: "1.0.0",
|
|
69
|
+
// ... 其他配置
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function loadDetail(url) {
|
|
73
|
+
const resp = await Widget.http.get('https://api.example.com/data')
|
|
74
|
+
/**
|
|
75
|
+
* @type {import('@rexnow/libs/env').VideoItem}
|
|
76
|
+
*/
|
|
77
|
+
const result = {
|
|
78
|
+
// ...
|
|
79
|
+
}
|
|
80
|
+
// ...
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 单元测试
|
|
86
|
+
|
|
87
|
+
`@rexnow/libs` 提供了 `WidgetAdaptor` 来模拟 Widget 运行环境,方便进行单元测试。
|
|
88
|
+
|
|
89
|
+
以 [Rstest](http://rstest.rs/) 为例:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { expect, test, beforeAll } from "@rstest/core";
|
|
93
|
+
|
|
94
|
+
beforeAll(async () => {
|
|
95
|
+
const { WidgetAdaptor } = await import("@rexnow/libs/widget-adaptor");
|
|
96
|
+
rstest.stubGlobal("Widget", WidgetAdaptor);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("测试 HTTP 请求", async () => {
|
|
100
|
+
const response = await Widget.http.get("https://api.example.com/data");
|
|
101
|
+
expect(response).toBeDefined();
|
|
102
|
+
expect(response.status).toBe(200);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### TMDB
|
|
107
|
+
|
|
108
|
+
如果需要使用到 `Widget.tmdb` 下的方法,需要环境变量中配置 `TMDB_API_KEY`
|
|
109
|
+
|
|
110
|
+
```ini
|
|
111
|
+
# .env
|
|
112
|
+
TMDB_API_KEY=xxxx
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// rstest.config.ts
|
|
117
|
+
|
|
118
|
+
import { defineConfig } from "@rstest/core";
|
|
119
|
+
|
|
120
|
+
export default defineConfig({
|
|
121
|
+
testEnvironment: "node",
|
|
122
|
+
pool: {
|
|
123
|
+
type: "forks",
|
|
124
|
+
execArgv: ["--env-file=.env"], // 通过指定 env-file 加载环境变量
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 🤝 贡献
|
|
130
|
+
|
|
131
|
+
欢迎提交 Issue 和 Pull Request 来改善这个项目。
|
|
132
|
+
|
|
133
|
+
## 📄 许可证
|
|
134
|
+
|
|
135
|
+
MIT License
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface BaseParams {
|
|
2
|
+
id: string;
|
|
3
|
+
/**
|
|
4
|
+
* TMDB ID
|
|
5
|
+
*/
|
|
6
|
+
tmdbId?: string;
|
|
7
|
+
/**
|
|
8
|
+
* IMDB ID
|
|
9
|
+
*/
|
|
10
|
+
imdbId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* 类型
|
|
13
|
+
*/
|
|
14
|
+
type: 'tv' | 'movie';
|
|
15
|
+
/**
|
|
16
|
+
* 搜索关键词
|
|
17
|
+
*/
|
|
18
|
+
title: string;
|
|
19
|
+
/**
|
|
20
|
+
* 剧名
|
|
21
|
+
*/
|
|
22
|
+
seriesName?: string;
|
|
23
|
+
/**
|
|
24
|
+
* 集名
|
|
25
|
+
*/
|
|
26
|
+
episodeName?: string;
|
|
27
|
+
/**
|
|
28
|
+
* 播出日期
|
|
29
|
+
*/
|
|
30
|
+
airDate?: string;
|
|
31
|
+
/**
|
|
32
|
+
* 首播日期
|
|
33
|
+
*/
|
|
34
|
+
premiereDate?: string;
|
|
35
|
+
/**
|
|
36
|
+
* 时长
|
|
37
|
+
*/
|
|
38
|
+
runtime?: string;
|
|
39
|
+
/**
|
|
40
|
+
* 季
|
|
41
|
+
*/
|
|
42
|
+
season?: string;
|
|
43
|
+
/**
|
|
44
|
+
* 集
|
|
45
|
+
*/
|
|
46
|
+
episode?: string;
|
|
47
|
+
/**
|
|
48
|
+
* 链接
|
|
49
|
+
*/
|
|
50
|
+
link?: string;
|
|
51
|
+
/**
|
|
52
|
+
* 视频链接
|
|
53
|
+
*/
|
|
54
|
+
videoUrl?: string;
|
|
55
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface AnimeItem {
|
|
2
|
+
animeId: string | number;
|
|
3
|
+
animeTitle: string;
|
|
4
|
+
bangumiId?: string | number;
|
|
5
|
+
}
|
|
6
|
+
interface GetDetailResponseItem {
|
|
7
|
+
/**
|
|
8
|
+
* 透传给 getComments 的 commentId
|
|
9
|
+
*/
|
|
10
|
+
episodeId: string | number;
|
|
11
|
+
episodeTitle: string;
|
|
12
|
+
}
|
|
13
|
+
interface EpisodeItem {
|
|
14
|
+
commentId: string;
|
|
15
|
+
}
|
|
16
|
+
interface CommentItem {
|
|
17
|
+
cid?: number | string;
|
|
18
|
+
p: `${number},${1 | 4 | 5},${number},${string}`;
|
|
19
|
+
m: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* README 弹幕格式 2:[时间, 位置, 颜色, 额外信息, 文本]
|
|
23
|
+
*/
|
|
24
|
+
type CommentTuple = [number, string, string, string, string];
|
|
25
|
+
interface GetCommentsResponse {
|
|
26
|
+
count: number;
|
|
27
|
+
comments: Array<CommentItem | CommentTuple>;
|
|
28
|
+
}
|
|
29
|
+
interface GetDanmuWithSegmentTimeParams extends BaseParams, AnimeItem, EpisodeItem {
|
|
30
|
+
/**
|
|
31
|
+
* 分段时间
|
|
32
|
+
*/
|
|
33
|
+
segmentTime: number;
|
|
34
|
+
}
|
|
35
|
+
interface GetDanmuWithSegmentTimeResponse extends GetCommentsResponse {
|
|
36
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
declare let WidgetMetadata: WidgetMetadata;
|
|
2
|
+
interface WidgetMetadata {
|
|
3
|
+
/** Widget 唯一标识符 */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Widget 显示标题 */
|
|
6
|
+
title: string;
|
|
7
|
+
/** Widget 描述 */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** 作者 */
|
|
10
|
+
author?: string;
|
|
11
|
+
/** 网站地址 */
|
|
12
|
+
site?: string;
|
|
13
|
+
/** Widget 版本 */
|
|
14
|
+
version?: string;
|
|
15
|
+
/** 所需 Rex 版本 */
|
|
16
|
+
requiredVersion?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Widget 图标地址(官方 demo 字段名)
|
|
19
|
+
*/
|
|
20
|
+
icon?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Widget 图标地址(Rex 插件字段名,与 icon 同一含义)
|
|
23
|
+
*/
|
|
24
|
+
iconurl?: string;
|
|
25
|
+
/**
|
|
26
|
+
* 文案国际化:locale → 源文案 → 译文
|
|
27
|
+
*/
|
|
28
|
+
i18n?: Partial<Record<WidgetI18nLocale, Record<string, string>>>;
|
|
29
|
+
/**
|
|
30
|
+
* 详情数据缓存时长,单位:秒
|
|
31
|
+
* @default 60
|
|
32
|
+
*/
|
|
33
|
+
detailCacheDuration?: number;
|
|
34
|
+
/**
|
|
35
|
+
* 全局参数配置
|
|
36
|
+
*/
|
|
37
|
+
globalParams?: WidgetModuleParam[];
|
|
38
|
+
/** 功能模块列表 */
|
|
39
|
+
modules: WidgetModule[];
|
|
40
|
+
/** 搜索功能配置 */
|
|
41
|
+
search?: {
|
|
42
|
+
/** 搜索标题 */
|
|
43
|
+
title: string;
|
|
44
|
+
/** 搜索函数名 */
|
|
45
|
+
functionName: string;
|
|
46
|
+
/** 搜索参数配置 */
|
|
47
|
+
params: WidgetModuleParam[];
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
interface BaseWidgetModule {
|
|
51
|
+
/** 模块唯一标识符 */
|
|
52
|
+
id: string;
|
|
53
|
+
/** 模块类型 */
|
|
54
|
+
type?: 'danmu' | 'stream' | 'subtitle';
|
|
55
|
+
/** 模块标题 */
|
|
56
|
+
title: string;
|
|
57
|
+
/** 模块描述 */
|
|
58
|
+
description?: string;
|
|
59
|
+
/** 是否需要 WebView */
|
|
60
|
+
requiresWebView?: boolean;
|
|
61
|
+
/** 处理函数名 */
|
|
62
|
+
functionName: string;
|
|
63
|
+
/** 是否支持分段模式 */
|
|
64
|
+
sectionMode?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* 缓存时长,单位:秒
|
|
67
|
+
* @default 3600
|
|
68
|
+
*/
|
|
69
|
+
cacheDuration?: number;
|
|
70
|
+
/** 参数配置 */
|
|
71
|
+
params?: WidgetModuleParam[];
|
|
72
|
+
}
|
|
73
|
+
interface WidgetModuleVideo extends BaseWidgetModule {
|
|
74
|
+
type?: never;
|
|
75
|
+
}
|
|
76
|
+
interface WidgetModuleDanmu extends BaseWidgetModule {
|
|
77
|
+
type: 'danmu';
|
|
78
|
+
id: 'searchDanmu' | 'getDetail' | 'getComments' | 'getDanmuWithSegmentTime';
|
|
79
|
+
}
|
|
80
|
+
interface WidgetModuleStream extends BaseWidgetModule {
|
|
81
|
+
type: 'stream';
|
|
82
|
+
id: 'loadResource';
|
|
83
|
+
}
|
|
84
|
+
interface WidgetModuleSubtitle extends BaseWidgetModule {
|
|
85
|
+
type: 'subtitle';
|
|
86
|
+
id: 'loadSubtitle';
|
|
87
|
+
}
|
|
88
|
+
type WidgetI18nLocale = 'en' | 'zh-Hans' | 'zh-Hant' | 'ja' | 'ko' | 'es' | 'fr' | 'pt-BR' | 'ru' | 'ar' | (string & {});
|
|
89
|
+
type WidgetModule = WidgetModuleVideo | WidgetModuleDanmu | WidgetModuleStream | WidgetModuleSubtitle;
|
|
90
|
+
type WidgetModuleParamType = 'input' | 'constant' | 'enumeration' | 'count' | 'page' | 'offset' | 'language' | 'userId';
|
|
91
|
+
interface WidgetModuleParam {
|
|
92
|
+
/** 参数名 */
|
|
93
|
+
name: string;
|
|
94
|
+
/** 参数显示标题 */
|
|
95
|
+
title: string;
|
|
96
|
+
/** 参数类型 */
|
|
97
|
+
type: WidgetModuleParamType;
|
|
98
|
+
/** 参数描述 */
|
|
99
|
+
description?: string;
|
|
100
|
+
/** 默认值 */
|
|
101
|
+
value?: string;
|
|
102
|
+
/** 当符合该条件时才会触发该参数 */
|
|
103
|
+
belongTo?: {
|
|
104
|
+
/** 所属参数的子参数 */
|
|
105
|
+
paramName: string;
|
|
106
|
+
/** 所属参数包含的值 */
|
|
107
|
+
value: string[];
|
|
108
|
+
};
|
|
109
|
+
/** 占位符选项 */
|
|
110
|
+
placeholders?: Array<{
|
|
111
|
+
title: string;
|
|
112
|
+
value: string;
|
|
113
|
+
}>;
|
|
114
|
+
/** 枚举选项 */
|
|
115
|
+
enumOptions?: Array<{
|
|
116
|
+
title: string;
|
|
117
|
+
value: string;
|
|
118
|
+
}>;
|
|
119
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface SubtitleItem {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
lang: string;
|
|
5
|
+
count: number;
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
interface SubtitleArchiveFile {
|
|
9
|
+
/** 解压目录内的相对路径,原样返回,不要拼绝对路径 */
|
|
10
|
+
path: string;
|
|
11
|
+
/** 文件名 */
|
|
12
|
+
name: string;
|
|
13
|
+
/** 小写扩展名 */
|
|
14
|
+
extension: string;
|
|
15
|
+
/** 文件大小,可能为空 */
|
|
16
|
+
size?: number;
|
|
17
|
+
}
|
|
18
|
+
interface ResolveSubtitleArchiveParams {
|
|
19
|
+
archiveName?: string;
|
|
20
|
+
archiveUrl?: string;
|
|
21
|
+
subtitleId?: string;
|
|
22
|
+
subtitleName?: string;
|
|
23
|
+
subtitleLanguage?: string;
|
|
24
|
+
/** 全部解压文件的 JSON 字符串 */
|
|
25
|
+
files?: string;
|
|
26
|
+
/** 客户端支持的字幕文件 JSON 字符串 */
|
|
27
|
+
subtitleFiles?: string;
|
|
28
|
+
}
|
|
29
|
+
type ResolveSubtitleArchiveResult = string | string[] | {
|
|
30
|
+
path: string;
|
|
31
|
+
} | {
|
|
32
|
+
files: string[];
|
|
33
|
+
} | null;
|
|
34
|
+
declare let resolveSubtitleArchive: (params: ResolveSubtitleArchiveParams) => ResolveSubtitleArchiveResult | Promise<ResolveSubtitleArchiveResult>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
interface GenreItem {
|
|
2
|
+
/** 分类 ID,详情页打开列表时作为 genreId 回传 */
|
|
3
|
+
id: string;
|
|
4
|
+
/** 分类标题 */
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
interface PersonItem {
|
|
8
|
+
/** 人物 ID,详情页打开列表时作为 peopleId 回传 */
|
|
9
|
+
id: string;
|
|
10
|
+
/** 人物名称 */
|
|
11
|
+
title: string;
|
|
12
|
+
/** 头像地址 */
|
|
13
|
+
avatar?: string;
|
|
14
|
+
/** 角色 */
|
|
15
|
+
role?: string;
|
|
16
|
+
}
|
|
17
|
+
interface TrailerItem {
|
|
18
|
+
/** 预告片封面 */
|
|
19
|
+
coverUrl?: string;
|
|
20
|
+
/** 预告片地址 */
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
interface VideoItemChild {
|
|
24
|
+
/** 唯一标识符。对于 url 类型为 url 地址,对于 douban/imdb/tmdb 类型为对应 ID。tmdb ID 格式为 type.id,如 tv.123 */
|
|
25
|
+
id: string;
|
|
26
|
+
/** 类型标识。forward 表示可匹配的网盘资源身份,客户端会归一为 fw- 前缀 */
|
|
27
|
+
type: 'url' | 'detail' | 'douban' | 'imdb' | 'tmdb' | 'forward';
|
|
28
|
+
/** 标题 */
|
|
29
|
+
title: string;
|
|
30
|
+
/** 通用兜底封面。横图位排在 backdropPath 之后,竖图位排在最后 */
|
|
31
|
+
coverUrl?: string;
|
|
32
|
+
/** 纵向封面图片地址 */
|
|
33
|
+
posterPath?: string;
|
|
34
|
+
/** 详情页海报位专用,覆盖 posterPath */
|
|
35
|
+
detailPoster?: string;
|
|
36
|
+
/** 横向封面地址 */
|
|
37
|
+
backdropPath?: string;
|
|
38
|
+
/** 发布时间 */
|
|
39
|
+
releaseDate?: string;
|
|
40
|
+
/** 媒体类型 */
|
|
41
|
+
mediaType?: 'tv' | 'movie';
|
|
42
|
+
/** 评分 */
|
|
43
|
+
rating?: string;
|
|
44
|
+
/** 分类 */
|
|
45
|
+
genreTitle?: string;
|
|
46
|
+
/** 可点击分类,详情页会用 id 打开列表 */
|
|
47
|
+
genreItems?: GenreItem[];
|
|
48
|
+
/** 演员/人物,详情页会用 id 打开列表 */
|
|
49
|
+
peoples?: PersonItem[];
|
|
50
|
+
/** 时长(秒) */
|
|
51
|
+
duration?: number;
|
|
52
|
+
/** 时长文本格式 */
|
|
53
|
+
durationText?: string;
|
|
54
|
+
/** 预览视频地址 */
|
|
55
|
+
previewUrl?: string;
|
|
56
|
+
/** 预告片 */
|
|
57
|
+
trailers?: TrailerItem[];
|
|
58
|
+
/** 视频播放地址 */
|
|
59
|
+
videoUrl?: string;
|
|
60
|
+
/** 详情页地址 */
|
|
61
|
+
link: string;
|
|
62
|
+
/** 集数 */
|
|
63
|
+
episode?: number;
|
|
64
|
+
/** 描述 */
|
|
65
|
+
description?: string;
|
|
66
|
+
/** 播放器类型 */
|
|
67
|
+
playerType?: 'system' | 'app';
|
|
68
|
+
/** 剧照/截图列表,详情页展示 */
|
|
69
|
+
backdropPaths?: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 视频项目的元数据接口
|
|
73
|
+
*/
|
|
74
|
+
interface VideoItem extends VideoItemChild {
|
|
75
|
+
/** 子项目列表(最多一层) */
|
|
76
|
+
childItems?: VideoItemChild[];
|
|
77
|
+
/** 剧集列表 */
|
|
78
|
+
episodeItems?: VideoItem[];
|
|
79
|
+
/** 相关推荐 */
|
|
80
|
+
relatedItems?: VideoItem[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 列表模块隐式参数。从详情页分类或人物进入列表时由客户端注入。
|
|
84
|
+
*/
|
|
85
|
+
interface VideoListParams {
|
|
86
|
+
/** 分类 ID,对应 genreItems[].id */
|
|
87
|
+
genreId?: string;
|
|
88
|
+
/** 人物 ID,对应 peoples[].id */
|
|
89
|
+
peopleId?: string;
|
|
90
|
+
}
|
|
91
|
+
declare let loadDetail: (link: string) => Promise<VideoItem | VideoItem[] | null>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare const Widget: typeof import('../widget-adaptor.js').WidgetAdaptor;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// Generated by ts-to-zod
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const baseParamsSchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
/**
|
|
7
|
+
* TMDB ID
|
|
8
|
+
*/
|
|
9
|
+
tmdbId: z.string().optional(),
|
|
10
|
+
/**
|
|
11
|
+
* IMDB ID
|
|
12
|
+
*/
|
|
13
|
+
imdbId: z.string().optional(),
|
|
14
|
+
/**
|
|
15
|
+
* 类型
|
|
16
|
+
*/
|
|
17
|
+
type: z.union([z.literal("tv"), z.literal("movie")]),
|
|
18
|
+
/**
|
|
19
|
+
* 搜索关键词
|
|
20
|
+
*/
|
|
21
|
+
title: z.string(),
|
|
22
|
+
/**
|
|
23
|
+
* 剧名
|
|
24
|
+
*/
|
|
25
|
+
seriesName: z.string().optional(),
|
|
26
|
+
/**
|
|
27
|
+
* 集名
|
|
28
|
+
*/
|
|
29
|
+
episodeName: z.string().optional(),
|
|
30
|
+
/**
|
|
31
|
+
* 播出日期
|
|
32
|
+
*/
|
|
33
|
+
airDate: z.string().optional(),
|
|
34
|
+
/**
|
|
35
|
+
* 首播日期
|
|
36
|
+
*/
|
|
37
|
+
premiereDate: z.string().optional(),
|
|
38
|
+
/**
|
|
39
|
+
* 时长
|
|
40
|
+
*/
|
|
41
|
+
runtime: z.string().optional(),
|
|
42
|
+
/**
|
|
43
|
+
* 季
|
|
44
|
+
*/
|
|
45
|
+
season: z.string().optional(),
|
|
46
|
+
/**
|
|
47
|
+
* 集
|
|
48
|
+
*/
|
|
49
|
+
episode: z.string().optional(),
|
|
50
|
+
/**
|
|
51
|
+
* 链接
|
|
52
|
+
*/
|
|
53
|
+
link: z.string().optional(),
|
|
54
|
+
/**
|
|
55
|
+
* 视频链接
|
|
56
|
+
*/
|
|
57
|
+
videoUrl: z.string().optional()
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const animeItemSchema = z.object({
|
|
61
|
+
animeId: z.union([z.string(), z.number()]),
|
|
62
|
+
animeTitle: z.string(),
|
|
63
|
+
bangumiId: z.union([z.string(), z.number()]).optional()
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const getDetailResponseItemSchema = z.object({
|
|
67
|
+
/**
|
|
68
|
+
* 透传给 getComments 的 commentId
|
|
69
|
+
*/
|
|
70
|
+
episodeId: z.union([z.string(), z.number()]),
|
|
71
|
+
episodeTitle: z.string()
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const episodeItemSchema = z.object({
|
|
75
|
+
commentId: z.string()
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const commentItemSchema = z.object({
|
|
79
|
+
cid: z.union([z.number(), z.string()]).optional(),
|
|
80
|
+
p: z.string(),
|
|
81
|
+
m: z.string()
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* README 弹幕格式 2:[时间, 位置, 颜色, 额外信息, 文本]
|
|
86
|
+
*/
|
|
87
|
+
export const commentTupleSchema = z.tuple([z.number(), z.string(), z.string(), z.string(), z.string()]);
|
|
88
|
+
|
|
89
|
+
export const getCommentsResponseSchema = z.object({
|
|
90
|
+
count: z.number(),
|
|
91
|
+
comments: z.array(z.union([commentItemSchema, commentTupleSchema]))
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const getDanmuWithSegmentTimeParamsSchema = baseParamsSchema.extend(animeItemSchema.shape).extend(episodeItemSchema.shape).extend({
|
|
95
|
+
/**
|
|
96
|
+
* 分段时间
|
|
97
|
+
*/
|
|
98
|
+
segmentTime: z.number()
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const getDanmuWithSegmentTimeResponseSchema = getCommentsResponseSchema;
|
|
102
|
+
|
|
103
|
+
export const widgetI18nLocaleSchema = z.union([z.literal("en"), z.literal("zh-Hans"), z.literal("zh-Hant"), z.literal("ja"), z.literal("ko"), z.literal("es"), z.literal("fr"), z.literal("pt-BR"), z.literal("ru"), z.literal("ar"), z.string()]);
|
|
104
|
+
|
|
105
|
+
export const widgetModuleParamTypeSchema = z.union([z.literal("input"), z.literal("constant"), z.literal("enumeration"), z.literal("count"), z.literal("page"), z.literal("offset"), z.literal("language"), z.literal("userId")]);
|
|
106
|
+
|
|
107
|
+
export const widgetModuleParamSchema = z.object({
|
|
108
|
+
/** 参数名 */
|
|
109
|
+
name: z.string(),
|
|
110
|
+
/** 参数显示标题 */
|
|
111
|
+
title: z.string(),
|
|
112
|
+
/** 参数类型 */
|
|
113
|
+
type: widgetModuleParamTypeSchema,
|
|
114
|
+
/** 参数描述 */
|
|
115
|
+
description: z.string().optional(),
|
|
116
|
+
/** 默认值 */
|
|
117
|
+
value: z.string().optional(),
|
|
118
|
+
/** 当符合该条件时才会触发该参数 */
|
|
119
|
+
belongTo: z.object({
|
|
120
|
+
/** 所属参数的子参数 */
|
|
121
|
+
paramName: z.string(),
|
|
122
|
+
/** 所属参数包含的值 */
|
|
123
|
+
value: z.array(z.string())
|
|
124
|
+
}).optional(),
|
|
125
|
+
/** 占位符选项 */
|
|
126
|
+
placeholders: z.array(z.object({
|
|
127
|
+
title: z.string(),
|
|
128
|
+
value: z.string()
|
|
129
|
+
})).optional(),
|
|
130
|
+
/** 枚举选项 */
|
|
131
|
+
enumOptions: z.array(z.object({
|
|
132
|
+
title: z.string(),
|
|
133
|
+
value: z.string()
|
|
134
|
+
})).optional()
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export const streamSourceItemSchema = z.object({
|
|
138
|
+
name: z.string(),
|
|
139
|
+
description: z.string().optional(),
|
|
140
|
+
url: z.string(),
|
|
141
|
+
customHeaders: z.record(z.string(), z.string()).optional(),
|
|
142
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
143
|
+
playerType: z.union([z.literal("system"), z.literal("app")]).optional()
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export const subtitleItemSchema = z.object({
|
|
147
|
+
id: z.string(),
|
|
148
|
+
title: z.string(),
|
|
149
|
+
lang: z.string(),
|
|
150
|
+
count: z.number(),
|
|
151
|
+
url: z.string()
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export const subtitleArchiveFileSchema = z.object({
|
|
155
|
+
/** 解压目录内的相对路径,原样返回,不要拼绝对路径 */
|
|
156
|
+
path: z.string(),
|
|
157
|
+
/** 文件名 */
|
|
158
|
+
name: z.string(),
|
|
159
|
+
/** 小写扩展名 */
|
|
160
|
+
extension: z.string(),
|
|
161
|
+
/** 文件大小,可能为空 */
|
|
162
|
+
size: z.number().optional()
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const resolveSubtitleArchiveParamsSchema = z.object({
|
|
166
|
+
archiveName: z.string().optional(),
|
|
167
|
+
archiveUrl: z.string().optional(),
|
|
168
|
+
subtitleId: z.string().optional(),
|
|
169
|
+
subtitleName: z.string().optional(),
|
|
170
|
+
subtitleLanguage: z.string().optional(),
|
|
171
|
+
/** 全部解压文件的 JSON 字符串 */
|
|
172
|
+
files: z.string().optional(),
|
|
173
|
+
/** 客户端支持的字幕文件 JSON 字符串 */
|
|
174
|
+
subtitleFiles: z.string().optional()
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const resolveSubtitleArchiveResultSchema = z.union([z.string(), z.array(z.string()), z.object({
|
|
178
|
+
path: z.string()
|
|
179
|
+
}), z.object({
|
|
180
|
+
files: z.array(z.string())
|
|
181
|
+
})]).nullable();
|
|
182
|
+
|
|
183
|
+
export const genreItemSchema = z.object({
|
|
184
|
+
/** 分类 ID,详情页打开列表时作为 genreId 回传 */
|
|
185
|
+
id: z.string(),
|
|
186
|
+
/** 分类标题 */
|
|
187
|
+
title: z.string()
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export const personItemSchema = z.object({
|
|
191
|
+
/** 人物 ID,详情页打开列表时作为 peopleId 回传 */
|
|
192
|
+
id: z.string(),
|
|
193
|
+
/** 人物名称 */
|
|
194
|
+
title: z.string(),
|
|
195
|
+
/** 头像地址 */
|
|
196
|
+
avatar: z.string().optional(),
|
|
197
|
+
/** 角色 */
|
|
198
|
+
role: z.string().optional()
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export const trailerItemSchema = z.object({
|
|
202
|
+
/** 预告片封面 */
|
|
203
|
+
coverUrl: z.string().optional(),
|
|
204
|
+
/** 预告片地址 */
|
|
205
|
+
url: z.string()
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export const videoItemChildSchema = z.object({
|
|
209
|
+
/** 唯一标识符。对于 url 类型为 url 地址,对于 douban/imdb/tmdb 类型为对应 ID。tmdb ID 格式为 type.id,如 tv.123 */
|
|
210
|
+
id: z.string(),
|
|
211
|
+
/** 类型标识。forward 表示可匹配的网盘资源身份,客户端会归一为 fw- 前缀 */
|
|
212
|
+
type: z.union([z.literal("url"), z.literal("detail"), z.literal("douban"), z.literal("imdb"), z.literal("tmdb"), z.literal("forward")]),
|
|
213
|
+
/** 标题 */
|
|
214
|
+
title: z.string(),
|
|
215
|
+
/** 通用兜底封面。横图位排在 backdropPath 之后,竖图位排在最后 */
|
|
216
|
+
coverUrl: z.string().optional(),
|
|
217
|
+
/** 纵向封面图片地址 */
|
|
218
|
+
posterPath: z.string().optional(),
|
|
219
|
+
/** 详情页海报位专用,覆盖 posterPath */
|
|
220
|
+
detailPoster: z.string().optional(),
|
|
221
|
+
/** 横向封面地址 */
|
|
222
|
+
backdropPath: z.string().optional(),
|
|
223
|
+
/** 发布时间 */
|
|
224
|
+
releaseDate: z.string().optional(),
|
|
225
|
+
/** 媒体类型 */
|
|
226
|
+
mediaType: z.union([z.literal("tv"), z.literal("movie")]).optional(),
|
|
227
|
+
/** 评分 */
|
|
228
|
+
rating: z.string().optional(),
|
|
229
|
+
/** 分类 */
|
|
230
|
+
genreTitle: z.string().optional(),
|
|
231
|
+
/** 可点击分类,详情页会用 id 打开列表 */
|
|
232
|
+
genreItems: z.array(genreItemSchema).optional(),
|
|
233
|
+
/** 演员/人物,详情页会用 id 打开列表 */
|
|
234
|
+
peoples: z.array(personItemSchema).optional(),
|
|
235
|
+
/** 时长(秒) */
|
|
236
|
+
duration: z.number().optional(),
|
|
237
|
+
/** 时长文本格式 */
|
|
238
|
+
durationText: z.string().optional(),
|
|
239
|
+
/** 预览视频地址 */
|
|
240
|
+
previewUrl: z.string().optional(),
|
|
241
|
+
/** 预告片 */
|
|
242
|
+
trailers: z.array(trailerItemSchema).optional(),
|
|
243
|
+
/** 视频播放地址 */
|
|
244
|
+
videoUrl: z.string().optional(),
|
|
245
|
+
/** 详情页地址 */
|
|
246
|
+
link: z.string(),
|
|
247
|
+
/** 集数 */
|
|
248
|
+
episode: z.number().optional(),
|
|
249
|
+
/** 描述 */
|
|
250
|
+
description: z.string().optional(),
|
|
251
|
+
/** 播放器类型 */
|
|
252
|
+
playerType: z.union([z.literal("system"), z.literal("app")]).optional(),
|
|
253
|
+
/** 剧照/截图列表,详情页展示 */
|
|
254
|
+
backdropPaths: z.array(z.string()).optional()
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 视频项目的元数据接口
|
|
259
|
+
*/
|
|
260
|
+
export const videoItemSchema: z.ZodTypeAny = z.lazy((): z.ZodTypeAny => videoItemChildSchema.extend({
|
|
261
|
+
/** 子项目列表(最多一层) */
|
|
262
|
+
childItems: z.array(videoItemChildSchema).optional(),
|
|
263
|
+
/** 剧集列表 */
|
|
264
|
+
episodeItems: z.array(videoItemSchema).optional(),
|
|
265
|
+
/** 相关推荐 */
|
|
266
|
+
relatedItems: z.array(videoItemSchema).optional()
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 列表模块隐式参数。从详情页分类或人物进入列表时由客户端注入。
|
|
271
|
+
*/
|
|
272
|
+
export const videoListParamsSchema = z.object({
|
|
273
|
+
/** 分类 ID,对应 genreItems[].id */
|
|
274
|
+
genreId: z.string().optional(),
|
|
275
|
+
/** 人物 ID,对应 peoples[].id */
|
|
276
|
+
peopleId: z.string().optional()
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
export const baseWidgetModuleSchema = z.object({
|
|
280
|
+
/** 模块唯一标识符 */
|
|
281
|
+
id: z.string(),
|
|
282
|
+
/** 模块类型 */
|
|
283
|
+
type: z.union([z.literal("danmu"), z.literal("stream"), z.literal("subtitle")]).optional(),
|
|
284
|
+
/** 模块标题 */
|
|
285
|
+
title: z.string(),
|
|
286
|
+
/** 模块描述 */
|
|
287
|
+
description: z.string().optional(),
|
|
288
|
+
/** 是否需要 WebView */
|
|
289
|
+
requiresWebView: z.boolean().optional(),
|
|
290
|
+
/** 处理函数名 */
|
|
291
|
+
functionName: z.string(),
|
|
292
|
+
/** 是否支持分段模式 */
|
|
293
|
+
sectionMode: z.boolean().optional(),
|
|
294
|
+
/**
|
|
295
|
+
* 缓存时长,单位:秒
|
|
296
|
+
* @default 3600
|
|
297
|
+
*/
|
|
298
|
+
cacheDuration: z.number().optional().default(3600),
|
|
299
|
+
/** 参数配置 */
|
|
300
|
+
params: z.array(widgetModuleParamSchema).optional()
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
export const widgetModuleVideoSchema = baseWidgetModuleSchema.extend({
|
|
304
|
+
type: z.never().optional()
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
export const widgetModuleDanmuSchema = baseWidgetModuleSchema.extend({
|
|
308
|
+
type: z.literal("danmu"),
|
|
309
|
+
id: z.union([z.literal("searchDanmu"), z.literal("getDetail"), z.literal("getComments"), z.literal("getDanmuWithSegmentTime")])
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
export const widgetModuleStreamSchema = baseWidgetModuleSchema.extend({
|
|
313
|
+
type: z.literal("stream"),
|
|
314
|
+
id: z.literal("loadResource")
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
export const widgetModuleSubtitleSchema = baseWidgetModuleSchema.extend({
|
|
318
|
+
type: z.literal("subtitle"),
|
|
319
|
+
id: z.literal("loadSubtitle")
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
export const widgetModuleSchema = z.union([widgetModuleVideoSchema, widgetModuleDanmuSchema, widgetModuleStreamSchema, widgetModuleSubtitleSchema]);
|
|
323
|
+
|
|
324
|
+
export const widgetMetadataSchema = z.object({
|
|
325
|
+
/** Widget 唯一标识符 */
|
|
326
|
+
id: z.string(),
|
|
327
|
+
/** Widget 显示标题 */
|
|
328
|
+
title: z.string(),
|
|
329
|
+
/** Widget 描述 */
|
|
330
|
+
description: z.string().optional(),
|
|
331
|
+
/** 作者 */
|
|
332
|
+
author: z.string().optional(),
|
|
333
|
+
/** 网站地址 */
|
|
334
|
+
site: z.string().optional(),
|
|
335
|
+
/** Widget 版本 */
|
|
336
|
+
version: z.string().optional(),
|
|
337
|
+
/** 所需 Rex 版本 */
|
|
338
|
+
requiredVersion: z.string().optional(),
|
|
339
|
+
/**
|
|
340
|
+
* Widget 图标地址(官方 demo 字段名)
|
|
341
|
+
*/
|
|
342
|
+
icon: z.string().optional(),
|
|
343
|
+
/**
|
|
344
|
+
* Widget 图标地址(Rex 插件字段名,与 icon 同一含义)
|
|
345
|
+
*/
|
|
346
|
+
iconurl: z.string().optional(),
|
|
347
|
+
/**
|
|
348
|
+
* 文案国际化:locale → 源文案 → 译文
|
|
349
|
+
*/
|
|
350
|
+
i18n: z.record(widgetI18nLocaleSchema, z.record(z.string(), z.string())).optional(),
|
|
351
|
+
/**
|
|
352
|
+
* 详情数据缓存时长,单位:秒
|
|
353
|
+
* @default 60
|
|
354
|
+
*/
|
|
355
|
+
detailCacheDuration: z.number().optional().default(60),
|
|
356
|
+
/**
|
|
357
|
+
* 全局参数配置
|
|
358
|
+
*/
|
|
359
|
+
globalParams: z.array(widgetModuleParamSchema).optional(),
|
|
360
|
+
/** 功能模块列表 */
|
|
361
|
+
modules: z.array(widgetModuleSchema),
|
|
362
|
+
/** 搜索功能配置 */
|
|
363
|
+
search: z.object({
|
|
364
|
+
/** 搜索标题 */
|
|
365
|
+
title: z.string(),
|
|
366
|
+
/** 搜索函数名 */
|
|
367
|
+
functionName: z.string(),
|
|
368
|
+
/** 搜索参数配置 */
|
|
369
|
+
params: z.array(widgetModuleParamSchema)
|
|
370
|
+
}).optional()
|
|
371
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
interface RequestOptions {
|
|
2
|
+
headers?: Record<string, string>;
|
|
3
|
+
params?: Record<string, string>;
|
|
4
|
+
zlibMode?: boolean;
|
|
5
|
+
base64Data?: boolean;
|
|
6
|
+
allow_redirects?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface HttpRequestOptions extends RequestOptions {
|
|
9
|
+
url: string;
|
|
10
|
+
method?: 'GET' | 'POST';
|
|
11
|
+
data?: unknown;
|
|
12
|
+
body?: unknown;
|
|
13
|
+
}
|
|
14
|
+
export declare const WidgetAdaptor: {
|
|
15
|
+
http: {
|
|
16
|
+
get: <T>(url: string, options?: RequestOptions) => Promise<{
|
|
17
|
+
data: T;
|
|
18
|
+
statusCode: number;
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
}>;
|
|
21
|
+
post: <T>(url: string, body: unknown, options?: RequestOptions) => Promise<{
|
|
22
|
+
data: T;
|
|
23
|
+
statusCode: number;
|
|
24
|
+
headers: Record<string, string>;
|
|
25
|
+
}>;
|
|
26
|
+
request: <T>(options: HttpRequestOptions) => Promise<{
|
|
27
|
+
data: T;
|
|
28
|
+
statusCode: number;
|
|
29
|
+
headers: Record<string, string>;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
tmdb: {
|
|
33
|
+
get: <T>(url: string, options?: RequestOptions) => Promise<T>;
|
|
34
|
+
};
|
|
35
|
+
html: {
|
|
36
|
+
load: (...args: Parameters<typeof import('cheerio').load>) => Promise<ReturnType<typeof import('cheerio').load>>;
|
|
37
|
+
};
|
|
38
|
+
storage: {
|
|
39
|
+
get: (key: string) => Promise<string | null>;
|
|
40
|
+
set: (key: string, value: string) => Promise<void>;
|
|
41
|
+
remove: (key: string) => Promise<void>;
|
|
42
|
+
keys: () => Promise<string[]>;
|
|
43
|
+
clear: () => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
sharedCache: {
|
|
46
|
+
get: (namespace: string, key: string) => unknown;
|
|
47
|
+
set: (namespace: string, key: string, value?: unknown) => void;
|
|
48
|
+
remove: (namespace: string, key: string) => void;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import node_fs from "node:fs";
|
|
2
|
+
import node_os from "node:os";
|
|
3
|
+
import node_path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { inflate } from "node:zlib";
|
|
6
|
+
import { load } from "cheerio";
|
|
7
|
+
const createHttpRequest = async (url, method, options)=>{
|
|
8
|
+
const fetchOptions = {
|
|
9
|
+
method,
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
...options?.headers
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
if ('POST' === method && options?.body !== void 0) fetchOptions.body = 'string' == typeof options.body ? options.body : JSON.stringify(options.body);
|
|
16
|
+
const uri = new URL(url);
|
|
17
|
+
if (options?.params) Object.entries(options.params).forEach(([key, value])=>{
|
|
18
|
+
uri.searchParams.set(key, value);
|
|
19
|
+
});
|
|
20
|
+
let data;
|
|
21
|
+
const response = await fetch(uri, fetchOptions);
|
|
22
|
+
if (options?.base64Data) {
|
|
23
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
24
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
25
|
+
const base64Data = buffer.toString('base64');
|
|
26
|
+
data = base64Data;
|
|
27
|
+
} else if (options?.zlibMode) data = (await promisify(inflate)(await response.arrayBuffer())).toString('utf-8');
|
|
28
|
+
else {
|
|
29
|
+
const textData = await response.text();
|
|
30
|
+
try {
|
|
31
|
+
data = JSON.parse(textData);
|
|
32
|
+
} catch {
|
|
33
|
+
data = textData;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const headers = {};
|
|
37
|
+
response.headers.forEach((value, key)=>{
|
|
38
|
+
if (headers[key]) headers[key] = `${headers[key]}, ${value}`;
|
|
39
|
+
else headers[key] = value;
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
data,
|
|
43
|
+
statusCode: response.status,
|
|
44
|
+
headers
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const ADAPTOR_ROOT = node_path.join(node_os.tmpdir(), 'rex-widget-adaptor');
|
|
48
|
+
const STORAGE_CONFIG = {
|
|
49
|
+
get DIR () {
|
|
50
|
+
const dir = node_path.join(ADAPTOR_ROOT, 'storage');
|
|
51
|
+
if (!node_fs.existsSync(dir)) node_fs.mkdirSync(dir, {
|
|
52
|
+
recursive: true
|
|
53
|
+
});
|
|
54
|
+
return dir;
|
|
55
|
+
},
|
|
56
|
+
getFilePath: (key)=>node_path.join(STORAGE_CONFIG.DIR, encodeURIComponent(key))
|
|
57
|
+
};
|
|
58
|
+
const SHARED_CACHE_CONFIG = {
|
|
59
|
+
getFilePath: (namespace, key)=>{
|
|
60
|
+
const dir = node_path.join(ADAPTOR_ROOT, 'shared-cache', encodeURIComponent(namespace));
|
|
61
|
+
if (!node_fs.existsSync(dir)) node_fs.mkdirSync(dir, {
|
|
62
|
+
recursive: true
|
|
63
|
+
});
|
|
64
|
+
return node_path.join(dir, encodeURIComponent(key));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const WidgetAdaptor = {
|
|
68
|
+
http: {
|
|
69
|
+
get: async (url, options)=>createHttpRequest(url, 'GET', options),
|
|
70
|
+
post: async (url, body, options)=>createHttpRequest(url, 'POST', {
|
|
71
|
+
...options,
|
|
72
|
+
body
|
|
73
|
+
}),
|
|
74
|
+
request: async (options)=>{
|
|
75
|
+
const { url, method = 'GET', data, body, ...rest } = options;
|
|
76
|
+
return createHttpRequest(url, method, {
|
|
77
|
+
...rest,
|
|
78
|
+
body: body ?? data
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
tmdb: {
|
|
83
|
+
get: async (url, options)=>{
|
|
84
|
+
const urlObj = new URL(`/3/${url}`, 'https://api.themoviedb.org/');
|
|
85
|
+
options ||= {};
|
|
86
|
+
options.headers = {
|
|
87
|
+
...options.headers,
|
|
88
|
+
accept: 'application/json',
|
|
89
|
+
Authorization: `Bearer ${process.env.TMDB_API_KEY}`
|
|
90
|
+
};
|
|
91
|
+
const resp = await WidgetAdaptor.http.get(urlObj.toString(), options);
|
|
92
|
+
return resp.data;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
html: {
|
|
96
|
+
load: (...args)=>new Promise((resolve)=>resolve(load(...args)))
|
|
97
|
+
},
|
|
98
|
+
storage: {
|
|
99
|
+
get: async (key)=>{
|
|
100
|
+
const filePath = STORAGE_CONFIG.getFilePath(key);
|
|
101
|
+
if (!node_fs.existsSync(filePath)) return null;
|
|
102
|
+
return node_fs.promises.readFile(filePath, 'utf-8');
|
|
103
|
+
},
|
|
104
|
+
set: (key, value)=>node_fs.promises.writeFile(STORAGE_CONFIG.getFilePath(key), value, 'utf-8'),
|
|
105
|
+
remove: (key)=>node_fs.promises.rm(STORAGE_CONFIG.getFilePath(key)),
|
|
106
|
+
keys: async ()=>{
|
|
107
|
+
const keys = await node_fs.promises.readdir(STORAGE_CONFIG.DIR);
|
|
108
|
+
return keys.map((key)=>decodeURIComponent(key));
|
|
109
|
+
},
|
|
110
|
+
clear: ()=>node_fs.promises.rm(STORAGE_CONFIG.DIR, {
|
|
111
|
+
recursive: true
|
|
112
|
+
})
|
|
113
|
+
},
|
|
114
|
+
sharedCache: {
|
|
115
|
+
get: (namespace, key)=>{
|
|
116
|
+
const filePath = SHARED_CACHE_CONFIG.getFilePath(namespace, key);
|
|
117
|
+
if (!node_fs.existsSync(filePath)) return null;
|
|
118
|
+
const raw = node_fs.readFileSync(filePath, 'utf-8');
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(raw);
|
|
121
|
+
} catch {
|
|
122
|
+
return raw;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
set: (namespace, key, value)=>{
|
|
126
|
+
node_fs.writeFileSync(SHARED_CACHE_CONFIG.getFilePath(namespace, key), JSON.stringify(value ?? null), 'utf-8');
|
|
127
|
+
},
|
|
128
|
+
remove: (namespace, key)=>{
|
|
129
|
+
node_fs.rmSync(SHARED_CACHE_CONFIG.getFilePath(namespace, key), {
|
|
130
|
+
force: true
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
export { WidgetAdaptor };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rexnow/libs",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
"./widget-adaptor": {
|
|
7
|
+
"types": "./dist/widget-adaptor.d.ts",
|
|
8
|
+
"import": "./dist/widget-adaptor.js"
|
|
9
|
+
},
|
|
10
|
+
"./env": "./dist/env/index.d.ts",
|
|
11
|
+
"./env.zod": "./dist/env.zod/index.ts",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"provenance": true
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"cheerio": "^1.2.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@rsbuild/core": "^2.2.5",
|
|
25
|
+
"@rslib/core": "^1.0.0",
|
|
26
|
+
"@rstest/adapter-rslib": "^0.11.12",
|
|
27
|
+
"@rstest/core": "^0.11.12",
|
|
28
|
+
"@types/node": "^26.5.1",
|
|
29
|
+
"ts-morph": "^28.0.0",
|
|
30
|
+
"ts-to-zod": "^5.1.0",
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
|
+
"zod": "^4.6.4"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/baranwang/rex-widget-libs.git",
|
|
37
|
+
"directory": "packages/libs"
|
|
38
|
+
},
|
|
39
|
+
"author": "baranwang",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "rslib build",
|
|
43
|
+
"dev": "rslib build --watch",
|
|
44
|
+
"test": "rstest"
|
|
45
|
+
}
|
|
46
|
+
}
|