ehbrowser 1.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 +202 -0
- package/README.md +531 -0
- package/dist/api/contract.js +7 -0
- package/dist/api/dto/auth.js +5 -0
- package/dist/api/dto/common.js +6 -0
- package/dist/api/dto/download.js +5 -0
- package/dist/api/dto/favorite.js +5 -0
- package/dist/api/dto/gallery.js +43 -0
- package/dist/api/dto/index.js +6 -0
- package/dist/api/dto/library.js +5 -0
- package/dist/api/dto/log.js +5 -0
- package/dist/api/dto/playlist.js +8 -0
- package/dist/api/dto/settings.js +5 -0
- package/dist/api/dto/storage.js +5 -0
- package/dist/api/dto/translate.js +6 -0
- package/dist/api/envelope.js +41 -0
- package/dist/api/events.js +20 -0
- package/dist/api/index.js +12 -0
- package/dist/api/routes.js +516 -0
- package/dist/config/atomic.js +85 -0
- package/dist/config/auth-setting.js +97 -0
- package/dist/config/cli.js +19 -0
- package/dist/config/db.js +168 -0
- package/dist/config/index.js +48 -0
- package/dist/config/json-store.js +219 -0
- package/dist/config/migrations.js +100 -0
- package/dist/config/schema.js +108 -0
- package/dist/config/user-setting.js +21 -0
- package/dist/config/validate.js +173 -0
- package/dist/eh/api.js +106 -0
- package/dist/eh/archives.js +100 -0
- package/dist/eh/auth.js +89 -0
- package/dist/eh/cookies.js +65 -0
- package/dist/eh/favorites.js +94 -0
- package/dist/eh/html.js +158 -0
- package/dist/eh/http.js +214 -0
- package/dist/eh/index.js +13 -0
- package/dist/eh/types.js +11 -0
- package/dist/eh/urls.js +81 -0
- package/dist/main.js +170 -0
- package/dist/platform/errors.js +76 -0
- package/dist/platform/open-external.js +90 -0
- package/dist/platform/os.js +28 -0
- package/dist/platform/paths.js +140 -0
- package/dist/server.js +1074 -0
- package/dist/services/auth-service.js +276 -0
- package/dist/services/config-service.js +227 -0
- package/dist/services/detail-cache.js +193 -0
- package/dist/services/detail-store.js +208 -0
- package/dist/services/download-file.js +127 -0
- package/dist/services/download-service.js +586 -0
- package/dist/services/favorite-service.js +237 -0
- package/dist/services/gallery-service.js +403 -0
- package/dist/services/local-library.js +471 -0
- package/dist/services/log-service.js +139 -0
- package/dist/services/playlist-service.js +85 -0
- package/dist/services/search-cache.js +78 -0
- package/dist/services/storage-service.js +27 -0
- package/dist/services/translate-service.js +208 -0
- package/dist/services/update-service.js +268 -0
- package/dist/services/upstream-service.js +64 -0
- package/dist/services/zip.js +210 -0
- package/dist/web/assets/index-C0tAHpmx.css +1 -0
- package/dist/web/assets/index-myYEnJ1q.js +4 -0
- package/dist/web/index.html +13 -0
- package/package.json +63 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 画廊相关线上结构。
|
|
3
|
+
*
|
|
4
|
+
* 字段与 EhViewer 的 GalleryInfo / GalleryDetail 对齐,有三处差异:
|
|
5
|
+
* category 使用上游原始字符串而非下标;数字字段统一为 number;
|
|
6
|
+
* parent / current / first 合并为单个关系对象。
|
|
7
|
+
*/
|
|
8
|
+
/** 分类名,取上游原始字符串(大小写与空格保持不变) */
|
|
9
|
+
export const GALLERY_CATEGORIES = [
|
|
10
|
+
"Doujinshi",
|
|
11
|
+
"Manga",
|
|
12
|
+
"Artist CG",
|
|
13
|
+
"Game CG",
|
|
14
|
+
"Western",
|
|
15
|
+
"Image Set",
|
|
16
|
+
"Non-H",
|
|
17
|
+
"Cosplay",
|
|
18
|
+
"Asian Porn",
|
|
19
|
+
"Misc",
|
|
20
|
+
"Private",
|
|
21
|
+
];
|
|
22
|
+
/** 对应上游 language:* 标签;translated 为独立标签,不属于语种 */
|
|
23
|
+
export const GALLERY_LANGUAGES = [
|
|
24
|
+
"chinese",
|
|
25
|
+
"dutch",
|
|
26
|
+
"english",
|
|
27
|
+
"french",
|
|
28
|
+
"german",
|
|
29
|
+
"hungarian",
|
|
30
|
+
"italian",
|
|
31
|
+
"japanese",
|
|
32
|
+
"korean",
|
|
33
|
+
"polish",
|
|
34
|
+
"portuguese",
|
|
35
|
+
"russian",
|
|
36
|
+
"spanish",
|
|
37
|
+
"thai",
|
|
38
|
+
"vietnamese",
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* 一次检索取多少条。界面与启动预热都使用该值,缓存命中判定以此为准。
|
|
42
|
+
*/
|
|
43
|
+
export const SEARCH_PAGE_LIMIT = 24;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 播放列表的线上结构。
|
|
3
|
+
* 用户播放列表持久化在 SQLite 的 playlist_items 表,应用重启不丢失。
|
|
4
|
+
*/
|
|
5
|
+
/** 由 gid 与分辨率拼出 key。分辨率缺失时用 net */
|
|
6
|
+
export function playlistKeyOf(gid, resolution) {
|
|
7
|
+
return `${gid}-${resolution === null || resolution === undefined || resolution === "" ? "net" : resolution}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 统一响应信封
|
|
3
|
+
* 未采用「裸数据 + HTTP 状态码」的原因:客户端只需判断一次 ok,错误统一经 error.code 区分;
|
|
4
|
+
* 校验问题随 issues 返回,可直接定位到输入框。HTTP 状态码仍按 HTTP_STATUS_BY_ERROR 设置
|
|
5
|
+
*/
|
|
6
|
+
/** 错误码到 HTTP 状态码的映射 */
|
|
7
|
+
export const HTTP_STATUS_BY_ERROR = {
|
|
8
|
+
bad_request: 400,
|
|
9
|
+
unauthorized: 401,
|
|
10
|
+
forbidden: 403,
|
|
11
|
+
not_found: 404,
|
|
12
|
+
conflict: 409,
|
|
13
|
+
config_invalid: 422,
|
|
14
|
+
not_logged_in: 403,
|
|
15
|
+
rate_limited: 429,
|
|
16
|
+
upstream_unavailable: 502,
|
|
17
|
+
busy: 503,
|
|
18
|
+
not_implemented: 501,
|
|
19
|
+
internal: 500,
|
|
20
|
+
};
|
|
21
|
+
/** 构造成功响应 */
|
|
22
|
+
export function ok(data, requestId) {
|
|
23
|
+
return requestId === undefined ? { ok: true, data } : { ok: true, data, requestId };
|
|
24
|
+
}
|
|
25
|
+
/** 构造失败响应 */
|
|
26
|
+
export function fail(code, message, options = {}) {
|
|
27
|
+
const error = options.issues === undefined
|
|
28
|
+
? { code, message }
|
|
29
|
+
: { code, message, issues: options.issues };
|
|
30
|
+
return options.requestId === undefined
|
|
31
|
+
? { ok: false, error }
|
|
32
|
+
: { ok: false, error, requestId: options.requestId };
|
|
33
|
+
}
|
|
34
|
+
/** 类型收窄辅助 */
|
|
35
|
+
export function isApiFailure(response) {
|
|
36
|
+
return !response.ok;
|
|
37
|
+
}
|
|
38
|
+
/** 取错误码对应的 HTTP 状态码 */
|
|
39
|
+
export function statusForError(code) {
|
|
40
|
+
return HTTP_STATUS_BY_ERROR[code];
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 服务端到浏览器的事件推送(SSE)
|
|
3
|
+
* 配置变更、账号切换、下载进度均产生于服务端,浏览器可能同时开启多个页面;
|
|
4
|
+
* 由服务端广播替代各页面轮询。事件名与负载类型一一对应,两端共用同一张表
|
|
5
|
+
*/
|
|
6
|
+
export const API_EVENT_NAMES = [
|
|
7
|
+
"server.ready",
|
|
8
|
+
"config.changed",
|
|
9
|
+
"auth.changed",
|
|
10
|
+
"download.changed",
|
|
11
|
+
"library.update",
|
|
12
|
+
"library.progress",
|
|
13
|
+
"log.appended",
|
|
14
|
+
];
|
|
15
|
+
/** 心跳间隔;长时间无事件时保活,避免浏览器断开连接 */
|
|
16
|
+
export const API_EVENT_HEARTBEAT_MS = 15000;
|
|
17
|
+
/** 校验事件名;名称错误时浏览器无法接收 */
|
|
18
|
+
export function isApiEventName(value) {
|
|
19
|
+
return API_EVENT_NAMES.includes(value);
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* src/api 出口。对外仅暴露本文件,内部文件结构可变。
|
|
3
|
+
* 依赖方向:不引用 config / services / platform / eh,属纯契约层。
|
|
4
|
+
*
|
|
5
|
+
* @author 西博卡滋
|
|
6
|
+
* @since 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
export * from "./envelope.js";
|
|
9
|
+
export * from "./contract.js";
|
|
10
|
+
export * from "./routes.js";
|
|
11
|
+
export * from "./events.js";
|
|
12
|
+
export * from "./dto/index.js";
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 路由表与 URL 工具。
|
|
3
|
+
* ROUTES 与 contract.ts 相互约束:漏写路由或写入契约中不存在的路由都会编译报错。
|
|
4
|
+
* 用法:routePath("galleries.detail", { gid, token }) + buildQueryString({ page: 2 })。
|
|
5
|
+
*/
|
|
6
|
+
/** 接口统一前缀 */
|
|
7
|
+
export const API_BASE_PATH = "/api";
|
|
8
|
+
/**
|
|
9
|
+
* 本地令牌请求头
|
|
10
|
+
* 服务端仅监听 127.0.0.1,但浏览器中的任意页面都可向 localhost 发起请求(CSRF),
|
|
11
|
+
* 因此所有接口均要求令牌;令牌每次启动重新生成,仅注入本地界面
|
|
12
|
+
*/
|
|
13
|
+
export const API_TOKEN_HEADER = "x-ehbrowser-token";
|
|
14
|
+
/** EventSource 无法设置请求头,令牌经 query 传递 */
|
|
15
|
+
export const API_TOKEN_QUERY_PARAM = "_token";
|
|
16
|
+
export const ROUTES = {
|
|
17
|
+
// ── 系统 ───────────────────────────────────────────────
|
|
18
|
+
"system.health": {
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/api/health",
|
|
21
|
+
description: "服务健康状态、版本与运行时长",
|
|
22
|
+
requiresToken: false,
|
|
23
|
+
requiresAccount: false,
|
|
24
|
+
},
|
|
25
|
+
"system.shutdown": {
|
|
26
|
+
method: "POST",
|
|
27
|
+
path: "/api/system/shutdown",
|
|
28
|
+
description: "关闭本地服务;界面与事件流随之断开",
|
|
29
|
+
requiresToken: true,
|
|
30
|
+
requiresAccount: false,
|
|
31
|
+
},
|
|
32
|
+
"system.events": {
|
|
33
|
+
method: "GET",
|
|
34
|
+
path: "/api/events",
|
|
35
|
+
description: "SSE 事件流:配置变更、鉴权变更、下载进度、日志",
|
|
36
|
+
requiresToken: true,
|
|
37
|
+
requiresAccount: false,
|
|
38
|
+
},
|
|
39
|
+
// ── 配置 ───────────────────────────────────────────────
|
|
40
|
+
"config.get": {
|
|
41
|
+
method: "GET",
|
|
42
|
+
path: "/api/config",
|
|
43
|
+
description: "取配置快照(生效值 + 每个字段的来源层)",
|
|
44
|
+
requiresToken: true,
|
|
45
|
+
requiresAccount: false,
|
|
46
|
+
},
|
|
47
|
+
"config.patch": {
|
|
48
|
+
method: "PATCH",
|
|
49
|
+
path: "/api/config",
|
|
50
|
+
description: "局部更新用户配置(深合并)",
|
|
51
|
+
requiresToken: true,
|
|
52
|
+
requiresAccount: false,
|
|
53
|
+
},
|
|
54
|
+
"config.paths": {
|
|
55
|
+
method: "GET",
|
|
56
|
+
path: "/api/config/paths",
|
|
57
|
+
description: "数据目录及其来源层(排错用)",
|
|
58
|
+
requiresToken: true,
|
|
59
|
+
requiresAccount: false,
|
|
60
|
+
},
|
|
61
|
+
"config.export": {
|
|
62
|
+
method: "POST",
|
|
63
|
+
path: "/api/config/export",
|
|
64
|
+
description: "导出配置包(可选包含账号凭据)",
|
|
65
|
+
requiresToken: true,
|
|
66
|
+
requiresAccount: false,
|
|
67
|
+
},
|
|
68
|
+
"config.import": {
|
|
69
|
+
method: "POST",
|
|
70
|
+
path: "/api/config/import",
|
|
71
|
+
description: "导入配置包(自动备份并迁移版本)",
|
|
72
|
+
requiresToken: true,
|
|
73
|
+
requiresAccount: false,
|
|
74
|
+
},
|
|
75
|
+
// ── 账号与鉴权 ─────────────────────────────────────────
|
|
76
|
+
"auth.status": {
|
|
77
|
+
method: "GET",
|
|
78
|
+
path: "/api/auth/status",
|
|
79
|
+
description: "登录状态摘要(不含任何凭据)",
|
|
80
|
+
requiresToken: true,
|
|
81
|
+
requiresAccount: false,
|
|
82
|
+
},
|
|
83
|
+
"auth.login": {
|
|
84
|
+
method: "POST",
|
|
85
|
+
path: "/api/auth/login",
|
|
86
|
+
description: "用账号密码登录,cookie 由服务端持有",
|
|
87
|
+
requiresToken: true,
|
|
88
|
+
requiresAccount: false,
|
|
89
|
+
},
|
|
90
|
+
"auth.logout": {
|
|
91
|
+
method: "POST",
|
|
92
|
+
path: "/api/auth/logout",
|
|
93
|
+
description: "清除当前账号的登录态",
|
|
94
|
+
requiresToken: true,
|
|
95
|
+
requiresAccount: false,
|
|
96
|
+
},
|
|
97
|
+
"auth.accounts.list": {
|
|
98
|
+
method: "GET",
|
|
99
|
+
path: "/api/auth/accounts",
|
|
100
|
+
description: "账号列表(脱敏)",
|
|
101
|
+
requiresToken: true,
|
|
102
|
+
requiresAccount: false,
|
|
103
|
+
},
|
|
104
|
+
"auth.accounts.create": {
|
|
105
|
+
method: "POST",
|
|
106
|
+
path: "/api/auth/accounts",
|
|
107
|
+
description: "新增账号(提交 cookie 或密码)",
|
|
108
|
+
requiresToken: true,
|
|
109
|
+
requiresAccount: false,
|
|
110
|
+
},
|
|
111
|
+
"auth.accounts.update": {
|
|
112
|
+
method: "PATCH",
|
|
113
|
+
path: "/api/auth/accounts/:accountId",
|
|
114
|
+
description: "更新账号备注或凭据",
|
|
115
|
+
requiresToken: true,
|
|
116
|
+
requiresAccount: false,
|
|
117
|
+
},
|
|
118
|
+
"auth.accounts.remove": {
|
|
119
|
+
method: "DELETE",
|
|
120
|
+
path: "/api/auth/accounts/:accountId",
|
|
121
|
+
description: "删除账号",
|
|
122
|
+
requiresToken: true,
|
|
123
|
+
requiresAccount: false,
|
|
124
|
+
},
|
|
125
|
+
"auth.accounts.activate": {
|
|
126
|
+
method: "POST",
|
|
127
|
+
path: "/api/auth/accounts/:accountId/activate",
|
|
128
|
+
description: "切换当前使用的账号",
|
|
129
|
+
requiresToken: true,
|
|
130
|
+
requiresAccount: false,
|
|
131
|
+
},
|
|
132
|
+
// ── 画廊 ───────────────────────────────────────────────
|
|
133
|
+
"galleries.search": {
|
|
134
|
+
method: "GET",
|
|
135
|
+
path: "/api/galleries",
|
|
136
|
+
description: "搜索/浏览画廊列表",
|
|
137
|
+
requiresToken: true,
|
|
138
|
+
requiresAccount: true,
|
|
139
|
+
},
|
|
140
|
+
"galleries.detailCache": {
|
|
141
|
+
method: "GET",
|
|
142
|
+
path: "/api/galleries/detail-cache",
|
|
143
|
+
description: "详情页缓存的状态(条数、上限、体积)",
|
|
144
|
+
requiresToken: true,
|
|
145
|
+
// 只读内存里的缓存,不访问上游
|
|
146
|
+
requiresAccount: false,
|
|
147
|
+
},
|
|
148
|
+
"galleries.detailCache.clear": {
|
|
149
|
+
method: "DELETE",
|
|
150
|
+
path: "/api/galleries/detail-cache",
|
|
151
|
+
description: "清空详情页缓存",
|
|
152
|
+
requiresToken: true,
|
|
153
|
+
requiresAccount: false,
|
|
154
|
+
},
|
|
155
|
+
"galleries.searchCache": {
|
|
156
|
+
method: "GET",
|
|
157
|
+
path: "/api/galleries/cache",
|
|
158
|
+
description: "上一次检索的条件与结果;进程内缓存,应用重启即丢",
|
|
159
|
+
requiresToken: true,
|
|
160
|
+
// 只读本地缓存,不访问上游,因此不要求账号
|
|
161
|
+
requiresAccount: false,
|
|
162
|
+
},
|
|
163
|
+
"galleries.newer": {
|
|
164
|
+
method: "GET",
|
|
165
|
+
path: "/api/galleries/:gid/:token/newer",
|
|
166
|
+
description: "查询是否存在更新的版本,结果落盘缓存",
|
|
167
|
+
requiresToken: true,
|
|
168
|
+
requiresAccount: false,
|
|
169
|
+
},
|
|
170
|
+
"galleries.detail": {
|
|
171
|
+
method: "GET",
|
|
172
|
+
path: "/api/galleries/:gid/:token",
|
|
173
|
+
description: "画廊详情(标签、预览、评论)",
|
|
174
|
+
requiresToken: true,
|
|
175
|
+
requiresAccount: true,
|
|
176
|
+
},
|
|
177
|
+
"galleries.page": {
|
|
178
|
+
method: "GET",
|
|
179
|
+
path: "/api/galleries/:gid/:token/pages/:page",
|
|
180
|
+
description: "取单页图片地址(服务端代为调用 showpage)",
|
|
181
|
+
requiresToken: true,
|
|
182
|
+
requiresAccount: true,
|
|
183
|
+
},
|
|
184
|
+
"galleries.token": {
|
|
185
|
+
method: "POST",
|
|
186
|
+
path: "/api/galleries/token",
|
|
187
|
+
description: "由单页链接反查画廊 token(gtoken)",
|
|
188
|
+
requiresToken: true,
|
|
189
|
+
requiresAccount: true,
|
|
190
|
+
},
|
|
191
|
+
"galleries.rate": {
|
|
192
|
+
method: "POST",
|
|
193
|
+
path: "/api/galleries/:gid/:token/rating",
|
|
194
|
+
description: "评分(需要 apiuid/apikey)",
|
|
195
|
+
requiresToken: true,
|
|
196
|
+
requiresAccount: true,
|
|
197
|
+
},
|
|
198
|
+
"galleries.favorite": {
|
|
199
|
+
method: "POST",
|
|
200
|
+
path: "/api/galleries/:gid/:token/favorite",
|
|
201
|
+
description: "收藏到指定分类,slot<0 表示取消收藏",
|
|
202
|
+
requiresToken: true,
|
|
203
|
+
requiresAccount: true,
|
|
204
|
+
},
|
|
205
|
+
"galleries.torrents": {
|
|
206
|
+
method: "GET",
|
|
207
|
+
path: "/api/galleries/:gid/:token/torrents",
|
|
208
|
+
description: "种子列表",
|
|
209
|
+
requiresToken: true,
|
|
210
|
+
requiresAccount: false,
|
|
211
|
+
},
|
|
212
|
+
"galleries.previews": {
|
|
213
|
+
method: "GET",
|
|
214
|
+
path: "/api/galleries/:gid/:token/previews",
|
|
215
|
+
description: "某一组预览图,每 20 页一组",
|
|
216
|
+
requiresToken: true,
|
|
217
|
+
requiresAccount: false,
|
|
218
|
+
},
|
|
219
|
+
"galleries.archives": {
|
|
220
|
+
method: "GET",
|
|
221
|
+
path: "/api/galleries/:gid/:token/archives",
|
|
222
|
+
description: "归档选项与账户资金",
|
|
223
|
+
requiresToken: true,
|
|
224
|
+
requiresAccount: true,
|
|
225
|
+
},
|
|
226
|
+
// ── 本地库 ─────────────────────────────────────────────
|
|
227
|
+
"library.list": {
|
|
228
|
+
method: "GET",
|
|
229
|
+
path: "/api/library",
|
|
230
|
+
description: "本地已下载的画廊列表",
|
|
231
|
+
requiresToken: true,
|
|
232
|
+
requiresAccount: false,
|
|
233
|
+
},
|
|
234
|
+
"library.progress": {
|
|
235
|
+
method: "PATCH",
|
|
236
|
+
path: "/api/library/:gid/:resolution",
|
|
237
|
+
description: "写回某个本地画廊的阅读进度",
|
|
238
|
+
requiresToken: true,
|
|
239
|
+
requiresAccount: false,
|
|
240
|
+
},
|
|
241
|
+
// 图片要能直接放进 <img src>,令牌经 query 传递(与 EventSource 的处理一致)
|
|
242
|
+
"library.image": {
|
|
243
|
+
method: "GET",
|
|
244
|
+
path: "/api/library/:gid/:resolution/pages/:page",
|
|
245
|
+
description: "取本地已下载的第 page 页图片",
|
|
246
|
+
requiresToken: true,
|
|
247
|
+
requiresAccount: false,
|
|
248
|
+
},
|
|
249
|
+
"library.updates": {
|
|
250
|
+
method: "GET",
|
|
251
|
+
path: "/api/library/updates",
|
|
252
|
+
description: "更新检查的缓存与进度(只读)",
|
|
253
|
+
requiresToken: true,
|
|
254
|
+
requiresAccount: false,
|
|
255
|
+
},
|
|
256
|
+
"library.check": {
|
|
257
|
+
method: "POST",
|
|
258
|
+
path: "/api/library/updates",
|
|
259
|
+
description: "发起更新检查,结果经 SSE 逐条推送",
|
|
260
|
+
requiresToken: true,
|
|
261
|
+
requiresAccount: true,
|
|
262
|
+
},
|
|
263
|
+
"library.updates.forget": {
|
|
264
|
+
method: "POST",
|
|
265
|
+
path: "/api/library/updates/forget",
|
|
266
|
+
description: "从更新列表里移除若干条目(不删除本地文件)",
|
|
267
|
+
requiresToken: true,
|
|
268
|
+
requiresAccount: false,
|
|
269
|
+
},
|
|
270
|
+
"library.remove": {
|
|
271
|
+
method: "DELETE",
|
|
272
|
+
path: "/api/library/:gid/:resolution",
|
|
273
|
+
description: "删除本地画廊及其目录",
|
|
274
|
+
requiresToken: true,
|
|
275
|
+
requiresAccount: false,
|
|
276
|
+
},
|
|
277
|
+
// ── 储存空间 ───────────────────────────────────────────
|
|
278
|
+
"storage.stats": {
|
|
279
|
+
method: "GET",
|
|
280
|
+
path: "/api/storage",
|
|
281
|
+
description: "本地画廊与画廊缓存的占用统计",
|
|
282
|
+
requiresToken: true,
|
|
283
|
+
requiresAccount: false,
|
|
284
|
+
},
|
|
285
|
+
"storage.cleanup": {
|
|
286
|
+
method: "POST",
|
|
287
|
+
path: "/api/storage/cleanup",
|
|
288
|
+
description: "按时间清理画廊缓存",
|
|
289
|
+
requiresToken: true,
|
|
290
|
+
requiresAccount: false,
|
|
291
|
+
},
|
|
292
|
+
// ── 收藏 ───────────────────────────────────────────────
|
|
293
|
+
"favorites.state": {
|
|
294
|
+
method: "GET",
|
|
295
|
+
path: "/api/favorites",
|
|
296
|
+
description: "本地收藏夹与云端收藏分类",
|
|
297
|
+
requiresToken: true,
|
|
298
|
+
requiresAccount: false,
|
|
299
|
+
},
|
|
300
|
+
"favorites.folder.create": {
|
|
301
|
+
method: "POST",
|
|
302
|
+
path: "/api/favorites/folders",
|
|
303
|
+
description: "新建本地收藏夹",
|
|
304
|
+
requiresToken: true,
|
|
305
|
+
requiresAccount: false,
|
|
306
|
+
},
|
|
307
|
+
"favorites.folder.remove": {
|
|
308
|
+
method: "DELETE",
|
|
309
|
+
path: "/api/favorites/folders/:folderId",
|
|
310
|
+
description: "删除本地收藏夹及其条目",
|
|
311
|
+
requiresToken: true,
|
|
312
|
+
requiresAccount: false,
|
|
313
|
+
},
|
|
314
|
+
"favorites.items": {
|
|
315
|
+
method: "GET",
|
|
316
|
+
path: "/api/favorites/folders/:folderId/items",
|
|
317
|
+
description: "本地收藏夹里的画廊",
|
|
318
|
+
requiresToken: true,
|
|
319
|
+
requiresAccount: false,
|
|
320
|
+
},
|
|
321
|
+
"favorites.items.add": {
|
|
322
|
+
method: "POST",
|
|
323
|
+
path: "/api/favorites/items",
|
|
324
|
+
description: "把一个画廊加入本地收藏夹(可同时指定云端槽位)",
|
|
325
|
+
requiresToken: true,
|
|
326
|
+
requiresAccount: false,
|
|
327
|
+
},
|
|
328
|
+
"favorites.items.remove": {
|
|
329
|
+
method: "DELETE",
|
|
330
|
+
path: "/api/favorites/folders/:folderId/items/:gid",
|
|
331
|
+
description: "从本地收藏夹移除一个画廊",
|
|
332
|
+
requiresToken: true,
|
|
333
|
+
requiresAccount: false,
|
|
334
|
+
},
|
|
335
|
+
"favorites.items.slot": {
|
|
336
|
+
method: "PATCH",
|
|
337
|
+
path: "/api/favorites/folders/:folderId/items/:gid",
|
|
338
|
+
description: "改这条收藏的标记号(云端槽位),登录时同步到上游",
|
|
339
|
+
requiresToken: true,
|
|
340
|
+
requiresAccount: false,
|
|
341
|
+
},
|
|
342
|
+
"favorites.cloud": {
|
|
343
|
+
method: "GET",
|
|
344
|
+
path: "/api/favorites/cloud",
|
|
345
|
+
description: "云端某个收藏分类里的画廊",
|
|
346
|
+
requiresToken: true,
|
|
347
|
+
requiresAccount: true,
|
|
348
|
+
},
|
|
349
|
+
"favorites.items.refresh": {
|
|
350
|
+
method: "POST",
|
|
351
|
+
path: "/api/favorites/folders/:folderId/items/refresh",
|
|
352
|
+
description: "更新标记号:把收藏从这一版挪到上游的最新版本",
|
|
353
|
+
requiresToken: true,
|
|
354
|
+
requiresAccount: false,
|
|
355
|
+
},
|
|
356
|
+
// ── 日志 ───────────────────────────────────────────────
|
|
357
|
+
"log.state": {
|
|
358
|
+
method: "GET",
|
|
359
|
+
path: "/api/logs",
|
|
360
|
+
description: "日志目录、文件名与最近若干条日志",
|
|
361
|
+
requiresToken: true,
|
|
362
|
+
// 只读内存里的环形缓冲,不访问上游
|
|
363
|
+
requiresAccount: false,
|
|
364
|
+
},
|
|
365
|
+
// ── 标签翻译词库 ───────────────────────────────────────
|
|
366
|
+
"translate.status": {
|
|
367
|
+
method: "GET",
|
|
368
|
+
path: "/api/translate",
|
|
369
|
+
description: "标签翻译词库的状态",
|
|
370
|
+
requiresToken: true,
|
|
371
|
+
// 只读本地文件,不访问上游
|
|
372
|
+
requiresAccount: false,
|
|
373
|
+
},
|
|
374
|
+
"translate.tags": {
|
|
375
|
+
method: "GET",
|
|
376
|
+
path: "/api/translate/tags",
|
|
377
|
+
description: "整份标签翻译词库(原文 → 中文名)",
|
|
378
|
+
requiresToken: true,
|
|
379
|
+
requiresAccount: false,
|
|
380
|
+
},
|
|
381
|
+
"translate.update": {
|
|
382
|
+
method: "POST",
|
|
383
|
+
path: "/api/translate/update",
|
|
384
|
+
description: "从 EhTagTranslation/Database 的发布包下载并重建词库",
|
|
385
|
+
requiresToken: true,
|
|
386
|
+
// 下载的是 GitHub 的发布包,不是 E-Hentai,因此不要求登录
|
|
387
|
+
requiresAccount: false,
|
|
388
|
+
},
|
|
389
|
+
"translate.remove": {
|
|
390
|
+
method: "DELETE",
|
|
391
|
+
path: "/api/translate",
|
|
392
|
+
description: "删除本地词库,回到内置的常用翻译",
|
|
393
|
+
requiresToken: true,
|
|
394
|
+
requiresAccount: false,
|
|
395
|
+
},
|
|
396
|
+
// ── 播放列表 ───────────────────────────────────────────
|
|
397
|
+
"playlist.list": {
|
|
398
|
+
method: "GET",
|
|
399
|
+
path: "/api/playlist",
|
|
400
|
+
description: "用户播放列表与当前播放项",
|
|
401
|
+
requiresToken: true,
|
|
402
|
+
requiresAccount: false,
|
|
403
|
+
},
|
|
404
|
+
"playlist.add": {
|
|
405
|
+
method: "POST",
|
|
406
|
+
path: "/api/playlist",
|
|
407
|
+
description: "把一个画廊加入用户播放列表",
|
|
408
|
+
requiresToken: true,
|
|
409
|
+
requiresAccount: false,
|
|
410
|
+
},
|
|
411
|
+
"playlist.play": {
|
|
412
|
+
method: "POST",
|
|
413
|
+
path: "/api/playlist/:key/play",
|
|
414
|
+
description: "把某一项设为当前播放项",
|
|
415
|
+
requiresToken: true,
|
|
416
|
+
requiresAccount: false,
|
|
417
|
+
},
|
|
418
|
+
"playlist.progress": {
|
|
419
|
+
method: "PATCH",
|
|
420
|
+
path: "/api/playlist/:key",
|
|
421
|
+
description: "写回某个画廊的播放进度",
|
|
422
|
+
requiresToken: true,
|
|
423
|
+
requiresAccount: false,
|
|
424
|
+
},
|
|
425
|
+
"playlist.remove": {
|
|
426
|
+
method: "DELETE",
|
|
427
|
+
path: "/api/playlist/:key",
|
|
428
|
+
description: "从播放列表移除一项",
|
|
429
|
+
requiresToken: true,
|
|
430
|
+
requiresAccount: false,
|
|
431
|
+
},
|
|
432
|
+
"playlist.clear": {
|
|
433
|
+
method: "DELETE",
|
|
434
|
+
path: "/api/playlist",
|
|
435
|
+
description: "清空播放列表",
|
|
436
|
+
requiresToken: true,
|
|
437
|
+
requiresAccount: false,
|
|
438
|
+
},
|
|
439
|
+
// ── 下载 ───────────────────────────────────────────────
|
|
440
|
+
"downloads.list": {
|
|
441
|
+
method: "GET",
|
|
442
|
+
path: "/api/downloads",
|
|
443
|
+
description: "下载任务列表",
|
|
444
|
+
requiresToken: true,
|
|
445
|
+
requiresAccount: false,
|
|
446
|
+
},
|
|
447
|
+
"downloads.create": {
|
|
448
|
+
method: "POST",
|
|
449
|
+
path: "/api/downloads",
|
|
450
|
+
description: "新建归档下载任务",
|
|
451
|
+
requiresToken: true,
|
|
452
|
+
requiresAccount: true,
|
|
453
|
+
},
|
|
454
|
+
"downloads.retry": {
|
|
455
|
+
method: "POST",
|
|
456
|
+
path: "/api/downloads/:taskId/retry",
|
|
457
|
+
description: "重新排队失败或已取消的下载任务",
|
|
458
|
+
requiresToken: true,
|
|
459
|
+
requiresAccount: false,
|
|
460
|
+
},
|
|
461
|
+
"downloads.cancel": {
|
|
462
|
+
method: "DELETE",
|
|
463
|
+
path: "/api/downloads/:taskId",
|
|
464
|
+
description: "取消或移出下载任务",
|
|
465
|
+
requiresToken: true,
|
|
466
|
+
requiresAccount: false,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* 填充 :name 占位符。缺少参数时抛出异常,避免生成无效 URL
|
|
471
|
+
*/
|
|
472
|
+
export function fillPath(template, params = {}) {
|
|
473
|
+
return template.replace(/:([A-Za-z0-9_]+)/g, (_match, name) => {
|
|
474
|
+
const value = params[name];
|
|
475
|
+
if (value === undefined) {
|
|
476
|
+
throw new Error(`缺少路径参数:${name}(模板 ${template})`);
|
|
477
|
+
}
|
|
478
|
+
return encodeURIComponent(String(value));
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* 构造查询串
|
|
483
|
+
* undefined / null / 空字符串跳过,可选字段可直接传入
|
|
484
|
+
* 数组以逗号连接(多值参数统一约定,如 categories),布尔输出 1 / 0
|
|
485
|
+
* 空格编码为 %20 而非 +,保证分类名原样传递给上游
|
|
486
|
+
*/
|
|
487
|
+
export function buildQueryString(params) {
|
|
488
|
+
const search = new URLSearchParams();
|
|
489
|
+
for (const [key, value] of Object.entries(params)) {
|
|
490
|
+
if (value === undefined || value === null || value === "") {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (Array.isArray(value)) {
|
|
494
|
+
if (value.length === 0) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
search.set(key, value.map((item) => String(item)).join(","));
|
|
498
|
+
}
|
|
499
|
+
else if (typeof value === "boolean") {
|
|
500
|
+
search.set(key, value ? "1" : "0");
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
search.set(key, String(value));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const query = search.toString().replaceAll("+", "%20");
|
|
507
|
+
return query === "" ? "" : `?${query}`;
|
|
508
|
+
}
|
|
509
|
+
/** 取路由路径;无路径参数的路由无需第二个实参,由条件类型约束 */
|
|
510
|
+
export function routePath(name, ...args) {
|
|
511
|
+
const params = args[0];
|
|
512
|
+
return fillPath(ROUTES[name].path, (params ?? {}));
|
|
513
|
+
}
|
|
514
|
+
/** 存在缺失时此行编译报错,错误信息包含路由名与参数名 */
|
|
515
|
+
const PATH_PARAMS_COVERAGE = true;
|
|
516
|
+
void PATH_PARAMS_COVERAGE;
|