Files
MoonTVPlus/src/lib/openlist.client.ts
2026-01-23 16:41:49 +08:00

344 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable @typescript-eslint/no-explicit-any */
// Token 内存缓存
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
export interface OpenListFile {
name: string;
size: number;
is_dir: boolean;
modified: string;
sign?: string; // 临时下载签名
raw_url?: string; // 完整下载链接
thumb?: string;
type: number;
path?: string;
}
export interface OpenListListResponse {
code: number;
message: string;
data: {
content: OpenListFile[];
total: number;
readme: string;
write: boolean;
};
}
export interface OpenListGetResponse {
code: number;
message: string;
data: OpenListFile;
}
export class OpenListClient {
private token = '';
constructor(
private baseURL: string,
private username: string,
private password: string
) {}
/**
* 使用账号密码登录获取Token
*/
static async login(
baseURL: string,
username: string,
password: string
): Promise<string> {
const response = await fetch(`${baseURL}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
}),
});
if (!response.ok) {
throw new Error(`OpenList 登录失败: ${response.status}`);
}
const data = await response.json();
if (data.code !== 200 || !data.data?.token) {
throw new Error('OpenList 登录失败: 未获取到Token');
}
return data.data.token;
}
/**
* 获取缓存的 Token 或重新登录
*/
private async getToken(): Promise<string> {
const cacheKey = `${this.baseURL}:${this.username}`;
const cached = tokenCache.get(cacheKey);
// 如果有缓存且未过期,直接返回
if (cached && cached.expiresAt > Date.now()) {
this.token = cached.token;
return this.token;
}
// 否则重新登录
console.log('[OpenListClient] Token 不存在或已过期,重新登录');
this.token = await OpenListClient.login(
this.baseURL,
this.username,
this.password
);
// 缓存 Token设置 1 小时过期
tokenCache.set(cacheKey, {
token: this.token,
expiresAt: Date.now() + 60 * 60 * 1000,
});
console.log('[OpenListClient] 登录成功Token 已缓存');
return this.token;
}
/**
* 清除 Token 缓存(当 Token 失效时调用)
*/
private clearTokenCache(): void {
const cacheKey = `${this.baseURL}:${this.username}`;
tokenCache.delete(cacheKey);
console.log('[OpenListClient] Token 缓存已清除');
}
/**
* 执行请求如果401则清除缓存并重新登录后重试
*/
private async fetchWithRetry(
url: string,
options: RequestInit,
retried = false
): Promise<Response> {
// 获取 Token
const token = await this.getToken();
// 更新请求头中的 Token
const requestOptions = {
...options,
headers: {
...options.headers,
Authorization: token,
},
};
const response = await fetch(url, requestOptions);
// 检查 HTTP status 401
if (response.status === 401 && !retried) {
console.log('[OpenListClient] 收到 HTTP 401清除 Token 缓存并重试');
this.clearTokenCache();
return this.fetchWithRetry(url, options, true);
}
// 检查响应体中的 code 字段OpenList 的 Token 过期时 HTTP status 是 200但 code 是 401
if (response.ok && !retried) {
try {
// 克隆响应以便读取 JSON
const clonedResponse = response.clone();
const data = await clonedResponse.json();
if (data.code === 401) {
console.log('[OpenListClient] 响应体 code 为 401Token 已过期,清除缓存并重试');
this.clearTokenCache();
return this.fetchWithRetry(url, options, true);
}
} catch (error) {
// 如果解析 JSON 失败,忽略错误,返回原始响应
console.warn('[OpenListClient] 解析响应 JSON 失败:', error);
}
}
return response;
}
private async getHeaders() {
const token = await this.getToken();
return {
Authorization: token, // 不带 bearer
'Content-Type': 'application/json',
};
}
// 列出目录
async listDirectory(
path: string,
page = 1,
perPage = 100,
refresh = false
): Promise<OpenListListResponse> {
const response = await this.fetchWithRetry(`${this.baseURL}/api/fs/list`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify({
path,
password: '',
refresh,
page,
per_page: perPage,
}),
});
if (!response.ok) {
throw new Error(`OpenList API 错误: ${response.status}`);
}
return response.json();
}
// 获取文件信息
async getFile(path: string): Promise<OpenListGetResponse> {
const response = await this.fetchWithRetry(`${this.baseURL}/api/fs/get`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify({
path,
password: '',
}),
});
if (!response.ok) {
throw new Error(`OpenList API 错误: ${response.status}`);
}
return response.json();
}
// 上传文件
async uploadFile(path: string, content: string): Promise<void> {
const token = await this.getToken();
const response = await this.fetchWithRetry(`${this.baseURL}/api/fs/put`, {
method: 'PUT',
headers: {
Authorization: token,
'Content-Type': 'text/plain; charset=utf-8',
'File-Path': encodeURIComponent(path),
'As-Task': 'false',
},
body: content,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`OpenList 上传失败: ${response.status} - ${errorText}`);
}
// 上传成功后刷新目录缓存
const dir = path.substring(0, path.lastIndexOf('/')) || '/';
await this.refreshDirectory(dir);
}
// 刷新目录缓存
async refreshDirectory(path: string): Promise<void> {
try {
const response = await this.fetchWithRetry(`${this.baseURL}/api/fs/list`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify({
path,
password: '',
refresh: true,
page: 1,
per_page: 1,
}),
});
if (!response.ok) {
console.warn(`刷新目录缓存失败: ${response.status}`);
}
} catch (error) {
console.warn('刷新目录缓存失败:', error);
}
}
// 删除文件
async deleteFile(path: string): Promise<void> {
const dir = path.substring(0, path.lastIndexOf('/')) || '/';
const fileName = path.substring(path.lastIndexOf('/') + 1);
const response = await this.fetchWithRetry(`${this.baseURL}/api/fs/remove`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify({
names: [fileName],
dir: dir,
}),
});
if (!response.ok) {
throw new Error(`OpenList 删除失败: ${response.status}`);
}
}
// 获取视频预览流
async getVideoPreview(path: string): Promise<any> {
const response = await this.fetchWithRetry(`${this.baseURL}/api/fs/other`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify({
path: path,
method: 'video_preview',
password: '',
}),
});
if (!response.ok) {
throw new Error(`视频预览请求失败: ${response.status}`);
}
const data = await response.json();
if (data.code !== 200) {
throw new Error(`视频预览失败: ${data.message}`);
}
return data;
}
// 检查连通性
async checkConnectivity(): Promise<{ success: boolean; message: string }> {
try {
const response = await this.fetchWithRetry(`${this.baseURL}/api/me`, {
method: 'GET',
headers: await this.getHeaders(),
});
if (response.status !== 200) {
return {
success: false,
message: `HTTP 状态码错误: ${response.status}`,
};
}
const data = await response.json();
if (data.code !== 200) {
return {
success: false,
message: `响应码错误: ${data.code}`,
};
}
return {
success: true,
message: '连接成功',
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : '连接失败',
};
}
}
}