豆瓣数据源增加备用源

This commit is contained in:
mtvpls
2026-04-09 21:30:07 +08:00
parent 204eec782f
commit 769dbf8310
11 changed files with 778 additions and 202 deletions
+244 -98
View File
@@ -88,6 +88,36 @@ interface DoubanDetailApiResponse {
[key: string]: any; // 允许其他字段
}
type DoubanProxyType =
| 'direct'
| 'cors-proxy-zwei'
| 'cmliussss-cdn-tencent'
| 'cmliussss-cdn-ali'
| 'cors-anywhere'
| 'custom';
function normalizeDoubanProxyConfig(
proxyType: DoubanProxyType,
proxyUrl: string
): {
proxyType: DoubanProxyType;
proxyUrl: string;
} {
const normalizedProxyUrl = proxyUrl.trim();
if (proxyType === 'custom' && !normalizedProxyUrl) {
return {
proxyType: 'direct',
proxyUrl: '',
};
}
return {
proxyType,
proxyUrl: normalizedProxyUrl,
};
}
/**
* 带超时的 fetch 请求
*/
@@ -135,6 +165,14 @@ function getDoubanProxyConfig(): {
| 'cors-anywhere'
| 'custom';
proxyUrl: string;
backupProxyType:
| 'direct'
| 'cors-proxy-zwei'
| 'cmliussss-cdn-tencent'
| 'cmliussss-cdn-ali'
| 'cors-anywhere'
| 'custom';
backupProxyUrl: string;
} {
const doubanProxyType =
localStorage.getItem('doubanDataSource') ||
@@ -144,12 +182,115 @@ function getDoubanProxyConfig(): {
localStorage.getItem('doubanProxyUrl') ||
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY ||
'';
const doubanProxyBackupType =
(localStorage.getItem('doubanDataSourceBackup') as DoubanProxyType | null) ||
'direct';
const doubanProxyBackupUrl =
localStorage.getItem('doubanProxyUrlBackup') || '';
const primaryConfig = normalizeDoubanProxyConfig(doubanProxyType, doubanProxy);
const backupConfig = normalizeDoubanProxyConfig(
doubanProxyBackupType,
doubanProxyBackupUrl
);
return {
proxyType: doubanProxyType,
proxyUrl: doubanProxy,
proxyType: primaryConfig.proxyType,
proxyUrl: primaryConfig.proxyUrl,
backupProxyType: backupConfig.proxyType,
backupProxyUrl: backupConfig.proxyUrl,
};
}
function buildDoubanRequester(
proxyType: DoubanProxyType,
proxyUrl: string
): {
useDirectApi: boolean;
requestProxyUrl: string;
useTencentCDN: boolean;
useAliCDN: boolean;
} {
switch (proxyType) {
case 'cors-proxy-zwei':
return {
useDirectApi: false,
requestProxyUrl: 'https://ciao-cors.is-an.org/',
useTencentCDN: false,
useAliCDN: false,
};
case 'cmliussss-cdn-tencent':
return {
useDirectApi: false,
requestProxyUrl: '',
useTencentCDN: true,
useAliCDN: false,
};
case 'cmliussss-cdn-ali':
return {
useDirectApi: false,
requestProxyUrl: '',
useTencentCDN: false,
useAliCDN: true,
};
case 'cors-anywhere':
return {
useDirectApi: false,
requestProxyUrl: 'https://cors-anywhere.com/',
useTencentCDN: false,
useAliCDN: false,
};
case 'custom':
return {
useDirectApi: false,
requestProxyUrl: proxyUrl,
useTencentCDN: false,
useAliCDN: false,
};
case 'direct':
default:
return {
useDirectApi: true,
requestProxyUrl: '',
useTencentCDN: false,
useAliCDN: false,
};
}
}
async function requestDoubanWithFallback<T>(
primary: { proxyType: DoubanProxyType; proxyUrl: string },
backup: { proxyType: DoubanProxyType; proxyUrl: string },
runner: (requester: ReturnType<typeof buildDoubanRequester>) => Promise<T>
): Promise<T> {
const primaryRequester = buildDoubanRequester(primary.proxyType, primary.proxyUrl);
const backupRequester = buildDoubanRequester(backup.proxyType, backup.proxyUrl);
try {
return await runner(primaryRequester);
} catch (primaryError) {
const sameStrategy =
primary.proxyType === backup.proxyType && primary.proxyUrl === backup.proxyUrl;
if (sameStrategy) {
throw primaryError;
}
console.warn(
`[Douban] 主渠道失败,切换备用渠道: ${primary.proxyType} -> ${backup.proxyType}`,
primaryError
);
return runner(backupRequester);
}
}
function dispatchDoubanGlobalError(message: string) {
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message },
})
);
}
}
/**
* 浏览器端豆瓣分类数据获取函数
*/
@@ -211,14 +352,6 @@ export async function fetchDoubanCategories(
list: list,
};
} catch (error) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣分类数据失败' },
})
);
}
throw new Error(`获取豆瓣分类数据失败: ${(error as Error).message}`);
}
}
@@ -230,25 +363,34 @@ 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}`
);
const { proxyType, proxyUrl, backupProxyType, backupProxyUrl } =
getDoubanProxyConfig();
try {
return await requestDoubanWithFallback(
{ proxyType, proxyUrl },
{ proxyType: backupProxyType, proxyUrl: backupProxyUrl },
async ({ useDirectApi, requestProxyUrl, useTencentCDN, useAliCDN }) => {
if (useDirectApi) {
const response = await fetch(
`/api/douban/categories?kind=${kind}&category=${category}&type=${type}&limit=${pageLimit}&start=${pageStart}`
);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
}
return response.json();
return fetchDoubanCategories(
params,
requestProxyUrl,
useTencentCDN,
useAliCDN
);
}
);
} catch (error) {
dispatchDoubanGlobalError('获取豆瓣分类数据失败');
throw error;
}
}
@@ -263,25 +405,34 @@ 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}`
);
const { proxyType, proxyUrl, backupProxyType, backupProxyUrl } =
getDoubanProxyConfig();
try {
return await requestDoubanWithFallback(
{ proxyType, proxyUrl },
{ proxyType: backupProxyType, proxyUrl: backupProxyUrl },
async ({ useDirectApi, requestProxyUrl, useTencentCDN, useAliCDN }) => {
if (useDirectApi) {
const response = await fetch(
`/api/douban?tag=${tag}&type=${type}&pageSize=${pageLimit}&pageStart=${pageStart}`
);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
}
return response.json();
return fetchDoubanList(
params,
requestProxyUrl,
useTencentCDN,
useAliCDN
);
}
);
} catch (error) {
dispatchDoubanGlobalError('获取豆瓣列表数据失败');
throw error;
}
}
@@ -343,14 +494,6 @@ export async function fetchDoubanList(
list: list,
};
} catch (error) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣列表数据失败' },
})
);
}
throw new Error(`获取豆瓣分类数据失败: ${(error as Error).message}`);
}
}
@@ -383,25 +526,34 @@ export async function getDoubanRecommends(
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}`
);
const { proxyType, proxyUrl, backupProxyType, backupProxyUrl } =
getDoubanProxyConfig();
try {
return await requestDoubanWithFallback(
{ proxyType, proxyUrl },
{ proxyType: backupProxyType, proxyUrl: backupProxyUrl },
async ({ useDirectApi, requestProxyUrl, useTencentCDN, useAliCDN }) => {
if (useDirectApi) {
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}`
);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
}
return response.json();
return fetchDoubanRecommends(
params,
requestProxyUrl,
useTencentCDN,
useAliCDN
);
}
);
} catch (error) {
dispatchDoubanGlobalError('获取豆瓣推荐数据失败');
throw error;
}
}
@@ -544,14 +696,6 @@ export async function fetchDoubanDetail(
const doubanData: DoubanDetailApiResponse = await response.json();
return doubanData;
} catch (error) {
// 触发全局错误提示
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('globalError', {
detail: { message: '获取豆瓣详情数据失败' },
})
);
}
throw new Error(`获取豆瓣详情数据失败: ${(error as Error).message}`);
}
}
@@ -562,24 +706,26 @@ export async function fetchDoubanDetail(
export async function getDoubanDetail(
id: string
): Promise<DoubanDetailApiResponse> {
const { proxyType, proxyUrl } = getDoubanProxyConfig();
switch (proxyType) {
case 'cors-proxy-zwei':
return fetchDoubanDetail(id, 'https://ciao-cors.is-an.org/');
case 'cmliussss-cdn-tencent':
return fetchDoubanDetail(id, '', true, false);
case 'cmliussss-cdn-ali':
return fetchDoubanDetail(id, '', false, true);
case 'cors-anywhere':
return fetchDoubanDetail(id, 'https://cors-anywhere.com/');
case 'custom':
return fetchDoubanDetail(id, proxyUrl);
case 'direct':
default:
const response = await fetch(`/api/douban/detail?id=${id}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
const { proxyType, proxyUrl, backupProxyType, backupProxyUrl } =
getDoubanProxyConfig();
try {
return await requestDoubanWithFallback(
{ proxyType, proxyUrl },
{ proxyType: backupProxyType, proxyUrl: backupProxyUrl },
async ({ useDirectApi, requestProxyUrl, useTencentCDN, useAliCDN }) => {
if (useDirectApi) {
const response = await fetch(`/api/douban/detail?id=${id}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
}
return fetchDoubanDetail(id, requestProxyUrl, useTencentCDN, useAliCDN);
}
return response.json();
);
} catch (error) {
dispatchDoubanGlobalError('获取豆瓣详情数据失败');
throw error;
}
}