OrangeTV/src/lib/douban.client.ts

480 lines
13 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any,no-console,no-case-declarations */
import { DoubanItem, DoubanResult } from './types';
interface DoubanCategoriesParams {
kind: 'tv' | 'movie';
category: string;
type: string;
pageLimit?: number;
pageStart?: number;
}
interface DoubanCategoryApiResponse {
total: number;
items: Array<{
id: string;
title: string;
card_subtitle: string;
pic: {
large: string;
normal: string;
};
rating: {
value: number;
};
}>;
}
interface DoubanListApiResponse {
total: number;
subjects: Array<{
id: string;
title: string;
card_subtitle: string;
cover: string;
rate: string;
}>;
}
interface DoubanRecommendApiResponse {
total: number;
items: Array<{
id: string;
title: string;
year: string;
type: string;
pic: {
large: string;
normal: string;
};
rating: {
value: number;
};
}>;
}
/**
* 带超时的 fetch 请求
*/
async function fetchWithTimeout(
url: string,
proxyUrl: string
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
// 检查是否使用代理
const finalUrl =
proxyUrl === 'https://cors-anywhere.com/'
? `${proxyUrl}${url}`
: proxyUrl
? `${proxyUrl}${encodeURIComponent(url)}`
: url;
const fetchOptions: RequestInit = {
signal: controller.signal,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
Referer: 'https://movie.douban.com/',
Accept: 'application/json, text/plain, */*',
},
};
try {
const response = await fetch(finalUrl, fetchOptions);
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
function getDoubanProxyConfig(): {
proxyType:
| 'direct'
| 'cors-proxy-zwei'
| 'cmliussss-cdn-tencent'
| 'cmliussss-cdn-ali'
| 'cors-anywhere'
| 'custom';
proxyUrl: string;
} {
const doubanProxyType =
localStorage.getItem('doubanDataSource') ||
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE ||
'cmliussss-cdn-tencent';
const doubanProxy =
localStorage.getItem('doubanProxyUrl') ||
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY ||
'';
return {
proxyType: doubanProxyType,
proxyUrl: doubanProxy,
};
}
/**
* 浏览器端豆瓣分类数据获取函数
*/
export async function fetchDoubanCategories(
params: DoubanCategoriesParams,
proxyUrl: string,
useTencentCDN = false,
useAliCDN = false
): Promise<DoubanResult> {
const { kind, category, type, pageLimit = 20, pageStart = 0 } = params;
// 验证参数
if (!['tv', 'movie'].includes(kind)) {
throw new Error('kind 参数必须是 tv 或 movie');
}
if (!category || !type) {
throw new Error('category 和 type 参数不能为空');
}
if (pageLimit < 1 || pageLimit > 100) {
throw new Error('pageLimit 必须在 1-100 之间');
}
if (pageStart < 0) {
throw new Error('pageStart 不能小于 0');
}
const target = useTencentCDN
? `https://m.douban.cmliussss.net/rexxar/api/v2/subject/recent_hot/${kind}?start=${pageStart}&limit=${pageLimit}&category=${category}&type=${type}`
: useAliCDN
? `https://m.douban.cmliussss.com/rexxar/api/v2/subject/recent_hot/${kind}?start=${pageStart}&limit=${pageLimit}&category=${category}&type=${type}`
: `https://m.douban.com/rexxar/api/v2/subject/recent_hot/${kind}?start=${pageStart}&limit=${pageLimit}&category=${category}&type=${type}`;
try {
const response = await fetchWithTimeout(
target,
useTencentCDN || useAliCDN ? '' : proxyUrl
);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const doubanData: DoubanCategoryApiResponse = await response.json();
// 转换数据格式
const list: DoubanItem[] = doubanData.items.map((item) => ({
id: item.id,
title: item.title,
poster: item.pic?.normal || item.pic?.large || '',
rate: item.rating?.value ? item.rating.value.toFixed(1) : '',
year: item.card_subtitle?.match(/(\d{4})/)?.[1] || '',
}));
return {
code: 200,
message: '获取成功',
list: list,
};
} catch (error) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣分类数据失败' },
})
);
}
throw new Error(`获取豆瓣分类数据失败: ${(error as Error).message}`);
}
}
/**
* 统一的豆瓣分类数据获取函数,根据代理设置选择使用服务端 API 或客户端代理获取
*/
export async function getDoubanCategories(
params: DoubanCategoriesParams
): Promise<DoubanResult> {
const { kind, category, type, pageLimit = 20, pageStart = 0 } = params;
const { proxyType, proxyUrl } = getDoubanProxyConfig();
switch (proxyType) {
case 'cors-proxy-zwei':
return fetchDoubanCategories(params, 'https://ciao-cors.is-an.org/');
case 'cmliussss-cdn-tencent':
return fetchDoubanCategories(params, '', true, false);
case 'cmliussss-cdn-ali':
return fetchDoubanCategories(params, '', false, true);
case 'cors-anywhere':
return fetchDoubanCategories(params, 'https://cors-anywhere.com/');
case 'custom':
return fetchDoubanCategories(params, proxyUrl);
case 'direct':
default:
const response = await fetch(
`/api/douban/categories?kind=${kind}&category=${category}&type=${type}&limit=${pageLimit}&start=${pageStart}`
);
return response.json();
}
}
interface DoubanListParams {
tag: string;
type: string;
pageLimit?: number;
pageStart?: number;
}
export async function getDoubanList(
params: DoubanListParams
): Promise<DoubanResult> {
const { tag, type, pageLimit = 20, pageStart = 0 } = params;
const { proxyType, proxyUrl } = getDoubanProxyConfig();
switch (proxyType) {
case 'cors-proxy-zwei':
return fetchDoubanList(params, 'https://ciao-cors.is-an.org/');
case 'cmliussss-cdn-tencent':
return fetchDoubanList(params, '', true, false);
case 'cmliussss-cdn-ali':
return fetchDoubanList(params, '', false, true);
case 'cors-anywhere':
return fetchDoubanList(params, 'https://cors-anywhere.com/');
case 'custom':
return fetchDoubanList(params, proxyUrl);
case 'direct':
default:
const response = await fetch(
`/api/douban?tag=${tag}&type=${type}&pageSize=${pageLimit}&pageStart=${pageStart}`
);
return response.json();
}
}
export async function fetchDoubanList(
params: DoubanListParams,
proxyUrl: string,
useTencentCDN = false,
useAliCDN = false
): Promise<DoubanResult> {
const { tag, type, pageLimit = 20, pageStart = 0 } = params;
// 验证参数
if (!tag || !type) {
throw new Error('tag 和 type 参数不能为空');
}
if (!['tv', 'movie'].includes(type)) {
throw new Error('type 参数必须是 tv 或 movie');
}
if (pageLimit < 1 || pageLimit > 100) {
throw new Error('pageLimit 必须在 1-100 之间');
}
if (pageStart < 0) {
throw new Error('pageStart 不能小于 0');
}
const target = useTencentCDN
? `https://movie.douban.cmliussss.net/j/search_subjects?type=${type}&tag=${tag}&sort=recommend&page_limit=${pageLimit}&page_start=${pageStart}`
: useAliCDN
? `https://movie.douban.cmliussss.com/j/search_subjects?type=${type}&tag=${tag}&sort=recommend&page_limit=${pageLimit}&page_start=${pageStart}`
: `https://movie.douban.com/j/search_subjects?type=${type}&tag=${tag}&sort=recommend&page_limit=${pageLimit}&page_start=${pageStart}`;
try {
const response = await fetchWithTimeout(
target,
useTencentCDN || useAliCDN ? '' : proxyUrl
);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const doubanData: DoubanListApiResponse = await response.json();
// 转换数据格式
const list: DoubanItem[] = doubanData.subjects.map((item) => ({
id: item.id,
title: item.title,
poster: item.cover,
rate: item.rate,
year: item.card_subtitle?.match(/(\d{4})/)?.[1] || '',
}));
return {
code: 200,
message: '获取成功',
list: list,
};
} catch (error) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣列表数据失败' },
})
);
}
throw new Error(`获取豆瓣分类数据失败: ${(error as Error).message}`);
}
}
interface DoubanRecommendsParams {
kind: 'tv' | 'movie';
pageLimit?: number;
pageStart?: number;
category?: string;
format?: string;
label?: string;
region?: string;
year?: string;
platform?: string;
sort?: string;
}
export async function getDoubanRecommends(
params: DoubanRecommendsParams
): Promise<DoubanResult> {
const {
kind,
pageLimit = 20,
pageStart = 0,
category,
format,
label,
region,
year,
platform,
sort,
} = params;
const { proxyType, proxyUrl } = getDoubanProxyConfig();
switch (proxyType) {
case 'cors-proxy-zwei':
return fetchDoubanRecommends(params, 'https://ciao-cors.is-an.org/');
case 'cmliussss-cdn-tencent':
return fetchDoubanRecommends(params, '', true, false);
case 'cmliussss-cdn-ali':
return fetchDoubanRecommends(params, '', false, true);
case 'cors-anywhere':
return fetchDoubanRecommends(params, 'https://cors-anywhere.com/');
case 'custom':
return fetchDoubanRecommends(params, proxyUrl);
case 'direct':
default:
const response = await fetch(
`/api/douban/recommends?kind=${kind}&limit=${pageLimit}&start=${pageStart}&category=${category}&format=${format}&region=${region}&year=${year}&platform=${platform}&sort=${sort}&label=${label}`
);
return response.json();
}
}
async function fetchDoubanRecommends(
params: DoubanRecommendsParams,
proxyUrl: string,
useTencentCDN = false,
useAliCDN = false
): Promise<DoubanResult> {
const { kind, pageLimit = 20, pageStart = 0 } = params;
let { category, format, region, year, platform, sort, label } = params;
if (category === 'all') {
category = '';
}
if (format === 'all') {
format = '';
}
if (label === 'all') {
label = '';
}
if (region === 'all') {
region = '';
}
if (year === 'all') {
year = '';
}
if (platform === 'all') {
platform = '';
}
if (sort === 'T') {
sort = '';
}
const selectedCategories = { 类型: category } as any;
if (format) {
selectedCategories['形式'] = format;
}
if (region) {
selectedCategories['地区'] = region;
}
const tags = [] as Array<string>;
if (category) {
tags.push(category);
}
if (!category && format) {
tags.push(format);
}
if (label) {
tags.push(label);
}
if (region) {
tags.push(region);
}
if (year) {
tags.push(year);
}
if (platform) {
tags.push(platform);
}
const baseUrl = useTencentCDN
? `https://m.douban.cmliussss.net/rexxar/api/v2/${kind}/recommend`
: useAliCDN
? `https://m.douban.cmliussss.com/rexxar/api/v2/${kind}/recommend`
: `https://m.douban.com/rexxar/api/v2/${kind}/recommend`;
const reqParams = new URLSearchParams();
reqParams.append('refresh', '0');
reqParams.append('start', pageStart.toString());
reqParams.append('count', pageLimit.toString());
reqParams.append('selected_categories', JSON.stringify(selectedCategories));
reqParams.append('uncollect', 'false');
reqParams.append('score_range', '0,10');
reqParams.append('tags', tags.join(','));
if (sort) {
reqParams.append('sort', sort);
}
const target = `${baseUrl}?${reqParams.toString()}`;
console.log(target);
try {
const response = await fetchWithTimeout(
target,
useTencentCDN || useAliCDN ? '' : proxyUrl
);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const doubanData: DoubanRecommendApiResponse = await response.json();
const list: DoubanItem[] = doubanData.items
.filter((item) => item.type == 'movie' || item.type == 'tv')
.map((item) => ({
id: item.id,
title: item.title,
poster: item.pic?.normal || item.pic?.large || '',
rate: item.rating?.value ? item.rating.value.toFixed(1) : '',
year: item.year,
}));
return {
code: 200,
message: '获取成功',
list: list,
};
} catch (error) {
throw new Error(`获取豆瓣推荐数据失败: ${(error as Error).message}`);
}
}