Files
MoonTVPlus/src/lib/xiaoya.client.ts
2026-01-15 23:48:13 +08:00

245 lines
5.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 XiaoyaFile {
name: string;
size: number;
is_dir: boolean;
modified: string;
}
export interface XiaoyaListResponse {
content: XiaoyaFile[];
total: number;
}
export class XiaoyaClient {
private token: string = '';
constructor(
private baseURL: string,
private username?: string,
private password?: string,
private configToken?: 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(`小雅登录失败: ${response.status}`);
}
const data = await response.json();
if (data.code !== 200 || !data.data?.token) {
throw new Error('小雅登录失败: 未获取到Token');
}
return data.data.token;
}
/**
* 获取缓存的 Token 或重新登录
*/
async getToken(): Promise<string> {
// 如果配置了 Token直接使用
if (this.configToken) {
return this.configToken;
}
// 如果没有配置用户名密码返回空字符串guest 模式)
if (!this.username || !this.password) {
return '';
}
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('[XiaoyaClient] Token 不存在或已过期,重新登录');
this.token = await XiaoyaClient.login(
this.baseURL,
this.username,
this.password
);
// 缓存 Token设置 1 小时过期
tokenCache.set(cacheKey, {
token: this.token,
expiresAt: Date.now() + 60 * 60 * 1000,
});
return this.token;
}
/**
* 获取基础 URL
*/
getBaseURL(): string {
return this.baseURL;
}
/**
* 列出目录内容
*/
async listDirectory(path: string, page = 1, perPage = 100, refresh = false): Promise<XiaoyaListResponse> {
const token = await this.getToken();
const response = await fetch(`${this.baseURL}/api/fs/list`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token,
},
body: JSON.stringify({
path,
page,
per_page: perPage,
refresh,
}),
});
if (!response.ok) {
throw new Error(`小雅列表获取失败: ${response.status}`);
}
const data = await response.json();
if (data.code !== 200) {
throw new Error(`小雅列表获取失败: ${data.message}`);
}
return {
content: data.data.content || [],
total: data.data.total || 0,
};
}
/**
* 搜索文件
*/
async search(keyword: string, page = 1, perPage = 100): Promise<XiaoyaListResponse> {
const token = await this.getToken();
const response = await fetch(`${this.baseURL}/api/fs/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token,
},
body: JSON.stringify({
parent: '/',
keywords: keyword,
scope: 1, // 递归搜索
page,
per_page: perPage,
}),
});
if (!response.ok) {
throw new Error(`小雅搜索失败: ${response.status}`);
}
const data = await response.json();
if (data.code !== 200) {
throw new Error(`小雅搜索失败: ${data.message}`);
}
return {
content: data.data.content || [],
total: data.data.total || 0,
};
}
/**
* 获取文件信息
*/
async getFileInfo(path: string): Promise<XiaoyaFile> {
const token = await this.getToken();
const response = await fetch(`${this.baseURL}/api/fs/get`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token,
},
body: JSON.stringify({
path,
}),
});
if (!response.ok) {
throw new Error(`小雅文件信息获取失败: ${response.status}`);
}
const data = await response.json();
if (data.code !== 200) {
throw new Error(`小雅文件信息获取失败: ${data.message}`);
}
return data.data;
}
/**
* 获取文件下载链接
*/
async getDownloadUrl(path: string): Promise<string> {
// Alist 的直接下载链接格式
return `${this.baseURL}/d${path}`;
}
/**
* 获取文件内容(用于读取 NFO 等文本文件)
*/
async getFileContent(path: string): Promise<string> {
const downloadUrl = await this.getDownloadUrl(path);
const response = await fetch(downloadUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
});
if (!response.ok) {
throw new Error(`文件读取失败: ${response.status}`);
}
return await response.text();
}
/**
* 检查文件是否存在
*/
async fileExists(path: string): Promise<boolean> {
try {
await this.getFileInfo(path);
return true;
} catch (error) {
return false;
}
}
}