mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 04:16:38 +08:00
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import { LONG_REQUEST_TIMEOUT, api } from './client'
|
||
|
||
// 阅读子系统 API(/api/reader/*),字段与后端 model/reader.go 对齐。
|
||
|
||
export interface ReaderSearchOrigin {
|
||
source_id: string
|
||
origin: string
|
||
origin_name: string
|
||
origin_type: number
|
||
book_url: string
|
||
latest_chapter: string
|
||
}
|
||
|
||
export interface ReaderSearchBook {
|
||
name: string
|
||
author: string
|
||
kind: string
|
||
word_count: string
|
||
latest_chapter: string
|
||
intro: string
|
||
cover_url: string
|
||
book_url: string
|
||
origins: ReaderSearchOrigin[]
|
||
}
|
||
|
||
export interface ReaderSearchSkipped {
|
||
source_id: string
|
||
origin_name: string
|
||
reason: string
|
||
}
|
||
|
||
export interface ReaderSource {
|
||
id: string
|
||
name: string
|
||
group: string
|
||
type: number
|
||
source_url: string
|
||
enabled: boolean
|
||
enabled_explore: boolean
|
||
custom_order: number
|
||
weight: number
|
||
comment: string
|
||
last_update_time: number
|
||
respond_time: number
|
||
// 书源声明了登录能力(loginUrl / loginUi)
|
||
has_login: boolean
|
||
}
|
||
|
||
export interface ReaderBook {
|
||
id: string
|
||
origin: string
|
||
origin_name: string
|
||
book_url: string
|
||
toc_url: string
|
||
name: string
|
||
author: string
|
||
kind: string
|
||
cover_url: string
|
||
intro: string
|
||
type: number
|
||
latest_chapter_title: string
|
||
total_chapter_num: number
|
||
dur_chapter_index: number
|
||
dur_chapter_pos: number
|
||
dur_chapter_title: string
|
||
dur_chapter_time: number
|
||
order: number
|
||
/** 本地导入书籍(TXT / EPUB / 有声书目录),正文在服务端,不走书源。 */
|
||
is_local: boolean
|
||
/** 原地引用服务器上已有的文件/目录:移出书架只解除引用,不删除源文件。 */
|
||
local_external: boolean
|
||
/** 听书跳过片头/片尾秒数(0 表示不跳过,对应 legado Book.openCredits/closeCredits)。 */
|
||
open_credits: number
|
||
close_credits: number
|
||
}
|
||
|
||
export interface ReaderBookInfo {
|
||
name: string
|
||
author: string
|
||
kind: string
|
||
word_count: string
|
||
latest_chapter: string
|
||
intro: string
|
||
cover_url: string
|
||
toc_url: string
|
||
book_url: string
|
||
}
|
||
|
||
export interface ReaderTocChapter {
|
||
index: number
|
||
title: string
|
||
url: string
|
||
is_volume: boolean
|
||
update_time: string
|
||
}
|
||
|
||
export interface ReaderChapter {
|
||
index: number
|
||
title: string
|
||
url: string
|
||
is_volume: boolean
|
||
}
|
||
|
||
export interface ReaderChapterContent {
|
||
type: 'text' | 'audio' | 'image'
|
||
content?: string
|
||
tracks?: string[]
|
||
images?: string[]
|
||
image_style?: string
|
||
/** 该音轨走了服务端转码(源格式浏览器解不了),首次播放需要等转码完成。 */
|
||
transcoding?: boolean
|
||
}
|
||
|
||
export interface ReaderReplaceRule {
|
||
id: string
|
||
name: string
|
||
group: string
|
||
pattern: string
|
||
replacement: string
|
||
scope: string
|
||
scope_title: boolean
|
||
scope_content: boolean
|
||
exclude_scope: string
|
||
is_enabled: boolean
|
||
is_regex: boolean
|
||
order: number
|
||
}
|
||
|
||
// ── 书源登录 ──
|
||
|
||
export interface ReaderLoginField {
|
||
name: string
|
||
type: 'text' | 'password' | 'button' | 'toggle' | 'select'
|
||
action?: string
|
||
chars?: string[]
|
||
default?: string
|
||
viewName?: string
|
||
style?: Record<string, unknown>
|
||
}
|
||
|
||
export interface ReaderSourceLogin {
|
||
source_id: string
|
||
source_name: string
|
||
has_login_js: boolean
|
||
login_js?: string
|
||
fields: ReaderLoginField[]
|
||
values: Record<string, string>
|
||
cookies: Record<string, string>
|
||
variable: string
|
||
variable_comment?: string
|
||
logged_in: boolean
|
||
error?: string
|
||
}
|
||
|
||
export interface ReaderBrowserRequest {
|
||
url: string
|
||
title: string
|
||
}
|
||
|
||
/** 书源 JS 交给宿主浏览器承载的一个页面(java.startBrowser / startBrowserAwait)。 */
|
||
export interface ReaderBrowserPage {
|
||
id: string
|
||
title: string
|
||
/** wait = 需要回传 DOM(点 √);open = 仅展示。 */
|
||
mode: 'wait' | 'open'
|
||
/** iframe 承载地址(同源,带签名)。 */
|
||
page_url: string
|
||
refetch: boolean
|
||
source_id: string
|
||
target_url?: string
|
||
}
|
||
|
||
export interface ReaderLoginResult {
|
||
ok: boolean
|
||
error?: string
|
||
toasts?: string[]
|
||
browsers?: ReaderBrowserRequest[]
|
||
/** 书源要求重新渲染登录表单(java.reLoginView / refreshExplore / upLoginData)。 */
|
||
ui_refresh?: boolean
|
||
values: Record<string, string>
|
||
cookies: Record<string, string>
|
||
logged_in: boolean
|
||
}
|
||
|
||
const longOpts = { timeout: LONG_REQUEST_TIMEOUT } as const
|
||
|
||
// 登录动作可能阻塞等待用户在页面里操作(java.startBrowserAwait),
|
||
// 服务端上限 10 分钟,这里留出余量,避免 axios 先超时把请求掐掉。
|
||
const browserWaitOpts = { timeout: 11 * 60_000 } as const
|
||
|
||
export const readerAPI = {
|
||
// ── 书源 ──
|
||
listSources: () => api.get<{ sources: ReaderSource[] }>('/reader/sources').then((r) => r.data.sources),
|
||
importSources: (text: string) =>
|
||
api.post<{ imported: number }>('/reader/sources/import', { text }, longOpts).then((r) => r.data.imported),
|
||
setSourceEnabled: (id: string, enabled: boolean) => api.patch(`/reader/sources/${id}`, { enabled }),
|
||
deleteSource: (id: string) => api.delete(`/reader/sources/${id}`),
|
||
debugSource: (id: string, key: string) =>
|
||
api.post<{ logs: string[] }>(`/reader/sources/${id}/debug`, { key }, longOpts).then((r) => r.data.logs),
|
||
|
||
// ── 书源登录与源变量 ──
|
||
// 取登录界面描述(loginUi 控件 + 已保存值 + 当前 Cookie 状态)
|
||
sourceLogin: (id: string) => api.get<ReaderSourceLogin>(`/reader/sources/${id}/login`).then((r) => r.data),
|
||
// 执行登录动作:action 为空表示执行 loginUrl 的 login()(确认登录)
|
||
runSourceLogin: (id: string, body: { action?: string; fields?: Record<string, string> }) =>
|
||
api.post<ReaderLoginResult>(`/reader/sources/${id}/login`, body, browserWaitOpts).then((r) => r.data),
|
||
// 书源 JS 的宿主浏览器:轮询待用户完成的页面(startBrowserAwait 会阻塞在服务端)
|
||
browserPending: (sourceId: string) =>
|
||
api
|
||
.get<{ pages: ReaderBrowserPage[] }>('/reader/browser/pending', { params: { source_id: sourceId } })
|
||
.then((r) => r.data.pages ?? []),
|
||
// 回传用户操作后的 DOM(或取消),解除服务端阻塞
|
||
submitBrowserResult: (body: { id: string; body?: string; url?: string; cancelled?: boolean }) =>
|
||
api.post('/reader/browser/result', body).then((r) => r.data),
|
||
// 承载页面内的接口请求转交服务端代发(iframe 是不透明源,带不上书源 Cookie)
|
||
browserXHR: (body: {
|
||
id: string
|
||
url: string
|
||
method?: string
|
||
headers?: Record<string, string>
|
||
body?: string
|
||
}) =>
|
||
api
|
||
.post<{ status: number; content_type: string; body: string; base64: boolean }>(
|
||
'/reader/browser/xhr',
|
||
body,
|
||
longOpts,
|
||
)
|
||
.then((r) => r.data),
|
||
// 仅保存表单值,不触发登录
|
||
saveSourceLoginInfo: (id: string, fields: Record<string, string>) =>
|
||
api.put(`/reader/sources/${id}/login-info`, { fields }),
|
||
logoutSource: (id: string) => api.delete(`/reader/sources/${id}/login`),
|
||
setSourceVariable: (id: string, variable: string) => api.put(`/reader/sources/${id}/variable`, { variable }),
|
||
|
||
// ── 搜索 / 详情 / 目录 / 正文 ──
|
||
search: (key: string) =>
|
||
api
|
||
.post<{ books: ReaderSearchBook[] | null; skipped: ReaderSearchSkipped[] | null }>(
|
||
'/reader/search',
|
||
{ key },
|
||
longOpts,
|
||
)
|
||
.then((r) => r.data),
|
||
bookInfo: (params: { source_id?: string; source_url?: string; book_url: string }) =>
|
||
api.get<ReaderBookInfo>('/reader/book-info', { params, timeout: LONG_REQUEST_TIMEOUT }).then((r) => r.data),
|
||
toc: (params: { source_id?: string; source_url?: string; book_url: string; toc_url: string }) =>
|
||
api
|
||
.get<{ chapters: ReaderTocChapter[] }>('/reader/toc', { params, timeout: LONG_REQUEST_TIMEOUT })
|
||
.then((r) => r.data.chapters),
|
||
content: (params: { source_id?: string; source_url?: string; book_url: string; chapter_url: string }) =>
|
||
api
|
||
.get<ReaderChapterContent>('/reader/content', { params, timeout: LONG_REQUEST_TIMEOUT })
|
||
.then((r) => r.data),
|
||
|
||
// ── 书架 ──
|
||
listBooks: () => api.get<{ books: ReaderBook[] }>('/reader/books').then((r) => r.data.books),
|
||
addBook: (body: { origin: ReaderSearchOrigin; name: string; author: string; cover_url: string }) =>
|
||
api.post<ReaderBook>('/reader/books', body).then((r) => r.data),
|
||
/** 上传本地书籍(TXT / EPUB),服务端解析目录后加入书架。 */
|
||
uploadLocalBook: (file: File, onProgress?: (percent: number) => void) =>
|
||
api
|
||
.post<ReaderBook>('/reader/local/books', (() => {
|
||
const form = new FormData()
|
||
form.append('file', file)
|
||
return form
|
||
})(), {
|
||
timeout: 0,
|
||
onUploadProgress: (e) => {
|
||
if (!onProgress || !e.total) return
|
||
onProgress(Math.round((e.loaded / e.total) * 100))
|
||
},
|
||
})
|
||
.then((r) => r.data),
|
||
/** 从服务器已有文件导入书籍(TXT / EPUB),原地引用不复制。仅管理员。 */
|
||
importLocalBookFromPath: (path: string) =>
|
||
api
|
||
.post<ReaderBook>('/reader/local/books/from-path', { path }, { timeout: LONG_REQUEST_TIMEOUT })
|
||
.then((r) => r.data),
|
||
/** 把服务器上的一本目录导入为有声书(音频文件 + .strm 播放指针)。仅管理员。 */
|
||
importLocalAudioDir: (path: string) =>
|
||
api
|
||
.post<ReaderBook>('/reader/local/audiobooks', { path }, { timeout: LONG_REQUEST_TIMEOUT })
|
||
.then((r) => r.data),
|
||
removeBook: (id: string) => api.delete(`/reader/books/${id}`),
|
||
saveProgress: (id: string, body: { chapter_index: number; pos: number; chapter_title: string }) =>
|
||
api.put(`/reader/books/${id}/progress`, body),
|
||
// 听书跳过片头/片尾(秒,0 不跳过)
|
||
saveAudioConfig: (id: string, body: { open_credits: number; close_credits: number }) =>
|
||
api.put(`/reader/books/${id}/audio-config`, body),
|
||
listChapters: (id: string) =>
|
||
api.get<{ chapters: ReaderChapter[] }>(`/reader/books/${id}/chapters`).then((r) => r.data.chapters),
|
||
saveChapters: (id: string, chapters: ReaderChapter[]) => api.post(`/reader/books/${id}/chapters`, { chapters }),
|
||
// 书架维度正文(服务端已应用书源 replaceRegex 与用户替换净化规则)
|
||
bookContent: (id: string, chapter: number) =>
|
||
api
|
||
.get<ReaderChapterContent>(`/reader/books/${id}/content`, { params: { chapter }, timeout: LONG_REQUEST_TIMEOUT })
|
||
.then((r) => r.data),
|
||
|
||
// ── 替换净化规则 ──
|
||
listReplaceRules: () =>
|
||
api.get<{ rules: ReaderReplaceRule[] }>('/reader/replace-rules').then((r) => r.data.rules),
|
||
createReplaceRule: (body: ReplaceRuleInput) =>
|
||
api.post<ReaderReplaceRule>('/reader/replace-rules', body).then((r) => r.data),
|
||
updateReplaceRule: (id: string, body: ReplaceRuleInput) => api.patch(`/reader/replace-rules/${id}`, body),
|
||
deleteReplaceRule: (id: string) => api.delete(`/reader/replace-rules/${id}`),
|
||
}
|
||
|
||
export interface ReplaceRuleInput {
|
||
name: string
|
||
group: string
|
||
pattern: string
|
||
replacement: string
|
||
scope: string
|
||
scope_title: boolean
|
||
scope_content: boolean
|
||
exclude_scope: string
|
||
is_enabled: boolean
|
||
is_regex: boolean
|
||
timeout_millisecond: number
|
||
order: number
|
||
}
|