Upload 14 files
Browse files- js/api.js +606 -0
- js/app.js +1358 -0
- js/config.js +277 -0
- js/customer_site.js +13 -0
- js/douban.js +788 -0
- js/index-page.js +80 -0
- js/password.js +253 -0
- js/player.js +1661 -0
- js/pwa-register.js +6 -0
- js/search.js +117 -0
- js/sha256.js +6 -0
- js/ui.js +965 -0
- js/version-check.js +187 -0
- js/watch.js +94 -0
js/api.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 改进的API请求处理函数
|
| 2 |
+
async function handleApiRequest(url) {
|
| 3 |
+
const customApi = url.searchParams.get('customApi') || '';
|
| 4 |
+
const customDetail = url.searchParams.get('customDetail') || '';
|
| 5 |
+
const source = url.searchParams.get('source') || 'heimuer';
|
| 6 |
+
|
| 7 |
+
try {
|
| 8 |
+
if (url.pathname === '/api/search') {
|
| 9 |
+
const searchQuery = url.searchParams.get('wd');
|
| 10 |
+
if (!searchQuery) {
|
| 11 |
+
throw new Error('缺少搜索参数');
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
// 验证API和source的有效性
|
| 15 |
+
if (source === 'custom' && !customApi) {
|
| 16 |
+
throw new Error('使用自定义API时必须提供API地址');
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
if (!API_SITES[source] && source !== 'custom') {
|
| 20 |
+
throw new Error('无效的API来源');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const apiUrl = customApi
|
| 24 |
+
? `${customApi}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`
|
| 25 |
+
: `${API_SITES[source].api}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
|
| 26 |
+
|
| 27 |
+
// 添加超时处理
|
| 28 |
+
const controller = new AbortController();
|
| 29 |
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
| 30 |
+
|
| 31 |
+
try {
|
| 32 |
+
const response = await fetch(PROXY_URL + encodeURIComponent(apiUrl), {
|
| 33 |
+
headers: API_CONFIG.search.headers,
|
| 34 |
+
signal: controller.signal
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
clearTimeout(timeoutId);
|
| 38 |
+
|
| 39 |
+
if (!response.ok) {
|
| 40 |
+
throw new Error(`API请求失败: ${response.status}`);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const data = await response.json();
|
| 44 |
+
|
| 45 |
+
// 检查JSON格式的有效性
|
| 46 |
+
if (!data || !Array.isArray(data.list)) {
|
| 47 |
+
throw new Error('API返回的数据格式无效');
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// 添加源信息到每个结果
|
| 51 |
+
data.list.forEach(item => {
|
| 52 |
+
item.source_name = source === 'custom' ? '自定义源' : API_SITES[source].name;
|
| 53 |
+
item.source_code = source;
|
| 54 |
+
// 对于自定义源,添加API URL信息
|
| 55 |
+
if (source === 'custom') {
|
| 56 |
+
item.api_url = customApi;
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
return JSON.stringify({
|
| 61 |
+
code: 200,
|
| 62 |
+
list: data.list || [],
|
| 63 |
+
});
|
| 64 |
+
} catch (fetchError) {
|
| 65 |
+
clearTimeout(timeoutId);
|
| 66 |
+
throw fetchError;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// 详情处理
|
| 71 |
+
if (url.pathname === '/api/detail') {
|
| 72 |
+
const id = url.searchParams.get('id');
|
| 73 |
+
const sourceCode = url.searchParams.get('source') || 'heimuer'; // 获取源代码
|
| 74 |
+
|
| 75 |
+
if (!id) {
|
| 76 |
+
throw new Error('缺少视频ID参数');
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// 验证ID格式 - 只允许数字和有限的特殊字符
|
| 80 |
+
if (!/^[\w-]+$/.test(id)) {
|
| 81 |
+
throw new Error('无效的视频ID格式');
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// 验证API和source的有效性
|
| 85 |
+
if (sourceCode === 'custom' && !customApi) {
|
| 86 |
+
throw new Error('使用自定义API时必须提供API地址');
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
if (!API_SITES[sourceCode] && sourceCode !== 'custom') {
|
| 90 |
+
throw new Error('无效的API来源');
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// 对于有detail参数的源,都使用特殊处理方式
|
| 94 |
+
if (sourceCode !== 'custom' && API_SITES[sourceCode].detail) {
|
| 95 |
+
return await handleSpecialSourceDetail(id, sourceCode);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// 如果是自定义API,并且传递了detail参数,尝试特殊处理
|
| 99 |
+
// 优先 customDetail
|
| 100 |
+
if (sourceCode === 'custom' && customDetail) {
|
| 101 |
+
return await handleCustomApiSpecialDetail(id, customDetail);
|
| 102 |
+
}
|
| 103 |
+
if (sourceCode === 'custom' && url.searchParams.get('useDetail') === 'true') {
|
| 104 |
+
return await handleCustomApiSpecialDetail(id, customApi);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const detailUrl = customApi
|
| 108 |
+
? `${customApi}${API_CONFIG.detail.path}${id}`
|
| 109 |
+
: `${API_SITES[sourceCode].api}${API_CONFIG.detail.path}${id}`;
|
| 110 |
+
|
| 111 |
+
// 添加超时处理
|
| 112 |
+
const controller = new AbortController();
|
| 113 |
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
| 114 |
+
|
| 115 |
+
try {
|
| 116 |
+
const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
|
| 117 |
+
headers: API_CONFIG.detail.headers,
|
| 118 |
+
signal: controller.signal
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
clearTimeout(timeoutId);
|
| 122 |
+
|
| 123 |
+
if (!response.ok) {
|
| 124 |
+
throw new Error(`详情请求失败: ${response.status}`);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// 解析JSON
|
| 128 |
+
const data = await response.json();
|
| 129 |
+
|
| 130 |
+
// 检查返回的数据是否有效
|
| 131 |
+
if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) {
|
| 132 |
+
throw new Error('获取到的详情内容无效');
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// 获取第一个匹配的视频详情
|
| 136 |
+
const videoDetail = data.list[0];
|
| 137 |
+
|
| 138 |
+
// 提取播放地址
|
| 139 |
+
let episodes = [];
|
| 140 |
+
|
| 141 |
+
if (videoDetail.vod_play_url) {
|
| 142 |
+
// 分割不同播放源
|
| 143 |
+
const playSources = videoDetail.vod_play_url.split('$$$');
|
| 144 |
+
|
| 145 |
+
// 提取第一个播放源的集数(通常为主要源)
|
| 146 |
+
if (playSources.length > 0) {
|
| 147 |
+
const mainSource = playSources[0];
|
| 148 |
+
const episodeList = mainSource.split('#');
|
| 149 |
+
|
| 150 |
+
// 从每个集数中提取URL
|
| 151 |
+
episodes = episodeList.map(ep => {
|
| 152 |
+
const parts = ep.split('$');
|
| 153 |
+
// 返回URL部分(通常是第二部分,如果有的话)
|
| 154 |
+
return parts.length > 1 ? parts[1] : '';
|
| 155 |
+
}).filter(url => url && (url.startsWith('http://') || url.startsWith('https://')));
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// 如果没有找到播放地址,尝试使用正则表达式查找m3u8链接
|
| 160 |
+
if (episodes.length === 0 && videoDetail.vod_content) {
|
| 161 |
+
const matches = videoDetail.vod_content.match(M3U8_PATTERN) || [];
|
| 162 |
+
episodes = matches.map(link => link.replace(/^\$/, ''));
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
return JSON.stringify({
|
| 166 |
+
code: 200,
|
| 167 |
+
episodes: episodes,
|
| 168 |
+
detailUrl: detailUrl,
|
| 169 |
+
videoInfo: {
|
| 170 |
+
title: videoDetail.vod_name,
|
| 171 |
+
cover: videoDetail.vod_pic,
|
| 172 |
+
desc: videoDetail.vod_content,
|
| 173 |
+
type: videoDetail.type_name,
|
| 174 |
+
year: videoDetail.vod_year,
|
| 175 |
+
area: videoDetail.vod_area,
|
| 176 |
+
director: videoDetail.vod_director,
|
| 177 |
+
actor: videoDetail.vod_actor,
|
| 178 |
+
remarks: videoDetail.vod_remarks,
|
| 179 |
+
// 添加源信息
|
| 180 |
+
source_name: sourceCode === 'custom' ? '自定义源' : API_SITES[sourceCode].name,
|
| 181 |
+
source_code: sourceCode
|
| 182 |
+
}
|
| 183 |
+
});
|
| 184 |
+
} catch (fetchError) {
|
| 185 |
+
clearTimeout(timeoutId);
|
| 186 |
+
throw fetchError;
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
throw new Error('未知的API路径');
|
| 191 |
+
} catch (error) {
|
| 192 |
+
console.error('API处理错误:', error);
|
| 193 |
+
return JSON.stringify({
|
| 194 |
+
code: 400,
|
| 195 |
+
msg: error.message || '请求处理失败',
|
| 196 |
+
list: [],
|
| 197 |
+
episodes: [],
|
| 198 |
+
});
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// 处理自定义API的特殊详情页
|
| 203 |
+
async function handleCustomApiSpecialDetail(id, customApi) {
|
| 204 |
+
try {
|
| 205 |
+
// 构建详情页URL
|
| 206 |
+
const detailUrl = `${customApi}/index.php/vod/detail/id/${id}.html`;
|
| 207 |
+
|
| 208 |
+
// 添加超时处理
|
| 209 |
+
const controller = new AbortController();
|
| 210 |
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
| 211 |
+
|
| 212 |
+
// 获取详情页HTML
|
| 213 |
+
const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
|
| 214 |
+
headers: {
|
| 215 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
| 216 |
+
},
|
| 217 |
+
signal: controller.signal
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
clearTimeout(timeoutId);
|
| 221 |
+
|
| 222 |
+
if (!response.ok) {
|
| 223 |
+
throw new Error(`自定义API详情页请求失败: ${response.status}`);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
// 获取HTML内容
|
| 227 |
+
const html = await response.text();
|
| 228 |
+
|
| 229 |
+
// 使用通用模式提取m3u8链接
|
| 230 |
+
const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
|
| 231 |
+
let matches = html.match(generalPattern) || [];
|
| 232 |
+
|
| 233 |
+
// 处理链接
|
| 234 |
+
matches = matches.map(link => {
|
| 235 |
+
link = link.substring(1, link.length);
|
| 236 |
+
const parenIndex = link.indexOf('(');
|
| 237 |
+
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
// 提取基本信息
|
| 241 |
+
const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
|
| 242 |
+
const titleText = titleMatch ? titleMatch[1].trim() : '';
|
| 243 |
+
|
| 244 |
+
const descMatch = html.match(/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/);
|
| 245 |
+
const descText = descMatch ? descMatch[1].replace(/<[^>]+>/g, ' ').trim() : '';
|
| 246 |
+
|
| 247 |
+
return JSON.stringify({
|
| 248 |
+
code: 200,
|
| 249 |
+
episodes: matches,
|
| 250 |
+
detailUrl: detailUrl,
|
| 251 |
+
videoInfo: {
|
| 252 |
+
title: titleText,
|
| 253 |
+
desc: descText,
|
| 254 |
+
source_name: '自定义源',
|
| 255 |
+
source_code: 'custom'
|
| 256 |
+
}
|
| 257 |
+
});
|
| 258 |
+
} catch (error) {
|
| 259 |
+
console.error(`自定义API详情获取失败:`, error);
|
| 260 |
+
throw error;
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// 通用特殊源详情处理函数
|
| 265 |
+
async function handleSpecialSourceDetail(id, sourceCode) {
|
| 266 |
+
try {
|
| 267 |
+
// 构建详情页URL(使用配置中的detail URL而不是api URL)
|
| 268 |
+
const detailUrl = `${API_SITES[sourceCode].detail}/index.php/vod/detail/id/${id}.html`;
|
| 269 |
+
|
| 270 |
+
// 添加超时处理
|
| 271 |
+
const controller = new AbortController();
|
| 272 |
+
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
| 273 |
+
|
| 274 |
+
// 获取详情页HTML
|
| 275 |
+
const response = await fetch(PROXY_URL + encodeURIComponent(detailUrl), {
|
| 276 |
+
headers: {
|
| 277 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
| 278 |
+
},
|
| 279 |
+
signal: controller.signal
|
| 280 |
+
});
|
| 281 |
+
|
| 282 |
+
clearTimeout(timeoutId);
|
| 283 |
+
|
| 284 |
+
if (!response.ok) {
|
| 285 |
+
throw new Error(`详情页请求失败: ${response.status}`);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// 获取HTML内容
|
| 289 |
+
const html = await response.text();
|
| 290 |
+
|
| 291 |
+
// 根据不同源类型使用不同的正则表达式
|
| 292 |
+
let matches = [];
|
| 293 |
+
|
| 294 |
+
if (sourceCode === 'ffzy') {
|
| 295 |
+
// 非凡影视使用特定的正则表达式
|
| 296 |
+
const ffzyPattern = /\$(https?:\/\/[^"'\s]+?\/\d{8}\/\d+_[a-f0-9]+\/index\.m3u8)/g;
|
| 297 |
+
matches = html.match(ffzyPattern) || [];
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
// 如果没有找到链接或者是其他源类型,尝试一个更通用的模式
|
| 301 |
+
if (matches.length === 0) {
|
| 302 |
+
const generalPattern = /\$(https?:\/\/[^"'\s]+?\.m3u8)/g;
|
| 303 |
+
matches = html.match(generalPattern) || [];
|
| 304 |
+
}
|
| 305 |
+
// 去重处理,避免一个播放源多集显示
|
| 306 |
+
matches = [...new Set(matches)];
|
| 307 |
+
// 处理链接
|
| 308 |
+
matches = matches.map(link => {
|
| 309 |
+
link = link.substring(1, link.length);
|
| 310 |
+
const parenIndex = link.indexOf('(');
|
| 311 |
+
return parenIndex > 0 ? link.substring(0, parenIndex) : link;
|
| 312 |
+
});
|
| 313 |
+
|
| 314 |
+
// 提取可能存在的标题、简介等基本信息
|
| 315 |
+
const titleMatch = html.match(/<h1[^>]*>([^<]+)<\/h1>/);
|
| 316 |
+
const titleText = titleMatch ? titleMatch[1].trim() : '';
|
| 317 |
+
|
| 318 |
+
const descMatch = html.match(/<div[^>]*class=["']sketch["'][^>]*>([\s\S]*?)<\/div>/);
|
| 319 |
+
const descText = descMatch ? descMatch[1].replace(/<[^>]+>/g, ' ').trim() : '';
|
| 320 |
+
|
| 321 |
+
return JSON.stringify({
|
| 322 |
+
code: 200,
|
| 323 |
+
episodes: matches,
|
| 324 |
+
detailUrl: detailUrl,
|
| 325 |
+
videoInfo: {
|
| 326 |
+
title: titleText,
|
| 327 |
+
desc: descText,
|
| 328 |
+
source_name: API_SITES[sourceCode].name,
|
| 329 |
+
source_code: sourceCode
|
| 330 |
+
}
|
| 331 |
+
});
|
| 332 |
+
} catch (error) {
|
| 333 |
+
console.error(`${API_SITES[sourceCode].name}详情获取失败:`, error);
|
| 334 |
+
throw error;
|
| 335 |
+
}
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// 处理聚合搜索
|
| 339 |
+
async function handleAggregatedSearch(searchQuery) {
|
| 340 |
+
// 获取可用的API源列表(排除aggregated和custom)
|
| 341 |
+
const availableSources = Object.keys(API_SITES).filter(key =>
|
| 342 |
+
key !== 'aggregated' && key !== 'custom'
|
| 343 |
+
);
|
| 344 |
+
|
| 345 |
+
if (availableSources.length === 0) {
|
| 346 |
+
throw new Error('没有可用的API源');
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
// 创建所有API源的搜索请求
|
| 350 |
+
const searchPromises = availableSources.map(async (source) => {
|
| 351 |
+
try {
|
| 352 |
+
const apiUrl = `${API_SITES[source].api}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
|
| 353 |
+
|
| 354 |
+
// 使用Promise.race添加超时处理
|
| 355 |
+
const timeoutPromise = new Promise((_, reject) =>
|
| 356 |
+
setTimeout(() => reject(new Error(`${source}源搜索超时`)), 8000)
|
| 357 |
+
);
|
| 358 |
+
|
| 359 |
+
const fetchPromise = fetch(PROXY_URL + encodeURIComponent(apiUrl), {
|
| 360 |
+
headers: API_CONFIG.search.headers
|
| 361 |
+
});
|
| 362 |
+
|
| 363 |
+
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
| 364 |
+
|
| 365 |
+
if (!response.ok) {
|
| 366 |
+
throw new Error(`${source}源请求失败: ${response.status}`);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
const data = await response.json();
|
| 370 |
+
|
| 371 |
+
if (!data || !Array.isArray(data.list)) {
|
| 372 |
+
throw new Error(`${source}源返回的数据格式无效`);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// 为搜索结果添加源信息
|
| 376 |
+
const results = data.list.map(item => ({
|
| 377 |
+
...item,
|
| 378 |
+
source_name: API_SITES[source].name,
|
| 379 |
+
source_code: source
|
| 380 |
+
}));
|
| 381 |
+
|
| 382 |
+
return results;
|
| 383 |
+
} catch (error) {
|
| 384 |
+
console.warn(`${source}源搜索失败:`, error);
|
| 385 |
+
return []; // 返回空数组表示该源搜索失败
|
| 386 |
+
}
|
| 387 |
+
});
|
| 388 |
+
|
| 389 |
+
try {
|
| 390 |
+
// 并行执行所有搜索请求
|
| 391 |
+
const resultsArray = await Promise.all(searchPromises);
|
| 392 |
+
|
| 393 |
+
// 合并所有结果
|
| 394 |
+
let allResults = [];
|
| 395 |
+
resultsArray.forEach(results => {
|
| 396 |
+
if (Array.isArray(results) && results.length > 0) {
|
| 397 |
+
allResults = allResults.concat(results);
|
| 398 |
+
}
|
| 399 |
+
});
|
| 400 |
+
|
| 401 |
+
// 如果没有搜索结果,返回空结果
|
| 402 |
+
if (allResults.length === 0) {
|
| 403 |
+
return JSON.stringify({
|
| 404 |
+
code: 200,
|
| 405 |
+
list: [],
|
| 406 |
+
msg: '所有源均无搜索结果'
|
| 407 |
+
});
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// 去重(根据vod_id和source_code组合)
|
| 411 |
+
const uniqueResults = [];
|
| 412 |
+
const seen = new Set();
|
| 413 |
+
|
| 414 |
+
allResults.forEach(item => {
|
| 415 |
+
const key = `${item.source_code}_${item.vod_id}`;
|
| 416 |
+
if (!seen.has(key)) {
|
| 417 |
+
seen.add(key);
|
| 418 |
+
uniqueResults.push(item);
|
| 419 |
+
}
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
// 按照视频名称和来源排序
|
| 423 |
+
uniqueResults.sort((a, b) => {
|
| 424 |
+
// 首先按照视频名称排序
|
| 425 |
+
const nameCompare = (a.vod_name || '').localeCompare(b.vod_name || '');
|
| 426 |
+
if (nameCompare !== 0) return nameCompare;
|
| 427 |
+
|
| 428 |
+
// 如果名称相同,则按照来源排序
|
| 429 |
+
return (a.source_name || '').localeCompare(b.source_name || '');
|
| 430 |
+
});
|
| 431 |
+
|
| 432 |
+
return JSON.stringify({
|
| 433 |
+
code: 200,
|
| 434 |
+
list: uniqueResults,
|
| 435 |
+
});
|
| 436 |
+
} catch (error) {
|
| 437 |
+
console.error('聚合搜索处理错误:', error);
|
| 438 |
+
return JSON.stringify({
|
| 439 |
+
code: 400,
|
| 440 |
+
msg: '聚合搜索处理失败: ' + error.message,
|
| 441 |
+
list: []
|
| 442 |
+
});
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
// 处理多个自定义API源的聚合搜索
|
| 447 |
+
async function handleMultipleCustomSearch(searchQuery, customApiUrls) {
|
| 448 |
+
// 解析自定义API列表
|
| 449 |
+
const apiUrls = customApiUrls.split(CUSTOM_API_CONFIG.separator)
|
| 450 |
+
.map(url => url.trim())
|
| 451 |
+
.filter(url => url.length > 0 && /^https?:\/\//.test(url))
|
| 452 |
+
.slice(0, CUSTOM_API_CONFIG.maxSources);
|
| 453 |
+
|
| 454 |
+
if (apiUrls.length === 0) {
|
| 455 |
+
throw new Error('没有提供有效的自定义API地址');
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
// 为每个API创建搜索请求
|
| 459 |
+
const searchPromises = apiUrls.map(async (apiUrl, index) => {
|
| 460 |
+
try {
|
| 461 |
+
const fullUrl = `${apiUrl}${API_CONFIG.search.path}${encodeURIComponent(searchQuery)}`;
|
| 462 |
+
|
| 463 |
+
// 使用Promise.race添加超时处理
|
| 464 |
+
const timeoutPromise = new Promise((_, reject) =>
|
| 465 |
+
setTimeout(() => reject(new Error(`自定义API ${index+1} 搜索超时`)), 8000)
|
| 466 |
+
);
|
| 467 |
+
|
| 468 |
+
const fetchPromise = fetch(PROXY_URL + encodeURIComponent(fullUrl), {
|
| 469 |
+
headers: API_CONFIG.search.headers
|
| 470 |
+
});
|
| 471 |
+
|
| 472 |
+
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
| 473 |
+
|
| 474 |
+
if (!response.ok) {
|
| 475 |
+
throw new Error(`自定义API ${index+1} 请求失败: ${response.status}`);
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
const data = await response.json();
|
| 479 |
+
|
| 480 |
+
if (!data || !Array.isArray(data.list)) {
|
| 481 |
+
throw new Error(`自定义API ${index+1} 返回的数据格式无效`);
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
// 为搜索结果添加源信息
|
| 485 |
+
const results = data.list.map(item => ({
|
| 486 |
+
...item,
|
| 487 |
+
source_name: `${CUSTOM_API_CONFIG.namePrefix}${index+1}`,
|
| 488 |
+
source_code: 'custom',
|
| 489 |
+
api_url: apiUrl // 保存API URL以便详情获取
|
| 490 |
+
}));
|
| 491 |
+
|
| 492 |
+
return results;
|
| 493 |
+
} catch (error) {
|
| 494 |
+
console.warn(`自定义API ${index+1} 搜索失败:`, error);
|
| 495 |
+
return []; // 返回空数组表示该源搜索失败
|
| 496 |
+
}
|
| 497 |
+
});
|
| 498 |
+
|
| 499 |
+
try {
|
| 500 |
+
// 并行执行所有搜索请求
|
| 501 |
+
const resultsArray = await Promise.all(searchPromises);
|
| 502 |
+
|
| 503 |
+
// 合并所有结果
|
| 504 |
+
let allResults = [];
|
| 505 |
+
resultsArray.forEach(results => {
|
| 506 |
+
if (Array.isArray(results) && results.length > 0) {
|
| 507 |
+
allResults = allResults.concat(results);
|
| 508 |
+
}
|
| 509 |
+
});
|
| 510 |
+
|
| 511 |
+
// 如果没有搜索结果,返回空结果
|
| 512 |
+
if (allResults.length === 0) {
|
| 513 |
+
return JSON.stringify({
|
| 514 |
+
code: 200,
|
| 515 |
+
list: [],
|
| 516 |
+
msg: '所有自定义API源均无搜索结果'
|
| 517 |
+
});
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
// 去重(根据vod_id和api_url组合)
|
| 521 |
+
const uniqueResults = [];
|
| 522 |
+
const seen = new Set();
|
| 523 |
+
|
| 524 |
+
allResults.forEach(item => {
|
| 525 |
+
const key = `${item.api_url || ''}_${item.vod_id}`;
|
| 526 |
+
if (!seen.has(key)) {
|
| 527 |
+
seen.add(key);
|
| 528 |
+
uniqueResults.push(item);
|
| 529 |
+
}
|
| 530 |
+
});
|
| 531 |
+
|
| 532 |
+
return JSON.stringify({
|
| 533 |
+
code: 200,
|
| 534 |
+
list: uniqueResults,
|
| 535 |
+
});
|
| 536 |
+
} catch (error) {
|
| 537 |
+
console.error('自定义API聚合搜索处理错误:', error);
|
| 538 |
+
return JSON.stringify({
|
| 539 |
+
code: 400,
|
| 540 |
+
msg: '自定义API聚合搜索处理失败: ' + error.message,
|
| 541 |
+
list: []
|
| 542 |
+
});
|
| 543 |
+
}
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
// 拦截API请求
|
| 547 |
+
(function() {
|
| 548 |
+
const originalFetch = window.fetch;
|
| 549 |
+
|
| 550 |
+
window.fetch = async function(input, init) {
|
| 551 |
+
const requestUrl = typeof input === 'string' ? new URL(input, window.location.origin) : input.url;
|
| 552 |
+
|
| 553 |
+
if (requestUrl.pathname.startsWith('/api/')) {
|
| 554 |
+
if (window.isPasswordProtected && window.isPasswordVerified) {
|
| 555 |
+
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
|
| 556 |
+
return;
|
| 557 |
+
}
|
| 558 |
+
}
|
| 559 |
+
try {
|
| 560 |
+
const data = await handleApiRequest(requestUrl);
|
| 561 |
+
return new Response(data, {
|
| 562 |
+
headers: {
|
| 563 |
+
'Content-Type': 'application/json',
|
| 564 |
+
'Access-Control-Allow-Origin': '*',
|
| 565 |
+
},
|
| 566 |
+
});
|
| 567 |
+
} catch (error) {
|
| 568 |
+
return new Response(JSON.stringify({
|
| 569 |
+
code: 500,
|
| 570 |
+
msg: '服务器内部错误',
|
| 571 |
+
}), {
|
| 572 |
+
status: 500,
|
| 573 |
+
headers: {
|
| 574 |
+
'Content-Type': 'application/json',
|
| 575 |
+
},
|
| 576 |
+
});
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
// 非API请求使用原始fetch
|
| 581 |
+
return originalFetch.apply(this, arguments);
|
| 582 |
+
};
|
| 583 |
+
})();
|
| 584 |
+
|
| 585 |
+
async function testSiteAvailability(apiUrl) {
|
| 586 |
+
try {
|
| 587 |
+
// 使用更简单的测试查询
|
| 588 |
+
const response = await fetch('/api/search?wd=test&customApi=' + encodeURIComponent(apiUrl), {
|
| 589 |
+
// 添加超时
|
| 590 |
+
signal: AbortSignal.timeout(5000)
|
| 591 |
+
});
|
| 592 |
+
|
| 593 |
+
// 检查响应状态
|
| 594 |
+
if (!response.ok) {
|
| 595 |
+
return false;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
const data = await response.json();
|
| 599 |
+
|
| 600 |
+
// 检查API响应的有效性
|
| 601 |
+
return data && data.code !== 400 && Array.isArray(data.list);
|
| 602 |
+
} catch (error) {
|
| 603 |
+
console.error('站点可用性测试失败:', error);
|
| 604 |
+
return false;
|
| 605 |
+
}
|
| 606 |
+
}
|
js/app.js
ADDED
|
@@ -0,0 +1,1358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 全局变量
|
| 2 |
+
let selectedAPIs = JSON.parse(localStorage.getItem('selectedAPIs') || '["tyyszy","dyttzy", "bfzy", "ruyi"]'); // 默认选中资源
|
| 3 |
+
let customAPIs = JSON.parse(localStorage.getItem('customAPIs') || '[]'); // 存储自定义API列表
|
| 4 |
+
|
| 5 |
+
// 添加当前播放的集数索引
|
| 6 |
+
let currentEpisodeIndex = 0;
|
| 7 |
+
// 添加当前视频的所有集数
|
| 8 |
+
let currentEpisodes = [];
|
| 9 |
+
// 添加当前视频的标题
|
| 10 |
+
let currentVideoTitle = '';
|
| 11 |
+
// 全局变量用于倒序状态
|
| 12 |
+
let episodesReversed = false;
|
| 13 |
+
|
| 14 |
+
// 页面初始化
|
| 15 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 16 |
+
// 初始化API复选框
|
| 17 |
+
initAPICheckboxes();
|
| 18 |
+
|
| 19 |
+
// 初始化自定义API列表
|
| 20 |
+
renderCustomAPIsList();
|
| 21 |
+
|
| 22 |
+
// 初始化显示选中的API数量
|
| 23 |
+
updateSelectedApiCount();
|
| 24 |
+
|
| 25 |
+
// 渲染搜索历史
|
| 26 |
+
renderSearchHistory();
|
| 27 |
+
|
| 28 |
+
// 设置默认API选择(如果是第一次加载)
|
| 29 |
+
if (!localStorage.getItem('hasInitializedDefaults')) {
|
| 30 |
+
// 默认选中资源
|
| 31 |
+
selectedAPIs = ["tyyszy", "bfzy", "dyttzy", "ruyi"];
|
| 32 |
+
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
| 33 |
+
|
| 34 |
+
// 默认选中过滤开关
|
| 35 |
+
localStorage.setItem('yellowFilterEnabled', 'true');
|
| 36 |
+
localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, 'true');
|
| 37 |
+
|
| 38 |
+
// 默认启用豆瓣功能
|
| 39 |
+
localStorage.setItem('doubanEnabled', 'true');
|
| 40 |
+
|
| 41 |
+
// 标记已初始化默认值
|
| 42 |
+
localStorage.setItem('hasInitializedDefaults', 'true');
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// 设置黄色内容过滤器开关初始状态
|
| 46 |
+
const yellowFilterToggle = document.getElementById('yellowFilterToggle');
|
| 47 |
+
if (yellowFilterToggle) {
|
| 48 |
+
yellowFilterToggle.checked = localStorage.getItem('yellowFilterEnabled') === 'true';
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// 设置广告过滤开关初始状态
|
| 52 |
+
const adFilterToggle = document.getElementById('adFilterToggle');
|
| 53 |
+
if (adFilterToggle) {
|
| 54 |
+
adFilterToggle.checked = localStorage.getItem(PLAYER_CONFIG.adFilteringStorage) !== 'false'; // 默认为true
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// 设置事件监听器
|
| 58 |
+
setupEventListeners();
|
| 59 |
+
|
| 60 |
+
// 初始检查成人API选中状态
|
| 61 |
+
setTimeout(checkAdultAPIsSelected, 100);
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
// 初始化API复选框
|
| 65 |
+
function initAPICheckboxes() {
|
| 66 |
+
const container = document.getElementById('apiCheckboxes');
|
| 67 |
+
container.innerHTML = '';
|
| 68 |
+
|
| 69 |
+
// 添加普通API组标题
|
| 70 |
+
const normaldiv = document.createElement('div');
|
| 71 |
+
normaldiv.id = 'normaldiv';
|
| 72 |
+
normaldiv.className = 'grid grid-cols-2 gap-2';
|
| 73 |
+
const normalTitle = document.createElement('div');
|
| 74 |
+
normalTitle.className = 'api-group-title';
|
| 75 |
+
normalTitle.textContent = '普通资源';
|
| 76 |
+
normaldiv.appendChild(normalTitle);
|
| 77 |
+
|
| 78 |
+
// 创建普通API源的复选框
|
| 79 |
+
Object.keys(API_SITES).forEach(apiKey => {
|
| 80 |
+
const api = API_SITES[apiKey];
|
| 81 |
+
if (api.adult) return; // 跳过成人内容API,稍后添加
|
| 82 |
+
|
| 83 |
+
const checked = selectedAPIs.includes(apiKey);
|
| 84 |
+
|
| 85 |
+
const checkbox = document.createElement('div');
|
| 86 |
+
checkbox.className = 'flex items-center';
|
| 87 |
+
checkbox.innerHTML = `
|
| 88 |
+
<input type="checkbox" id="api_${apiKey}"
|
| 89 |
+
class="form-checkbox h-3 w-3 text-blue-600 bg-[#222] border border-[#333]"
|
| 90 |
+
${checked ? 'checked' : ''}
|
| 91 |
+
data-api="${apiKey}">
|
| 92 |
+
<label for="api_${apiKey}" class="ml-1 text-xs text-gray-400 truncate">${api.name}</label>
|
| 93 |
+
`;
|
| 94 |
+
normaldiv.appendChild(checkbox);
|
| 95 |
+
|
| 96 |
+
// 添加事件监听器
|
| 97 |
+
checkbox.querySelector('input').addEventListener('change', function () {
|
| 98 |
+
updateSelectedAPIs();
|
| 99 |
+
checkAdultAPIsSelected();
|
| 100 |
+
});
|
| 101 |
+
});
|
| 102 |
+
container.appendChild(normaldiv);
|
| 103 |
+
|
| 104 |
+
// 添加成人API列表
|
| 105 |
+
addAdultAPI();
|
| 106 |
+
|
| 107 |
+
// 初始检查成人内容状态
|
| 108 |
+
checkAdultAPIsSelected();
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// 添加成人API列表
|
| 112 |
+
function addAdultAPI() {
|
| 113 |
+
// 仅在隐藏设置为false时添加成人API组
|
| 114 |
+
if (!HIDE_BUILTIN_ADULT_APIS && (localStorage.getItem('yellowFilterEnabled') === 'false')) {
|
| 115 |
+
const container = document.getElementById('apiCheckboxes');
|
| 116 |
+
|
| 117 |
+
// 添加成人API组标题
|
| 118 |
+
const adultdiv = document.createElement('div');
|
| 119 |
+
adultdiv.id = 'adultdiv';
|
| 120 |
+
adultdiv.className = 'grid grid-cols-2 gap-2';
|
| 121 |
+
const adultTitle = document.createElement('div');
|
| 122 |
+
adultTitle.className = 'api-group-title adult';
|
| 123 |
+
adultTitle.innerHTML = `黄色资源采集站 <span class="adult-warning">
|
| 124 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 125 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
| 126 |
+
</svg>
|
| 127 |
+
</span>`;
|
| 128 |
+
adultdiv.appendChild(adultTitle);
|
| 129 |
+
|
| 130 |
+
// 创建成人API源的复选框
|
| 131 |
+
Object.keys(API_SITES).forEach(apiKey => {
|
| 132 |
+
const api = API_SITES[apiKey];
|
| 133 |
+
if (!api.adult) return; // 仅添加成人内容API
|
| 134 |
+
|
| 135 |
+
const checked = selectedAPIs.includes(apiKey);
|
| 136 |
+
|
| 137 |
+
const checkbox = document.createElement('div');
|
| 138 |
+
checkbox.className = 'flex items-center';
|
| 139 |
+
checkbox.innerHTML = `
|
| 140 |
+
<input type="checkbox" id="api_${apiKey}"
|
| 141 |
+
class="form-checkbox h-3 w-3 text-blue-600 bg-[#222] border border-[#333] api-adult"
|
| 142 |
+
${checked ? 'checked' : ''}
|
| 143 |
+
data-api="${apiKey}">
|
| 144 |
+
<label for="api_${apiKey}" class="ml-1 text-xs text-pink-400 truncate">${api.name}</label>
|
| 145 |
+
`;
|
| 146 |
+
adultdiv.appendChild(checkbox);
|
| 147 |
+
|
| 148 |
+
// 添加事件监听器
|
| 149 |
+
checkbox.querySelector('input').addEventListener('change', function () {
|
| 150 |
+
updateSelectedAPIs();
|
| 151 |
+
checkAdultAPIsSelected();
|
| 152 |
+
});
|
| 153 |
+
});
|
| 154 |
+
container.appendChild(adultdiv);
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// 检查是否有成人API被选中
|
| 159 |
+
function checkAdultAPIsSelected() {
|
| 160 |
+
// 查找所有内置成人API复选框
|
| 161 |
+
const adultBuiltinCheckboxes = document.querySelectorAll('#apiCheckboxes .api-adult:checked');
|
| 162 |
+
|
| 163 |
+
// 查找所有自定义成人API复选框
|
| 164 |
+
const customApiCheckboxes = document.querySelectorAll('#customApisList .api-adult:checked');
|
| 165 |
+
|
| 166 |
+
const hasAdultSelected = adultBuiltinCheckboxes.length > 0 || customApiCheckboxes.length > 0;
|
| 167 |
+
|
| 168 |
+
const yellowFilterToggle = document.getElementById('yellowFilterToggle');
|
| 169 |
+
const yellowFilterContainer = yellowFilterToggle.closest('div').parentNode;
|
| 170 |
+
const filterDescription = yellowFilterContainer.querySelector('p.filter-description');
|
| 171 |
+
|
| 172 |
+
// 如果选择了成人API,禁用黄色内容过滤器
|
| 173 |
+
if (hasAdultSelected) {
|
| 174 |
+
yellowFilterToggle.checked = false;
|
| 175 |
+
yellowFilterToggle.disabled = true;
|
| 176 |
+
localStorage.setItem('yellowFilterEnabled', 'false');
|
| 177 |
+
|
| 178 |
+
// 添加禁用样式
|
| 179 |
+
yellowFilterContainer.classList.add('filter-disabled');
|
| 180 |
+
|
| 181 |
+
// 修改描述文字
|
| 182 |
+
if (filterDescription) {
|
| 183 |
+
filterDescription.innerHTML = '<strong class="text-pink-300">选中黄色资源站时无法启用此过滤</strong>';
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// 移除提示信息(如果存在)
|
| 187 |
+
const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip');
|
| 188 |
+
if (existingTooltip) {
|
| 189 |
+
existingTooltip.remove();
|
| 190 |
+
}
|
| 191 |
+
} else {
|
| 192 |
+
// 启用黄色内容过滤器
|
| 193 |
+
yellowFilterToggle.disabled = false;
|
| 194 |
+
yellowFilterContainer.classList.remove('filter-disabled');
|
| 195 |
+
|
| 196 |
+
// 恢复原来的描述文字
|
| 197 |
+
if (filterDescription) {
|
| 198 |
+
filterDescription.innerHTML = '过滤"伦理片"等黄色内容';
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// 移除提示信息
|
| 202 |
+
const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip');
|
| 203 |
+
if (existingTooltip) {
|
| 204 |
+
existingTooltip.remove();
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
// 渲染自定义API列表
|
| 210 |
+
function renderCustomAPIsList() {
|
| 211 |
+
const container = document.getElementById('customApisList');
|
| 212 |
+
if (!container) return;
|
| 213 |
+
|
| 214 |
+
if (customAPIs.length === 0) {
|
| 215 |
+
container.innerHTML = '<p class="text-xs text-gray-500 text-center my-2">未添加自定义API</p>';
|
| 216 |
+
return;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
container.innerHTML = '';
|
| 220 |
+
customAPIs.forEach((api, index) => {
|
| 221 |
+
const apiItem = document.createElement('div');
|
| 222 |
+
apiItem.className = 'flex items-center justify-between p-1 mb-1 bg-[#222] rounded';
|
| 223 |
+
const textColorClass = api.isAdult ? 'text-pink-400' : 'text-white';
|
| 224 |
+
const adultTag = api.isAdult ? '<span class="text-xs text-pink-400 mr-1">(18+)</span>' : '';
|
| 225 |
+
// 新增 detail 地址显示
|
| 226 |
+
const detailLine = api.detail ? `<div class="text-xs text-gray-400 truncate">detail: ${api.detail}</div>` : '';
|
| 227 |
+
apiItem.innerHTML = `
|
| 228 |
+
<div class="flex items-center flex-1 min-w-0">
|
| 229 |
+
<input type="checkbox" id="custom_api_${index}"
|
| 230 |
+
class="form-checkbox h-3 w-3 text-blue-600 mr-1 ${api.isAdult ? 'api-adult' : ''}"
|
| 231 |
+
${selectedAPIs.includes('custom_' + index) ? 'checked' : ''}
|
| 232 |
+
data-custom-index="${index}">
|
| 233 |
+
<div class="flex-1 min-w-0">
|
| 234 |
+
<div class="text-xs font-medium ${textColorClass} truncate">
|
| 235 |
+
${adultTag}${api.name}
|
| 236 |
+
</div>
|
| 237 |
+
<div class="text-xs text-gray-500 truncate">${api.url}</div>
|
| 238 |
+
${detailLine}
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="flex items-center">
|
| 242 |
+
<button class="text-blue-500 hover:text-blue-700 text-xs px-1" onclick="editCustomApi(${index})">✎</button>
|
| 243 |
+
<button class="text-red-500 hover:text-red-700 text-xs px-1" onclick="removeCustomApi(${index})">✕</button>
|
| 244 |
+
</div>
|
| 245 |
+
`;
|
| 246 |
+
container.appendChild(apiItem);
|
| 247 |
+
apiItem.querySelector('input').addEventListener('change', function () {
|
| 248 |
+
updateSelectedAPIs();
|
| 249 |
+
checkAdultAPIsSelected();
|
| 250 |
+
});
|
| 251 |
+
});
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// 编辑自定义API
|
| 255 |
+
function editCustomApi(index) {
|
| 256 |
+
if (index < 0 || index >= customAPIs.length) return;
|
| 257 |
+
const api = customAPIs[index];
|
| 258 |
+
document.getElementById('customApiName').value = api.name;
|
| 259 |
+
document.getElementById('customApiUrl').value = api.url;
|
| 260 |
+
document.getElementById('customApiDetail').value = api.detail || '';
|
| 261 |
+
const isAdultInput = document.getElementById('customApiIsAdult');
|
| 262 |
+
if (isAdultInput) isAdultInput.checked = api.isAdult || false;
|
| 263 |
+
const form = document.getElementById('addCustomApiForm');
|
| 264 |
+
if (form) {
|
| 265 |
+
form.classList.remove('hidden');
|
| 266 |
+
const buttonContainer = form.querySelector('div:last-child');
|
| 267 |
+
buttonContainer.innerHTML = `
|
| 268 |
+
<button onclick="updateCustomApi(${index})" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-xs">更新</button>
|
| 269 |
+
<button onclick="cancelEditCustomApi()" class="bg-[#444] hover:bg-[#555] text-white px-3 py-1 rounded text-xs">取消</button>
|
| 270 |
+
`;
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// 更新自定义API
|
| 275 |
+
function updateCustomApi(index) {
|
| 276 |
+
if (index < 0 || index >= customAPIs.length) return;
|
| 277 |
+
const nameInput = document.getElementById('customApiName');
|
| 278 |
+
const urlInput = document.getElementById('customApiUrl');
|
| 279 |
+
const detailInput = document.getElementById('customApiDetail');
|
| 280 |
+
const isAdultInput = document.getElementById('customApiIsAdult');
|
| 281 |
+
const name = nameInput.value.trim();
|
| 282 |
+
let url = urlInput.value.trim();
|
| 283 |
+
const detail = detailInput ? detailInput.value.trim() : '';
|
| 284 |
+
const isAdult = isAdultInput ? isAdultInput.checked : false;
|
| 285 |
+
if (!name || !url) {
|
| 286 |
+
showToast('请输入API名称和链接', 'warning');
|
| 287 |
+
return;
|
| 288 |
+
}
|
| 289 |
+
if (!/^https?:\/\/.+/.test(url)) {
|
| 290 |
+
showToast('API链接格式不正确,需以http://或https://开头', 'warning');
|
| 291 |
+
return;
|
| 292 |
+
}
|
| 293 |
+
if (url.endsWith('/')) url = url.slice(0, -1);
|
| 294 |
+
// 保存 detail 字段
|
| 295 |
+
customAPIs[index] = { name, url, detail, isAdult };
|
| 296 |
+
localStorage.setItem('customAPIs', JSON.stringify(customAPIs));
|
| 297 |
+
renderCustomAPIsList();
|
| 298 |
+
checkAdultAPIsSelected();
|
| 299 |
+
restoreAddCustomApiButtons();
|
| 300 |
+
nameInput.value = '';
|
| 301 |
+
urlInput.value = '';
|
| 302 |
+
if (detailInput) detailInput.value = '';
|
| 303 |
+
if (isAdultInput) isAdultInput.checked = false;
|
| 304 |
+
document.getElementById('addCustomApiForm').classList.add('hidden');
|
| 305 |
+
showToast('已更新自定义API: ' + name, 'success');
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// 取消编辑自定义API
|
| 309 |
+
function cancelEditCustomApi() {
|
| 310 |
+
// 清空表单
|
| 311 |
+
document.getElementById('customApiName').value = '';
|
| 312 |
+
document.getElementById('customApiUrl').value = '';
|
| 313 |
+
document.getElementById('customApiDetail').value = '';
|
| 314 |
+
const isAdultInput = document.getElementById('customApiIsAdult');
|
| 315 |
+
if (isAdultInput) isAdultInput.checked = false;
|
| 316 |
+
|
| 317 |
+
// 隐藏表单
|
| 318 |
+
document.getElementById('addCustomApiForm').classList.add('hidden');
|
| 319 |
+
|
| 320 |
+
// 恢复添加按钮
|
| 321 |
+
restoreAddCustomApiButtons();
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// 恢复自定义API添加按钮
|
| 325 |
+
function restoreAddCustomApiButtons() {
|
| 326 |
+
const form = document.getElementById('addCustomApiForm');
|
| 327 |
+
const buttonContainer = form.querySelector('div:last-child');
|
| 328 |
+
buttonContainer.innerHTML = `
|
| 329 |
+
<button onclick="addCustomApi()" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-xs">添加</button>
|
| 330 |
+
<button onclick="cancelAddCustomApi()" class="bg-[#444] hover:bg-[#555] text-white px-3 py-1 rounded text-xs">取消</button>
|
| 331 |
+
`;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
// 更新选中的API列表
|
| 335 |
+
function updateSelectedAPIs() {
|
| 336 |
+
// 获取所有内置API复选框
|
| 337 |
+
const builtInApiCheckboxes = document.querySelectorAll('#apiCheckboxes input:checked');
|
| 338 |
+
|
| 339 |
+
// 获取选中的内置API
|
| 340 |
+
const builtInApis = Array.from(builtInApiCheckboxes).map(input => input.dataset.api);
|
| 341 |
+
|
| 342 |
+
// 获取选中的自定义API
|
| 343 |
+
const customApiCheckboxes = document.querySelectorAll('#customApisList input:checked');
|
| 344 |
+
const customApiIndices = Array.from(customApiCheckboxes).map(input => 'custom_' + input.dataset.customIndex);
|
| 345 |
+
|
| 346 |
+
// 合并内置和自定义API
|
| 347 |
+
selectedAPIs = [...builtInApis, ...customApiIndices];
|
| 348 |
+
|
| 349 |
+
// 保存到localStorage
|
| 350 |
+
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
| 351 |
+
|
| 352 |
+
// 更新显示选中的API数量
|
| 353 |
+
updateSelectedApiCount();
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
// 更新选中的API数量显示
|
| 357 |
+
function updateSelectedApiCount() {
|
| 358 |
+
const countEl = document.getElementById('selectedApiCount');
|
| 359 |
+
if (countEl) {
|
| 360 |
+
countEl.textContent = selectedAPIs.length;
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// 全选或取消全选API
|
| 365 |
+
function selectAllAPIs(selectAll = true, excludeAdult = false) {
|
| 366 |
+
const checkboxes = document.querySelectorAll('#apiCheckboxes input[type="checkbox"]');
|
| 367 |
+
|
| 368 |
+
checkboxes.forEach(checkbox => {
|
| 369 |
+
if (excludeAdult && checkbox.classList.contains('api-adult')) {
|
| 370 |
+
checkbox.checked = false;
|
| 371 |
+
} else {
|
| 372 |
+
checkbox.checked = selectAll;
|
| 373 |
+
}
|
| 374 |
+
});
|
| 375 |
+
|
| 376 |
+
updateSelectedAPIs();
|
| 377 |
+
checkAdultAPIsSelected();
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
// 显示添加自定义API表单
|
| 381 |
+
function showAddCustomApiForm() {
|
| 382 |
+
const form = document.getElementById('addCustomApiForm');
|
| 383 |
+
if (form) {
|
| 384 |
+
form.classList.remove('hidden');
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
// 取消添加自定义API - 修改函数来重用恢复按钮逻辑
|
| 389 |
+
function cancelAddCustomApi() {
|
| 390 |
+
const form = document.getElementById('addCustomApiForm');
|
| 391 |
+
if (form) {
|
| 392 |
+
form.classList.add('hidden');
|
| 393 |
+
document.getElementById('customApiName').value = '';
|
| 394 |
+
document.getElementById('customApiUrl').value = '';
|
| 395 |
+
document.getElementById('customApiDetail').value = '';
|
| 396 |
+
const isAdultInput = document.getElementById('customApiIsAdult');
|
| 397 |
+
if (isAdultInput) isAdultInput.checked = false;
|
| 398 |
+
|
| 399 |
+
// 确保按钮是添加按钮
|
| 400 |
+
restoreAddCustomApiButtons();
|
| 401 |
+
}
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
// 添加自定义API
|
| 405 |
+
function addCustomApi() {
|
| 406 |
+
const nameInput = document.getElementById('customApiName');
|
| 407 |
+
const urlInput = document.getElementById('customApiUrl');
|
| 408 |
+
const detailInput = document.getElementById('customApiDetail');
|
| 409 |
+
const isAdultInput = document.getElementById('customApiIsAdult');
|
| 410 |
+
const name = nameInput.value.trim();
|
| 411 |
+
let url = urlInput.value.trim();
|
| 412 |
+
const detail = detailInput ? detailInput.value.trim() : '';
|
| 413 |
+
const isAdult = isAdultInput ? isAdultInput.checked : false;
|
| 414 |
+
if (!name || !url) {
|
| 415 |
+
showToast('请输入API名称和链接', 'warning');
|
| 416 |
+
return;
|
| 417 |
+
}
|
| 418 |
+
if (!/^https?:\/\/.+/.test(url)) {
|
| 419 |
+
showToast('API链接格式不正确,需以http://或https://开头', 'warning');
|
| 420 |
+
return;
|
| 421 |
+
}
|
| 422 |
+
if (url.endsWith('/')) {
|
| 423 |
+
url = url.slice(0, -1);
|
| 424 |
+
}
|
| 425 |
+
// 保存 detail 字段
|
| 426 |
+
customAPIs.push({ name, url, detail, isAdult });
|
| 427 |
+
localStorage.setItem('customAPIs', JSON.stringify(customAPIs));
|
| 428 |
+
const newApiIndex = customAPIs.length - 1;
|
| 429 |
+
selectedAPIs.push('custom_' + newApiIndex);
|
| 430 |
+
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
| 431 |
+
|
| 432 |
+
// 重新渲染自定义API列表
|
| 433 |
+
renderCustomAPIsList();
|
| 434 |
+
updateSelectedApiCount();
|
| 435 |
+
checkAdultAPIsSelected();
|
| 436 |
+
nameInput.value = '';
|
| 437 |
+
urlInput.value = '';
|
| 438 |
+
if (detailInput) detailInput.value = '';
|
| 439 |
+
if (isAdultInput) isAdultInput.checked = false;
|
| 440 |
+
document.getElementById('addCustomApiForm').classList.add('hidden');
|
| 441 |
+
showToast('已添加自定义API: ' + name, 'success');
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
// 移除自定义API
|
| 445 |
+
function removeCustomApi(index) {
|
| 446 |
+
if (index < 0 || index >= customAPIs.length) return;
|
| 447 |
+
|
| 448 |
+
const apiName = customAPIs[index].name;
|
| 449 |
+
|
| 450 |
+
// 从列表中移除API
|
| 451 |
+
customAPIs.splice(index, 1);
|
| 452 |
+
localStorage.setItem('customAPIs', JSON.stringify(customAPIs));
|
| 453 |
+
|
| 454 |
+
// 从选中列表中移除此API
|
| 455 |
+
const customApiId = 'custom_' + index;
|
| 456 |
+
selectedAPIs = selectedAPIs.filter(id => id !== customApiId);
|
| 457 |
+
|
| 458 |
+
// 更新大于此索引的自定义API索引
|
| 459 |
+
selectedAPIs = selectedAPIs.map(id => {
|
| 460 |
+
if (id.startsWith('custom_')) {
|
| 461 |
+
const currentIndex = parseInt(id.replace('custom_', ''));
|
| 462 |
+
if (currentIndex > index) {
|
| 463 |
+
return 'custom_' + (currentIndex - 1);
|
| 464 |
+
}
|
| 465 |
+
}
|
| 466 |
+
return id;
|
| 467 |
+
});
|
| 468 |
+
|
| 469 |
+
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
| 470 |
+
|
| 471 |
+
// 重新渲染自定义API列表
|
| 472 |
+
renderCustomAPIsList();
|
| 473 |
+
|
| 474 |
+
// 更新选中的API数量
|
| 475 |
+
updateSelectedApiCount();
|
| 476 |
+
|
| 477 |
+
// 重新检查成人API选中状态
|
| 478 |
+
checkAdultAPIsSelected();
|
| 479 |
+
|
| 480 |
+
showToast('已移除自定义API: ' + apiName, 'info');
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
function toggleSettings(e) {
|
| 484 |
+
const settingsPanel = document.getElementById('settingsPanel');
|
| 485 |
+
if (!settingsPanel) return;
|
| 486 |
+
|
| 487 |
+
// 检查是否有管理员密码
|
| 488 |
+
const hasAdminPassword = window.__ENV__?.ADMINPASSWORD &&
|
| 489 |
+
window.__ENV__.ADMINPASSWORD.length === 64 &&
|
| 490 |
+
!/^0+$/.test(window.__ENV__.ADMINPASSWORD);
|
| 491 |
+
|
| 492 |
+
if (settingsPanel.classList.contains('show')) {
|
| 493 |
+
settingsPanel.classList.remove('show');
|
| 494 |
+
} else {
|
| 495 |
+
// 只有设置了管理员密码且未验证时才拦截
|
| 496 |
+
if (hasAdminPassword && !isAdminVerified()) {
|
| 497 |
+
e.preventDefault();
|
| 498 |
+
e.stopPropagation();
|
| 499 |
+
showAdminPasswordModal();
|
| 500 |
+
return;
|
| 501 |
+
}
|
| 502 |
+
settingsPanel.classList.add('show');
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
if (e) {
|
| 506 |
+
e.preventDefault();
|
| 507 |
+
e.stopPropagation();
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
// 设置事件监听器
|
| 512 |
+
function setupEventListeners() {
|
| 513 |
+
// 回车搜索
|
| 514 |
+
document.getElementById('searchInput').addEventListener('keypress', function (e) {
|
| 515 |
+
if (e.key === 'Enter') {
|
| 516 |
+
search();
|
| 517 |
+
}
|
| 518 |
+
});
|
| 519 |
+
|
| 520 |
+
// 点击外部关闭设置面板和历史记录面板
|
| 521 |
+
document.addEventListener('click', function (e) {
|
| 522 |
+
// 关闭设置面板
|
| 523 |
+
const settingsPanel = document.querySelector('#settingsPanel.show');
|
| 524 |
+
const settingsButton = document.querySelector('#settingsPanel .close-btn');
|
| 525 |
+
|
| 526 |
+
if (settingsPanel && settingsButton &&
|
| 527 |
+
!settingsPanel.contains(e.target) &&
|
| 528 |
+
!settingsButton.contains(e.target)) {
|
| 529 |
+
settingsPanel.classList.remove('show');
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
// 关闭历史记录面板
|
| 533 |
+
const historyPanel = document.querySelector('#historyPanel.show');
|
| 534 |
+
const historyButton = document.querySelector('#historyPanel .close-btn');
|
| 535 |
+
|
| 536 |
+
if (historyPanel && historyButton &&
|
| 537 |
+
!historyPanel.contains(e.target) &&
|
| 538 |
+
!historyButton.contains(e.target)) {
|
| 539 |
+
historyPanel.classList.remove('show');
|
| 540 |
+
}
|
| 541 |
+
});
|
| 542 |
+
|
| 543 |
+
// 黄色内容过滤开关事件绑定
|
| 544 |
+
const yellowFilterToggle = document.getElementById('yellowFilterToggle');
|
| 545 |
+
if (yellowFilterToggle) {
|
| 546 |
+
yellowFilterToggle.addEventListener('change', function (e) {
|
| 547 |
+
localStorage.setItem('yellowFilterEnabled', e.target.checked);
|
| 548 |
+
|
| 549 |
+
// 控制黄色内容接口的显示状态
|
| 550 |
+
const adultdiv = document.getElementById('adultdiv');
|
| 551 |
+
if (adultdiv) {
|
| 552 |
+
if (e.target.checked === true) {
|
| 553 |
+
adultdiv.style.display = 'none';
|
| 554 |
+
} else if (e.target.checked === false) {
|
| 555 |
+
adultdiv.style.display = ''
|
| 556 |
+
}
|
| 557 |
+
} else {
|
| 558 |
+
// 添加成人API列表
|
| 559 |
+
addAdultAPI();
|
| 560 |
+
}
|
| 561 |
+
});
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
// 广告过滤开关事件绑定
|
| 565 |
+
const adFilterToggle = document.getElementById('adFilterToggle');
|
| 566 |
+
if (adFilterToggle) {
|
| 567 |
+
adFilterToggle.addEventListener('change', function (e) {
|
| 568 |
+
localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, e.target.checked);
|
| 569 |
+
});
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
// 重置搜索区域
|
| 574 |
+
function resetSearchArea() {
|
| 575 |
+
// 清理搜索结果
|
| 576 |
+
document.getElementById('results').innerHTML = '';
|
| 577 |
+
document.getElementById('searchInput').value = '';
|
| 578 |
+
|
| 579 |
+
// 恢复搜索区域的样式
|
| 580 |
+
document.getElementById('searchArea').classList.add('flex-1');
|
| 581 |
+
document.getElementById('searchArea').classList.remove('mb-8');
|
| 582 |
+
document.getElementById('resultsArea').classList.add('hidden');
|
| 583 |
+
|
| 584 |
+
// 确保页脚正确显示,移除相对定位
|
| 585 |
+
const footer = document.querySelector('.footer');
|
| 586 |
+
if (footer) {
|
| 587 |
+
footer.style.position = '';
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
// 如果有豆瓣功能,检查是否需要显示豆瓣推荐区域
|
| 591 |
+
if (typeof updateDoubanVisibility === 'function') {
|
| 592 |
+
updateDoubanVisibility();
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
// 重置URL为主页
|
| 596 |
+
try {
|
| 597 |
+
window.history.pushState(
|
| 598 |
+
{},
|
| 599 |
+
`LibreTV - 免费在线视频搜索与观看平台`,
|
| 600 |
+
`/`
|
| 601 |
+
);
|
| 602 |
+
// 更新页面标题
|
| 603 |
+
document.title = `LibreTV - 免费在线视频搜索与观看平台`;
|
| 604 |
+
} catch (e) {
|
| 605 |
+
console.error('更新浏览器历史失败:', e);
|
| 606 |
+
}
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// 获取自定义API信息
|
| 610 |
+
function getCustomApiInfo(customApiIndex) {
|
| 611 |
+
const index = parseInt(customApiIndex);
|
| 612 |
+
if (isNaN(index) || index < 0 || index >= customAPIs.length) {
|
| 613 |
+
return null;
|
| 614 |
+
}
|
| 615 |
+
return customAPIs[index];
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
// 搜索功能 - 修改为支持多选API和多页结果
|
| 619 |
+
async function search() {
|
| 620 |
+
// 密码保护校验
|
| 621 |
+
if (window.isPasswordProtected && window.isPasswordVerified) {
|
| 622 |
+
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
|
| 623 |
+
showPasswordModal && showPasswordModal();
|
| 624 |
+
return;
|
| 625 |
+
}
|
| 626 |
+
}
|
| 627 |
+
const query = document.getElementById('searchInput').value.trim();
|
| 628 |
+
|
| 629 |
+
if (!query) {
|
| 630 |
+
showToast('请输入搜索内容', 'info');
|
| 631 |
+
return;
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
if (selectedAPIs.length === 0) {
|
| 635 |
+
showToast('请至少选择一个API源', 'warning');
|
| 636 |
+
return;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
showLoading();
|
| 640 |
+
|
| 641 |
+
try {
|
| 642 |
+
// 保存搜索历史
|
| 643 |
+
saveSearchHistory(query);
|
| 644 |
+
|
| 645 |
+
// 从所有选中的API源搜索
|
| 646 |
+
let allResults = [];
|
| 647 |
+
const searchPromises = selectedAPIs.map(apiId =>
|
| 648 |
+
searchByAPIAndKeyWord(apiId, query)
|
| 649 |
+
);
|
| 650 |
+
|
| 651 |
+
// 等待所有搜索请求完成
|
| 652 |
+
const resultsArray = await Promise.all(searchPromises);
|
| 653 |
+
|
| 654 |
+
// 合并所有结果
|
| 655 |
+
resultsArray.forEach(results => {
|
| 656 |
+
if (Array.isArray(results) && results.length > 0) {
|
| 657 |
+
allResults = allResults.concat(results);
|
| 658 |
+
}
|
| 659 |
+
});
|
| 660 |
+
|
| 661 |
+
// 对搜索结果进行排序:按名称优先,名称相同时按接口源排序
|
| 662 |
+
allResults.sort((a, b) => {
|
| 663 |
+
// 首先按照视频名称排序
|
| 664 |
+
const nameCompare = (a.vod_name || '').localeCompare(b.vod_name || '');
|
| 665 |
+
if (nameCompare !== 0) return nameCompare;
|
| 666 |
+
|
| 667 |
+
// 如果名称相同,则按照来源排序
|
| 668 |
+
return (a.source_name || '').localeCompare(b.source_name || '');
|
| 669 |
+
});
|
| 670 |
+
|
| 671 |
+
// 更新搜索结果计数
|
| 672 |
+
const searchResultsCount = document.getElementById('searchResultsCount');
|
| 673 |
+
if (searchResultsCount) {
|
| 674 |
+
searchResultsCount.textContent = allResults.length;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
// 显示结果区域,调整搜索区域
|
| 678 |
+
document.getElementById('searchArea').classList.remove('flex-1');
|
| 679 |
+
document.getElementById('searchArea').classList.add('mb-8');
|
| 680 |
+
document.getElementById('resultsArea').classList.remove('hidden');
|
| 681 |
+
|
| 682 |
+
// 隐藏豆瓣推荐区域(如果存在)
|
| 683 |
+
const doubanArea = document.getElementById('doubanArea');
|
| 684 |
+
if (doubanArea) {
|
| 685 |
+
doubanArea.classList.add('hidden');
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
const resultsDiv = document.getElementById('results');
|
| 689 |
+
|
| 690 |
+
// 如果没有结果
|
| 691 |
+
if (!allResults || allResults.length === 0) {
|
| 692 |
+
resultsDiv.innerHTML = `
|
| 693 |
+
<div class="col-span-full text-center py-16">
|
| 694 |
+
<svg class="mx-auto h-12 w-12 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 695 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 696 |
+
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 697 |
+
</svg>
|
| 698 |
+
<h3 class="mt-2 text-lg font-medium text-gray-400">没有找到匹配的结果</h3>
|
| 699 |
+
<p class="mt-1 text-sm text-gray-500">请尝试其他关键词或更换数据源</p>
|
| 700 |
+
</div>
|
| 701 |
+
`;
|
| 702 |
+
hideLoading();
|
| 703 |
+
return;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
// 有搜索结果时,才更新URL
|
| 707 |
+
try {
|
| 708 |
+
// 使用URI编码确保特殊字符能够正确显示
|
| 709 |
+
const encodedQuery = encodeURIComponent(query);
|
| 710 |
+
// 使用HTML5 History API更新URL,不刷新页面
|
| 711 |
+
window.history.pushState(
|
| 712 |
+
{ search: query },
|
| 713 |
+
`搜索: ${query} - LibreTV`,
|
| 714 |
+
`/s=${encodedQuery}`
|
| 715 |
+
);
|
| 716 |
+
// 更新页面标题
|
| 717 |
+
document.title = `搜索: ${query} - LibreTV`;
|
| 718 |
+
} catch (e) {
|
| 719 |
+
console.error('更新浏览器历史失败:', e);
|
| 720 |
+
// 如果更新URL失败,继续执行搜索
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
// 处理搜索结果过滤:如果启用了黄色内容过滤,则过滤掉分类含有敏感内容的项目
|
| 724 |
+
const yellowFilterEnabled = localStorage.getItem('yellowFilterEnabled') === 'true';
|
| 725 |
+
if (yellowFilterEnabled) {
|
| 726 |
+
const banned = ['伦理片', '福利', '里番动漫', '门事件', '萝莉少女', '制服诱惑', '国产传媒', 'cosplay', '黑丝诱惑', '无码', '日本无码', '有码', '日本有码', 'SWAG', '网红主播', '色情片', '同性片', '福利视频', '福利片'];
|
| 727 |
+
allResults = allResults.filter(item => {
|
| 728 |
+
const typeName = item.type_name || '';
|
| 729 |
+
return !banned.some(keyword => typeName.includes(keyword));
|
| 730 |
+
});
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
// 添加XSS保护,使用textContent和属性转义
|
| 734 |
+
const safeResults = allResults.map(item => {
|
| 735 |
+
const safeId = item.vod_id ? item.vod_id.toString().replace(/[^\w-]/g, '') : '';
|
| 736 |
+
const safeName = (item.vod_name || '').toString()
|
| 737 |
+
.replace(/</g, '<')
|
| 738 |
+
.replace(/>/g, '>')
|
| 739 |
+
.replace(/"/g, '"');
|
| 740 |
+
const sourceInfo = item.source_name ?
|
| 741 |
+
`<span class="bg-[#222] text-xs px-1.5 py-0.5 rounded-full">${item.source_name}</span>` : '';
|
| 742 |
+
const sourceCode = item.source_code || '';
|
| 743 |
+
|
| 744 |
+
// 添加API URL属性,用于详情获取
|
| 745 |
+
const apiUrlAttr = item.api_url ?
|
| 746 |
+
`data-api-url="${item.api_url.replace(/"/g, '"')}"` : '';
|
| 747 |
+
|
| 748 |
+
// 修改为水平卡片布局,图片在左侧,文本在右侧,并优化样式
|
| 749 |
+
const hasCover = item.vod_pic && item.vod_pic.startsWith('http');
|
| 750 |
+
|
| 751 |
+
return `
|
| 752 |
+
<div class="card-hover bg-[#111] rounded-lg overflow-hidden cursor-pointer transition-all hover:scale-[1.02] h-full shadow-sm hover:shadow-md"
|
| 753 |
+
onclick="showDetails('${safeId}','${safeName}','${sourceCode}')" ${apiUrlAttr}>
|
| 754 |
+
<div class="flex h-full">
|
| 755 |
+
${hasCover ? `
|
| 756 |
+
<div class="relative flex-shrink-0 search-card-img-container">
|
| 757 |
+
<img src="${item.vod_pic}" alt="${safeName}"
|
| 758 |
+
class="h-full w-full object-cover transition-transform hover:scale-110"
|
| 759 |
+
onerror="this.onerror=null; this.src='https://via.placeholder.com/300x450?text=无封面'; this.classList.add('object-contain');"
|
| 760 |
+
loading="lazy">
|
| 761 |
+
<div class="absolute inset-0 bg-gradient-to-r from-black/30 to-transparent"></div>
|
| 762 |
+
</div>` : ''}
|
| 763 |
+
|
| 764 |
+
<div class="p-2 flex flex-col flex-grow">
|
| 765 |
+
<div class="flex-grow">
|
| 766 |
+
<h3 class="font-semibold mb-2 break-words line-clamp-2 ${hasCover ? '' : 'text-center'}" title="${safeName}">${safeName}</h3>
|
| 767 |
+
|
| 768 |
+
<div class="flex flex-wrap ${hasCover ? '' : 'justify-center'} gap-1 mb-2">
|
| 769 |
+
${(item.type_name || '').toString().replace(/</g, '<') ?
|
| 770 |
+
`<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-blue-500 text-blue-300">
|
| 771 |
+
${(item.type_name || '').toString().replace(/</g, '<')}
|
| 772 |
+
</span>` : ''}
|
| 773 |
+
${(item.vod_year || '') ?
|
| 774 |
+
`<span class="text-xs py-0.5 px-1.5 rounded bg-opacity-20 bg-purple-500 text-purple-300">
|
| 775 |
+
${item.vod_year}
|
| 776 |
+
</span>` : ''}
|
| 777 |
+
</div>
|
| 778 |
+
<p class="text-gray-400 line-clamp-2 overflow-hidden ${hasCover ? '' : 'text-center'} mb-2">
|
| 779 |
+
${(item.vod_remarks || '暂无介绍').toString().replace(/</g, '<')}
|
| 780 |
+
</p>
|
| 781 |
+
</div>
|
| 782 |
+
|
| 783 |
+
<div class="flex justify-between items-center mt-1 pt-1 border-t border-gray-800">
|
| 784 |
+
${sourceInfo ? `<div>${sourceInfo}</div>` : '<div></div>'}
|
| 785 |
+
<!-- 接口名称过长会被挤变形
|
| 786 |
+
<div>
|
| 787 |
+
<span class="text-gray-500 flex items-center hover:text-blue-400 transition-colors">
|
| 788 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 789 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
| 790 |
+
</svg>
|
| 791 |
+
播放
|
| 792 |
+
</span>
|
| 793 |
+
</div>
|
| 794 |
+
-->
|
| 795 |
+
</div>
|
| 796 |
+
</div>
|
| 797 |
+
</div>
|
| 798 |
+
</div>
|
| 799 |
+
`;
|
| 800 |
+
}).join('');
|
| 801 |
+
|
| 802 |
+
resultsDiv.innerHTML = safeResults;
|
| 803 |
+
} catch (error) {
|
| 804 |
+
console.error('搜索错误:', error);
|
| 805 |
+
if (error.name === 'AbortError') {
|
| 806 |
+
showToast('搜索请求超时,请检查网络连接', 'error');
|
| 807 |
+
} else {
|
| 808 |
+
showToast('搜索请求失败,请稍后重试', 'error');
|
| 809 |
+
}
|
| 810 |
+
} finally {
|
| 811 |
+
hideLoading();
|
| 812 |
+
}
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
// 切换清空按钮的显示状态
|
| 816 |
+
function toggleClearButton() {
|
| 817 |
+
const searchInput = document.getElementById('searchInput');
|
| 818 |
+
const clearButton = document.getElementById('clearSearchInput');
|
| 819 |
+
if (searchInput.value !== '') {
|
| 820 |
+
clearButton.classList.remove('hidden');
|
| 821 |
+
} else {
|
| 822 |
+
clearButton.classList.add('hidden');
|
| 823 |
+
}
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
// 清空搜索框内容
|
| 827 |
+
function clearSearchInput() {
|
| 828 |
+
const searchInput = document.getElementById('searchInput');
|
| 829 |
+
searchInput.value = '';
|
| 830 |
+
const clearButton = document.getElementById('clearSearchInput');
|
| 831 |
+
clearButton.classList.add('hidden');
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
// 劫持搜索框的value属性以检测外部修改
|
| 835 |
+
function hookInput() {
|
| 836 |
+
const input = document.getElementById('searchInput');
|
| 837 |
+
const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
|
| 838 |
+
|
| 839 |
+
// 重写 value 属性的 getter 和 setter
|
| 840 |
+
Object.defineProperty(input, 'value', {
|
| 841 |
+
get: function () {
|
| 842 |
+
// 确保读取时返回字符串(即使原始值为 undefined/null)
|
| 843 |
+
const originalValue = descriptor.get.call(this);
|
| 844 |
+
return originalValue != null ? String(originalValue) : '';
|
| 845 |
+
},
|
| 846 |
+
set: function (value) {
|
| 847 |
+
// 显式将值转换为字符串后写入
|
| 848 |
+
const strValue = String(value);
|
| 849 |
+
descriptor.set.call(this, strValue);
|
| 850 |
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
| 851 |
+
}
|
| 852 |
+
});
|
| 853 |
+
|
| 854 |
+
// 初始化输入框值为空字符串(避免初始值为 undefined)
|
| 855 |
+
input.value = '';
|
| 856 |
+
}
|
| 857 |
+
document.addEventListener('DOMContentLoaded', hookInput);
|
| 858 |
+
|
| 859 |
+
// 显示详情 - 修改为支持自定义API
|
| 860 |
+
async function showDetails(id, vod_name, sourceCode) {
|
| 861 |
+
// 密码保护校验
|
| 862 |
+
if (window.isPasswordProtected && window.isPasswordVerified) {
|
| 863 |
+
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
|
| 864 |
+
showPasswordModal && showPasswordModal();
|
| 865 |
+
return;
|
| 866 |
+
}
|
| 867 |
+
}
|
| 868 |
+
if (!id) {
|
| 869 |
+
showToast('视频ID无效', 'error');
|
| 870 |
+
return;
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
showLoading();
|
| 874 |
+
try {
|
| 875 |
+
// 构建API参数
|
| 876 |
+
let apiParams = '';
|
| 877 |
+
|
| 878 |
+
// 处理自定义API源
|
| 879 |
+
if (sourceCode.startsWith('custom_')) {
|
| 880 |
+
const customIndex = sourceCode.replace('custom_', '');
|
| 881 |
+
const customApi = getCustomApiInfo(customIndex);
|
| 882 |
+
if (!customApi) {
|
| 883 |
+
showToast('自定义API配置无效', 'error');
|
| 884 |
+
hideLoading();
|
| 885 |
+
return;
|
| 886 |
+
}
|
| 887 |
+
// 传递 detail 字段
|
| 888 |
+
if (customApi.detail) {
|
| 889 |
+
apiParams = '&customApi=' + encodeURIComponent(customApi.url) + '&customDetail=' + encodeURIComponent(customApi.detail) + '&source=custom';
|
| 890 |
+
} else {
|
| 891 |
+
apiParams = '&customApi=' + encodeURIComponent(customApi.url) + '&source=custom';
|
| 892 |
+
}
|
| 893 |
+
} else {
|
| 894 |
+
// 内置API
|
| 895 |
+
apiParams = '&source=' + sourceCode;
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
// Add a timestamp to prevent caching
|
| 899 |
+
const timestamp = new Date().getTime();
|
| 900 |
+
const cacheBuster = `&_t=${timestamp}`;
|
| 901 |
+
const response = await fetch(`/api/detail?id=${encodeURIComponent(id)}${apiParams}${cacheBuster}`);
|
| 902 |
+
|
| 903 |
+
const data = await response.json();
|
| 904 |
+
|
| 905 |
+
const modal = document.getElementById('modal');
|
| 906 |
+
const modalTitle = document.getElementById('modalTitle');
|
| 907 |
+
const modalContent = document.getElementById('modalContent');
|
| 908 |
+
|
| 909 |
+
// 显示来源信息
|
| 910 |
+
const sourceName = data.videoInfo && data.videoInfo.source_name ?
|
| 911 |
+
` <span class="text-sm font-normal text-gray-400">(${data.videoInfo.source_name})</span>` : '';
|
| 912 |
+
|
| 913 |
+
// 不对标题进行截断处理,允许完整显示
|
| 914 |
+
modalTitle.innerHTML = `<span class="break-words">${vod_name || '未知视频'}</span>${sourceName}`;
|
| 915 |
+
currentVideoTitle = vod_name || '未知视频';
|
| 916 |
+
|
| 917 |
+
if (data.episodes && data.episodes.length > 0) {
|
| 918 |
+
// 构建详情信息HTML
|
| 919 |
+
let detailInfoHtml = '';
|
| 920 |
+
if (data.videoInfo) {
|
| 921 |
+
// Prepare description text, strip HTML and trim whitespace
|
| 922 |
+
const descriptionText = data.videoInfo.desc ? data.videoInfo.desc.replace(/<[^>]+>/g, '').trim() : '';
|
| 923 |
+
|
| 924 |
+
// Check if there's any actual grid content
|
| 925 |
+
const hasGridContent = data.videoInfo.type || data.videoInfo.year || data.videoInfo.area || data.videoInfo.director || data.videoInfo.actor || data.videoInfo.remarks;
|
| 926 |
+
|
| 927 |
+
if (hasGridContent || descriptionText) { // Only build if there's something to show
|
| 928 |
+
detailInfoHtml = `
|
| 929 |
+
<div class="modal-detail-info">
|
| 930 |
+
${hasGridContent ? `
|
| 931 |
+
<div class="detail-grid">
|
| 932 |
+
${data.videoInfo.type ? `<div class="detail-item"><span class="detail-label">类型:</span> <span class="detail-value">${data.videoInfo.type}</span></div>` : ''}
|
| 933 |
+
${data.videoInfo.year ? `<div class="detail-item"><span class="detail-label">年份:</span> <span class="detail-value">${data.videoInfo.year}</span></div>` : ''}
|
| 934 |
+
${data.videoInfo.area ? `<div class="detail-item"><span class="detail-label">地区:</span> <span class="detail-value">${data.videoInfo.area}</span></div>` : ''}
|
| 935 |
+
${data.videoInfo.director ? `<div class="detail-item"><span class="detail-label">导演:</span> <span class="detail-value">${data.videoInfo.director}</span></div>` : ''}
|
| 936 |
+
${data.videoInfo.actor ? `<div class="detail-item"><span class="detail-label">主演:</span> <span class="detail-value">${data.videoInfo.actor}</span></div>` : ''}
|
| 937 |
+
${data.videoInfo.remarks ? `<div class="detail-item"><span class="detail-label">备注:</span> <span class="detail-value">${data.videoInfo.remarks}</span></div>` : ''}
|
| 938 |
+
</div>` : ''}
|
| 939 |
+
${descriptionText ? `
|
| 940 |
+
<div class="detail-desc">
|
| 941 |
+
<p class="detail-label">简介:</p>
|
| 942 |
+
<p class="detail-desc-content">${descriptionText}</p>
|
| 943 |
+
</div>` : ''}
|
| 944 |
+
</div>
|
| 945 |
+
`;
|
| 946 |
+
}
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
currentEpisodes = data.episodes;
|
| 950 |
+
currentEpisodeIndex = 0;
|
| 951 |
+
|
| 952 |
+
modalContent.innerHTML = `
|
| 953 |
+
${detailInfoHtml}
|
| 954 |
+
<div class="flex flex-wrap items-center justify-between mb-4 gap-2">
|
| 955 |
+
<div class="flex items-center gap-2">
|
| 956 |
+
<button onclick="toggleEpisodeOrder('${sourceCode}', '${id}')"
|
| 957 |
+
class="px-3 py-1.5 bg-[#333] hover:bg-[#444] border border-[#444] rounded text-sm transition-colors flex items-center gap-1">
|
| 958 |
+
<svg class="w-4 h-4 transform ${episodesReversed ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 959 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
| 960 |
+
</svg>
|
| 961 |
+
<span>${episodesReversed ? '正序排列' : '倒序排列'}</span>
|
| 962 |
+
</button>
|
| 963 |
+
<span class="text-gray-400 text-sm">共 ${data.episodes.length} 集</span>
|
| 964 |
+
</div>
|
| 965 |
+
<button onclick="copyLinks()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors">
|
| 966 |
+
复制链接
|
| 967 |
+
</button>
|
| 968 |
+
</div>
|
| 969 |
+
<div id="episodesGrid" class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
|
| 970 |
+
${renderEpisodes(vod_name, sourceCode, id)}
|
| 971 |
+
</div>
|
| 972 |
+
`;
|
| 973 |
+
} else {
|
| 974 |
+
modalContent.innerHTML = `
|
| 975 |
+
<div class="text-center py-8">
|
| 976 |
+
<div class="text-red-400 mb-2">❌ 未找到播放资源</div>
|
| 977 |
+
<div class="text-gray-500 text-sm">该视频可能暂时无法播放,请尝试其他视频</div>
|
| 978 |
+
</div>
|
| 979 |
+
`;
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
modal.classList.remove('hidden');
|
| 983 |
+
} catch (error) {
|
| 984 |
+
console.error('获取详情错误:', error);
|
| 985 |
+
showToast('获取详情失败,请稍后重试', 'error');
|
| 986 |
+
} finally {
|
| 987 |
+
hideLoading();
|
| 988 |
+
}
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
// 更新播放视频函数,修改为使用/watch路径而不是直接打开player.html
|
| 992 |
+
function playVideo(url, vod_name, sourceCode, episodeIndex = 0, vodId = '') {
|
| 993 |
+
// 密码保护校验
|
| 994 |
+
if (window.isPasswordProtected && window.isPasswordVerified) {
|
| 995 |
+
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
|
| 996 |
+
showPasswordModal && showPasswordModal();
|
| 997 |
+
return;
|
| 998 |
+
}
|
| 999 |
+
}
|
| 1000 |
+
|
| 1001 |
+
// 获取当前路径作为返回页面
|
| 1002 |
+
let currentPath = window.location.href;
|
| 1003 |
+
|
| 1004 |
+
// 构建播放页面URL,使用watch.html作为中间跳转页
|
| 1005 |
+
let watchUrl = `watch.html?id=${vodId || ''}&source=${sourceCode || ''}&url=${encodeURIComponent(url)}&index=${episodeIndex}&title=${encodeURIComponent(vod_name || '')}`;
|
| 1006 |
+
|
| 1007 |
+
// 添加返回URL参数
|
| 1008 |
+
if (currentPath.includes('index.html') || currentPath.endsWith('/')) {
|
| 1009 |
+
watchUrl += `&back=${encodeURIComponent(currentPath)}`;
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
// 保存当前状态到localStorage
|
| 1013 |
+
try {
|
| 1014 |
+
localStorage.setItem('currentVideoTitle', vod_name || '未知视频');
|
| 1015 |
+
localStorage.setItem('currentEpisodes', JSON.stringify(currentEpisodes));
|
| 1016 |
+
localStorage.setItem('currentEpisodeIndex', episodeIndex);
|
| 1017 |
+
localStorage.setItem('currentSourceCode', sourceCode || '');
|
| 1018 |
+
localStorage.setItem('lastPlayTime', Date.now());
|
| 1019 |
+
localStorage.setItem('lastSearchPage', currentPath);
|
| 1020 |
+
localStorage.setItem('lastPageUrl', currentPath); // 确保保存返回页面URL
|
| 1021 |
+
} catch (e) {
|
| 1022 |
+
console.error('保存播放状态失败:', e);
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
// 在当前标签页中打开播放页面
|
| 1026 |
+
window.location.href = watchUrl;
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
// 弹出播放器页面
|
| 1030 |
+
function showVideoPlayer(url) {
|
| 1031 |
+
// 在打开播放器前,隐藏详情弹窗
|
| 1032 |
+
const detailModal = document.getElementById('modal');
|
| 1033 |
+
if (detailModal) {
|
| 1034 |
+
detailModal.classList.add('hidden');
|
| 1035 |
+
}
|
| 1036 |
+
// 临时隐藏搜索结果和豆瓣区域,防止高度超出播放器而出现滚动条
|
| 1037 |
+
document.getElementById('resultsArea').classList.add('hidden');
|
| 1038 |
+
document.getElementById('doubanArea').classList.add('hidden');
|
| 1039 |
+
// 在框架中打开播放页面
|
| 1040 |
+
videoPlayerFrame = document.createElement('iframe');
|
| 1041 |
+
videoPlayerFrame.id = 'VideoPlayerFrame';
|
| 1042 |
+
videoPlayerFrame.className = 'fixed w-full h-screen z-40';
|
| 1043 |
+
videoPlayerFrame.src = url;
|
| 1044 |
+
document.body.appendChild(videoPlayerFrame);
|
| 1045 |
+
// 将焦点移入iframe
|
| 1046 |
+
videoPlayerFrame.focus();
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
// 关闭播放器页面
|
| 1050 |
+
function closeVideoPlayer(home = false) {
|
| 1051 |
+
videoPlayerFrame = document.getElementById('VideoPlayerFrame');
|
| 1052 |
+
if (videoPlayerFrame) {
|
| 1053 |
+
videoPlayerFrame.remove();
|
| 1054 |
+
// 恢复搜索结果显示
|
| 1055 |
+
document.getElementById('resultsArea').classList.remove('hidden');
|
| 1056 |
+
// 关闭播放器时也隐藏详情弹窗
|
| 1057 |
+
const detailModal = document.getElementById('modal');
|
| 1058 |
+
if (detailModal) {
|
| 1059 |
+
detailModal.classList.add('hidden');
|
| 1060 |
+
}
|
| 1061 |
+
// 如果启用豆瓣区域则显示豆瓣区域
|
| 1062 |
+
if (localStorage.getItem('doubanEnabled') === 'true') {
|
| 1063 |
+
document.getElementById('doubanArea').classList.remove('hidden');
|
| 1064 |
+
}
|
| 1065 |
+
}
|
| 1066 |
+
if (home) {
|
| 1067 |
+
// 刷新主页
|
| 1068 |
+
window.location.href = '/'
|
| 1069 |
+
}
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
// 播放上一集
|
| 1073 |
+
function playPreviousEpisode(sourceCode) {
|
| 1074 |
+
if (currentEpisodeIndex > 0) {
|
| 1075 |
+
const prevIndex = currentEpisodeIndex - 1;
|
| 1076 |
+
const prevUrl = currentEpisodes[prevIndex];
|
| 1077 |
+
playVideo(prevUrl, currentVideoTitle, sourceCode, prevIndex);
|
| 1078 |
+
}
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
// 播放下一集
|
| 1082 |
+
function playNextEpisode(sourceCode) {
|
| 1083 |
+
if (currentEpisodeIndex < currentEpisodes.length - 1) {
|
| 1084 |
+
const nextIndex = currentEpisodeIndex + 1;
|
| 1085 |
+
const nextUrl = currentEpisodes[nextIndex];
|
| 1086 |
+
playVideo(nextUrl, currentVideoTitle, sourceCode, nextIndex);
|
| 1087 |
+
}
|
| 1088 |
+
}
|
| 1089 |
+
|
| 1090 |
+
// 处理播放器加载错误
|
| 1091 |
+
function handlePlayerError() {
|
| 1092 |
+
hideLoading();
|
| 1093 |
+
showToast('视频播放加载失败,请尝试其他视频源', 'error');
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
// 辅助函数用于渲染剧集按钮(使用当前的排序状态)
|
| 1097 |
+
function renderEpisodes(vodName, sourceCode, vodId) {
|
| 1098 |
+
const episodes = episodesReversed ? [...currentEpisodes].reverse() : currentEpisodes;
|
| 1099 |
+
return episodes.map((episode, index) => {
|
| 1100 |
+
// 根据倒序状态计算真实的剧集索引
|
| 1101 |
+
const realIndex = episodesReversed ? currentEpisodes.length - 1 - index : index;
|
| 1102 |
+
return `
|
| 1103 |
+
<button id="episode-${realIndex}" onclick="playVideo('${episode}','${vodName.replace(/"/g, '"')}', '${sourceCode}', ${realIndex}, '${vodId}')"
|
| 1104 |
+
class="px-4 py-2 bg-[#222] hover:bg-[#333] border border-[#333] rounded-lg transition-colors text-center episode-btn">
|
| 1105 |
+
${realIndex + 1}
|
| 1106 |
+
</button>
|
| 1107 |
+
`;
|
| 1108 |
+
}).join('');
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
// 复制视频链接到剪贴板
|
| 1112 |
+
function copyLinks() {
|
| 1113 |
+
const episodes = episodesReversed ? [...currentEpisodes].reverse() : currentEpisodes;
|
| 1114 |
+
const linkList = episodes.join('\r\n');
|
| 1115 |
+
navigator.clipboard.writeText(linkList).then(() => {
|
| 1116 |
+
showToast('播放链接已复制', 'success');
|
| 1117 |
+
}).catch(err => {
|
| 1118 |
+
showToast('复制失败,请检查浏览器权限', 'error');
|
| 1119 |
+
});
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
// 切换排序状态的函数
|
| 1123 |
+
function toggleEpisodeOrder(sourceCode, vodId) {
|
| 1124 |
+
episodesReversed = !episodesReversed;
|
| 1125 |
+
// 重新渲染剧集区域,使用 currentVideoTitle 作为视频标题
|
| 1126 |
+
const episodesGrid = document.getElementById('episodesGrid');
|
| 1127 |
+
if (episodesGrid) {
|
| 1128 |
+
episodesGrid.innerHTML = renderEpisodes(currentVideoTitle, sourceCode, vodId);
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
// 更新按钮文本和箭头方向
|
| 1132 |
+
const toggleBtn = document.querySelector(`button[onclick="toggleEpisodeOrder('${sourceCode}', '${vodId}')"]`);
|
| 1133 |
+
if (toggleBtn) {
|
| 1134 |
+
toggleBtn.querySelector('span').textContent = episodesReversed ? '正序排列' : '倒序排列';
|
| 1135 |
+
const arrowIcon = toggleBtn.querySelector('svg');
|
| 1136 |
+
if (arrowIcon) {
|
| 1137 |
+
arrowIcon.style.transform = episodesReversed ? 'rotate(180deg)' : 'rotate(0deg)';
|
| 1138 |
+
}
|
| 1139 |
+
}
|
| 1140 |
+
}
|
| 1141 |
+
|
| 1142 |
+
// 从URL导入配置
|
| 1143 |
+
async function importConfigFromUrl() {
|
| 1144 |
+
// 创建模态框元素
|
| 1145 |
+
let modal = document.getElementById('importUrlModal');
|
| 1146 |
+
if (modal) {
|
| 1147 |
+
document.body.removeChild(modal);
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
modal = document.createElement('div');
|
| 1151 |
+
modal.id = 'importUrlModal';
|
| 1152 |
+
modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40';
|
| 1153 |
+
|
| 1154 |
+
modal.innerHTML = `
|
| 1155 |
+
<div class="bg-[#191919] rounded-lg p-6 max-w-md w-full max-h-[90vh] overflow-y-auto relative">
|
| 1156 |
+
<button id="closeUrlModal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-xl">×</button>
|
| 1157 |
+
|
| 1158 |
+
<h3 class="text-xl font-bold mb-4">从URL导入配置</h3>
|
| 1159 |
+
|
| 1160 |
+
<div class="mb-4">
|
| 1161 |
+
<input type="text" id="configUrl" placeholder="输入配置文件URL"
|
| 1162 |
+
class="w-full px-3 py-2 bg-[#222] border border-[#333] rounded-lg text-white focus:outline-none focus:ring-1 focus:ring-blue-500">
|
| 1163 |
+
</div>
|
| 1164 |
+
|
| 1165 |
+
<div class="flex justify-end space-x-2">
|
| 1166 |
+
<button id="confirmUrlImport" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">导入</button>
|
| 1167 |
+
<button id="cancelUrlImport" class="bg-[#444] hover:bg-[#555] text-white px-4 py-2 rounded">取消</button>
|
| 1168 |
+
</div>
|
| 1169 |
+
</div>`;
|
| 1170 |
+
|
| 1171 |
+
document.body.appendChild(modal);
|
| 1172 |
+
|
| 1173 |
+
// 关闭按钮事件
|
| 1174 |
+
document.getElementById('closeUrlModal').addEventListener('click', () => {
|
| 1175 |
+
document.body.removeChild(modal);
|
| 1176 |
+
});
|
| 1177 |
+
|
| 1178 |
+
// 取消按钮事件
|
| 1179 |
+
document.getElementById('cancelUrlImport').addEventListener('click', () => {
|
| 1180 |
+
document.body.removeChild(modal);
|
| 1181 |
+
});
|
| 1182 |
+
|
| 1183 |
+
// 确认导入按钮事件
|
| 1184 |
+
document.getElementById('confirmUrlImport').addEventListener('click', async () => {
|
| 1185 |
+
const url = document.getElementById('configUrl').value.trim();
|
| 1186 |
+
if (!url) {
|
| 1187 |
+
showToast('请输入配置文件URL', 'warning');
|
| 1188 |
+
return;
|
| 1189 |
+
}
|
| 1190 |
+
|
| 1191 |
+
// 验证URL格式
|
| 1192 |
+
try {
|
| 1193 |
+
const urlObj = new URL(url);
|
| 1194 |
+
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
|
| 1195 |
+
showToast('URL必须以http://或https://开头', 'warning');
|
| 1196 |
+
return;
|
| 1197 |
+
}
|
| 1198 |
+
} catch (e) {
|
| 1199 |
+
showToast('URL格式不正确', 'warning');
|
| 1200 |
+
return;
|
| 1201 |
+
}
|
| 1202 |
+
|
| 1203 |
+
showLoading('正在从URL导入配置...');
|
| 1204 |
+
|
| 1205 |
+
try {
|
| 1206 |
+
// 获取配置文件 - 直接请求URL
|
| 1207 |
+
const response = await fetch(url, {
|
| 1208 |
+
mode: 'cors',
|
| 1209 |
+
headers: {
|
| 1210 |
+
'Accept': 'application/json'
|
| 1211 |
+
}
|
| 1212 |
+
});
|
| 1213 |
+
if (!response.ok) throw '获取配置文件失败';
|
| 1214 |
+
|
| 1215 |
+
// 验证响应内容类型
|
| 1216 |
+
const contentType = response.headers.get('content-type');
|
| 1217 |
+
if (!contentType || !contentType.includes('application/json')) {
|
| 1218 |
+
throw '响应不是有效的JSON格式';
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
const config = await response.json();
|
| 1222 |
+
if (config.name !== 'LibreTV-Settings') throw '配置文件格式不正确';
|
| 1223 |
+
|
| 1224 |
+
// 验证哈希
|
| 1225 |
+
const dataHash = await sha256(JSON.stringify(config.data));
|
| 1226 |
+
if (dataHash !== config.hash) throw '配置文件哈希值不匹配';
|
| 1227 |
+
|
| 1228 |
+
// 导入配置
|
| 1229 |
+
for (let item in config.data) {
|
| 1230 |
+
localStorage.setItem(item, config.data[item]);
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
showToast('配置文件导入成功,3 秒后自动刷新本页面。', 'success');
|
| 1234 |
+
setTimeout(() => {
|
| 1235 |
+
window.location.reload();
|
| 1236 |
+
}, 3000);
|
| 1237 |
+
} catch (error) {
|
| 1238 |
+
const message = typeof error === 'string' ? error : '导入配置失败';
|
| 1239 |
+
showToast(`从URL导入配置出错 (${message})`, 'error');
|
| 1240 |
+
} finally {
|
| 1241 |
+
hideLoading();
|
| 1242 |
+
document.body.removeChild(modal);
|
| 1243 |
+
}
|
| 1244 |
+
});
|
| 1245 |
+
|
| 1246 |
+
// 点击模态框外部关闭
|
| 1247 |
+
modal.addEventListener('click', (e) => {
|
| 1248 |
+
if (e.target === modal) {
|
| 1249 |
+
document.body.removeChild(modal);
|
| 1250 |
+
}
|
| 1251 |
+
});
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
// 配置文件导入功能
|
| 1255 |
+
async function importConfig() {
|
| 1256 |
+
showImportBox(async (file) => {
|
| 1257 |
+
try {
|
| 1258 |
+
// 检查文件类型
|
| 1259 |
+
if (!(file.type === 'application/json' || file.name.endsWith('.json'))) throw '文件类型不正确';
|
| 1260 |
+
|
| 1261 |
+
// 检查文件大小
|
| 1262 |
+
if (file.size > 1024 * 1024 * 10) throw new Error('文件大小超过 10MB');
|
| 1263 |
+
|
| 1264 |
+
// 读取文件内容
|
| 1265 |
+
const content = await new Promise((resolve, reject) => {
|
| 1266 |
+
const reader = new FileReader();
|
| 1267 |
+
reader.onload = () => resolve(reader.result);
|
| 1268 |
+
reader.onerror = () => reject('文件读取失败');
|
| 1269 |
+
reader.readAsText(file);
|
| 1270 |
+
});
|
| 1271 |
+
|
| 1272 |
+
// 解析并验证配置
|
| 1273 |
+
const config = JSON.parse(content);
|
| 1274 |
+
if (config.name !== 'LibreTV-Settings') throw '配置文件格式不正确';
|
| 1275 |
+
|
| 1276 |
+
// 验证哈希
|
| 1277 |
+
const dataHash = await sha256(JSON.stringify(config.data));
|
| 1278 |
+
if (dataHash !== config.hash) throw '配置文件哈希值不匹配';
|
| 1279 |
+
|
| 1280 |
+
// 导入配置
|
| 1281 |
+
for (let item in config.data) {
|
| 1282 |
+
localStorage.setItem(item, config.data[item]);
|
| 1283 |
+
}
|
| 1284 |
+
|
| 1285 |
+
showToast('配置文件导入成功,3 秒后自动刷新本页面。', 'success');
|
| 1286 |
+
setTimeout(() => {
|
| 1287 |
+
window.location.reload();
|
| 1288 |
+
}, 3000);
|
| 1289 |
+
} catch (error) {
|
| 1290 |
+
const message = typeof error === 'string' ? error : '配置文件格式错误';
|
| 1291 |
+
showToast(`配置文件读取出错 (${message})`, 'error');
|
| 1292 |
+
}
|
| 1293 |
+
});
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
// 配置文件导出功能
|
| 1297 |
+
async function exportConfig() {
|
| 1298 |
+
// 存储配置数据
|
| 1299 |
+
const config = {};
|
| 1300 |
+
const items = {};
|
| 1301 |
+
|
| 1302 |
+
const settingsToExport = [
|
| 1303 |
+
'selectedAPIs',
|
| 1304 |
+
'customAPIs',
|
| 1305 |
+
'yellowFilterEnabled',
|
| 1306 |
+
'adFilteringEnabled',
|
| 1307 |
+
'doubanEnabled',
|
| 1308 |
+
'hasInitializedDefaults'
|
| 1309 |
+
];
|
| 1310 |
+
|
| 1311 |
+
// 导出设置项
|
| 1312 |
+
settingsToExport.forEach(key => {
|
| 1313 |
+
const value = localStorage.getItem(key);
|
| 1314 |
+
if (value !== null) {
|
| 1315 |
+
items[key] = value;
|
| 1316 |
+
}
|
| 1317 |
+
});
|
| 1318 |
+
|
| 1319 |
+
// 导出历史记录
|
| 1320 |
+
const viewingHistory = localStorage.getItem('viewingHistory');
|
| 1321 |
+
if (viewingHistory) {
|
| 1322 |
+
items['viewingHistory'] = viewingHistory;
|
| 1323 |
+
}
|
| 1324 |
+
|
| 1325 |
+
const searchHistory = localStorage.getItem(SEARCH_HISTORY_KEY);
|
| 1326 |
+
if (searchHistory) {
|
| 1327 |
+
items[SEARCH_HISTORY_KEY] = searchHistory;
|
| 1328 |
+
}
|
| 1329 |
+
|
| 1330 |
+
const times = Date.now().toString();
|
| 1331 |
+
config['name'] = 'LibreTV-Settings'; // 配置文件名,用于校验
|
| 1332 |
+
config['time'] = times; // 配置文件生成时间
|
| 1333 |
+
config['cfgVer'] = '1.0.0'; // 配置文件版本
|
| 1334 |
+
config['data'] = items; // 配置文件数据
|
| 1335 |
+
config['hash'] = await sha256(JSON.stringify(config['data'])); // 计算数据的哈希值,用于校验
|
| 1336 |
+
|
| 1337 |
+
// 将配置数据保存为 JSON 文件
|
| 1338 |
+
saveStringAsFile(JSON.stringify(config), 'LibreTV-Settings_' + times + '.json');
|
| 1339 |
+
}
|
| 1340 |
+
|
| 1341 |
+
// 将字符串保存为文件
|
| 1342 |
+
function saveStringAsFile(content, fileName) {
|
| 1343 |
+
// 创建Blob对象并指定类型
|
| 1344 |
+
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
| 1345 |
+
// 生成临时URL
|
| 1346 |
+
const url = window.URL.createObjectURL(blob);
|
| 1347 |
+
// 创建<a>标签并触发下载
|
| 1348 |
+
const a = document.createElement('a');
|
| 1349 |
+
a.href = url;
|
| 1350 |
+
a.download = fileName;
|
| 1351 |
+
document.body.appendChild(a);
|
| 1352 |
+
a.click();
|
| 1353 |
+
// 清理临时对象
|
| 1354 |
+
document.body.removeChild(a);
|
| 1355 |
+
window.URL.revokeObjectURL(url);
|
| 1356 |
+
}
|
| 1357 |
+
|
| 1358 |
+
// 移除Node.js的require语句,因为这是在浏览器环境中运行的
|
js/config.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 全局常量配置
|
| 2 |
+
const PROXY_URL = '/proxy/'; // 适用于 Cloudflare, Netlify (带重写), Vercel (带重写)
|
| 3 |
+
// const HOPLAYER_URL = 'https://hoplayer.com/index.html';
|
| 4 |
+
const SEARCH_HISTORY_KEY = 'videoSearchHistory';
|
| 5 |
+
const MAX_HISTORY_ITEMS = 5;
|
| 6 |
+
|
| 7 |
+
// 密码保护配置
|
| 8 |
+
const PASSWORD_CONFIG = {
|
| 9 |
+
localStorageKey: 'passwordVerified', // 存储验证状态的键名
|
| 10 |
+
verificationTTL: 90 * 24 * 60 * 60 * 1000, // 验证有效期(90天,约3个月)
|
| 11 |
+
adminLocalStorageKey: 'adminPasswordVerified' // 新增的管理员验证状态的键名
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
// 网站信息配置
|
| 15 |
+
const SITE_CONFIG = {
|
| 16 |
+
name: 'LibreTV',
|
| 17 |
+
url: 'https://libretv.is-an.org',
|
| 18 |
+
description: '免费在线视频搜索与观看平台',
|
| 19 |
+
logo: 'image/logo.png',
|
| 20 |
+
version: '1.0.3'
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
// API站点配置
|
| 24 |
+
const API_SITES = {
|
| 25 |
+
dyttzy: {
|
| 26 |
+
api: 'http://caiji.dyttzyapi.com/api.php/provide/vod',
|
| 27 |
+
name: '电影天堂资源',
|
| 28 |
+
detail: 'http://caiji.dyttzyapi.com',
|
| 29 |
+
},
|
| 30 |
+
ruyi: {
|
| 31 |
+
api: 'https://cj.rycjapi.com/api.php/provide/vod',
|
| 32 |
+
name: '如意资源',
|
| 33 |
+
},
|
| 34 |
+
bfzy: {
|
| 35 |
+
api: 'https://bfzyapi.com/api.php/provide/vod',
|
| 36 |
+
name: '暴风资源',
|
| 37 |
+
},
|
| 38 |
+
tyyszy: {
|
| 39 |
+
api: 'https://tyyszy.com/api.php/provide/vod',
|
| 40 |
+
name: '天涯资源',
|
| 41 |
+
},
|
| 42 |
+
xiaomaomi: {
|
| 43 |
+
api: 'https://zy.xmm.hk/api.php/provide/vod',
|
| 44 |
+
name: '小猫咪资源',
|
| 45 |
+
},
|
| 46 |
+
ffzy: {
|
| 47 |
+
api: 'http://ffzy5.tv/api.php/provide/vod',
|
| 48 |
+
name: '非凡影视',
|
| 49 |
+
detail: 'http://ffzy5.tv',
|
| 50 |
+
},
|
| 51 |
+
heimuer: {
|
| 52 |
+
api: 'https://json.heimuer.xyz/api.php/provide/vod',
|
| 53 |
+
name: '黑木耳',
|
| 54 |
+
detail: 'https://heimuer.tv',
|
| 55 |
+
},
|
| 56 |
+
zy360: {
|
| 57 |
+
api: 'https://360zy.com/api.php/provide/vod',
|
| 58 |
+
name: '360资源',
|
| 59 |
+
},
|
| 60 |
+
iqiyi: {
|
| 61 |
+
api: 'https://www.iqiyizyapi.com/api.php/provide/vod',
|
| 62 |
+
name: 'iqiyi资源',
|
| 63 |
+
},
|
| 64 |
+
wolong: {
|
| 65 |
+
api: 'https://wolongzyw.com/api.php/provide/vod',
|
| 66 |
+
name: '卧龙资源',
|
| 67 |
+
},
|
| 68 |
+
hwba: {
|
| 69 |
+
api: 'https://cjhwba.com/api.php/provide/vod',
|
| 70 |
+
name: '华为吧资源',
|
| 71 |
+
},
|
| 72 |
+
jisu: {
|
| 73 |
+
api: 'https://jszyapi.com/api.php/provide/vod',
|
| 74 |
+
name: '极速资源',
|
| 75 |
+
detail: 'https://jszyapi.com',
|
| 76 |
+
},
|
| 77 |
+
dbzy: {
|
| 78 |
+
api: 'https://dbzy.tv/api.php/provide/vod',
|
| 79 |
+
name: '豆瓣资源',
|
| 80 |
+
},
|
| 81 |
+
mozhua: {
|
| 82 |
+
api: 'https://mozhuazy.com/api.php/provide/vod',
|
| 83 |
+
name: '魔爪资源',
|
| 84 |
+
},
|
| 85 |
+
mdzy: {
|
| 86 |
+
api: 'https://www.mdzyapi.com/api.php/provide/vod',
|
| 87 |
+
name: '魔都资源',
|
| 88 |
+
},
|
| 89 |
+
zuid: {
|
| 90 |
+
api: 'https://api.zuidapi.com/api.php/provide/vod',
|
| 91 |
+
name: '最大资源'
|
| 92 |
+
},
|
| 93 |
+
yinghua: {
|
| 94 |
+
api: 'https://m3u8.apiyhzy.com/api.php/provide/vod',
|
| 95 |
+
name: '樱花资源'
|
| 96 |
+
},
|
| 97 |
+
baidu: {
|
| 98 |
+
api: 'https://api.apibdzy.com/api.php/provide/vod',
|
| 99 |
+
name: '百度云资源'
|
| 100 |
+
},
|
| 101 |
+
wujin: {
|
| 102 |
+
api: 'https://api.wujinapi.me/api.php/provide/vod',
|
| 103 |
+
name: '无尽资源'
|
| 104 |
+
},
|
| 105 |
+
wwzy: {
|
| 106 |
+
api: 'https://wwzy.tv/api.php/provide/vod',
|
| 107 |
+
name: '旺旺短剧'
|
| 108 |
+
},
|
| 109 |
+
ikun: {
|
| 110 |
+
api: 'https://ikunzyapi.com/api.php/provide/vod',
|
| 111 |
+
name: 'iKun资源'
|
| 112 |
+
},
|
| 113 |
+
lzi: {
|
| 114 |
+
api: 'https://cj.lziapi.com/api.php/provide/vod/',
|
| 115 |
+
name: '量子资源站'
|
| 116 |
+
},
|
| 117 |
+
testSource: {
|
| 118 |
+
api: 'https://www.example.com/api.php/provide/vod',
|
| 119 |
+
name: '空内容测试源',
|
| 120 |
+
adult: true
|
| 121 |
+
},
|
| 122 |
+
// 下面是一些成人内容的API源,默认隐藏,使用本项目浏览黄色内容违背项目初衷
|
| 123 |
+
// 互联网上传播的色情内容将人彻底客体化、工具化,是性别解放和人类平等道路上的巨大障碍。
|
| 124 |
+
// 这些黄色影片是资本主义父权制压迫的最恶毒体现,它将暴力和屈辱商品化,践踏人的尊严,对受害者造成无法弥愈的伤害,并毒害社会关系。
|
| 125 |
+
// 资本为了利润,不惜将最卑劣的剥削(包括对受害者和表演者的剥削)和暴力商品化,
|
| 126 |
+
// 把性别剥削塑造成“性享受”麻痹观众的意识,转移我们对现实生活中矛盾和压迫的注意力。
|
| 127 |
+
// 这些影片和背后的产业已经使数百万男女“下海”,出卖自己的身体,甚至以此为生计。
|
| 128 |
+
// 而作为观众无辜吗?毫无疑问,他们促成了黄色产业链的再生产。
|
| 129 |
+
// 我们提供此警告,是希望您能认清这些内容的本质——它们是压迫和奴役的工具,而非娱乐。
|
| 130 |
+
// ckzy: {
|
| 131 |
+
// api: 'https://www.ckzy1.com',
|
| 132 |
+
// name: 'CK资源',
|
| 133 |
+
// adult: true
|
| 134 |
+
// },
|
| 135 |
+
// jkun: {
|
| 136 |
+
// api: 'https://jkunzyapi.com',
|
| 137 |
+
// name: 'jkun资源',
|
| 138 |
+
// adult: true
|
| 139 |
+
// },
|
| 140 |
+
// bwzy: {
|
| 141 |
+
// api: 'https://api.bwzym3u8.com',
|
| 142 |
+
// name: '百万资源',
|
| 143 |
+
// adult: true
|
| 144 |
+
// },
|
| 145 |
+
// souav: {
|
| 146 |
+
// api: 'https://api.souavzy.vip',
|
| 147 |
+
// name: 'souav资源',
|
| 148 |
+
// adult: true
|
| 149 |
+
// },
|
| 150 |
+
// r155: {
|
| 151 |
+
// api: 'https://155api.com',
|
| 152 |
+
// name: '155资源',
|
| 153 |
+
// adult: true
|
| 154 |
+
// },
|
| 155 |
+
// lsb: {
|
| 156 |
+
// api: 'https://apilsbzy1.com',
|
| 157 |
+
// name: 'lsb资源',
|
| 158 |
+
// adult: true
|
| 159 |
+
// },
|
| 160 |
+
// huangcang: {
|
| 161 |
+
// api: 'https://hsckzy.vip',
|
| 162 |
+
// name: '黄色仓库',
|
| 163 |
+
// adult: true,
|
| 164 |
+
// detail: 'https://hsckzy.vip'
|
| 165 |
+
// },
|
| 166 |
+
// yutu: {
|
| 167 |
+
// api: 'https://yutuzy10.com',
|
| 168 |
+
// name: '玉兔资源',
|
| 169 |
+
// adult: true
|
| 170 |
+
// },
|
| 171 |
+
|
| 172 |
+
// 下面是资源失效率高的API源,不建议使用
|
| 173 |
+
// subo: {
|
| 174 |
+
// api: 'https://subocaiji.com/api.php/provide/vod',
|
| 175 |
+
// name: '速播资源'
|
| 176 |
+
// },
|
| 177 |
+
// fczy: {
|
| 178 |
+
// api: 'https://api.fczy888.me/api.php/provide/vod',
|
| 179 |
+
// name: '蜂巢资源'
|
| 180 |
+
// },
|
| 181 |
+
// ukzy: {
|
| 182 |
+
// api: 'https://api.ukuapi88.com/api.php/provide/vod',
|
| 183 |
+
// name: 'U酷资源'
|
| 184 |
+
// },
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
// 定义合并方法
|
| 188 |
+
function extendAPISites(newSites) {
|
| 189 |
+
Object.assign(API_SITES, newSites);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// 暴露到全局
|
| 193 |
+
window.API_SITES = API_SITES;
|
| 194 |
+
window.extendAPISites = extendAPISites;
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
// 添加聚合搜索的配置选项
|
| 198 |
+
const AGGREGATED_SEARCH_CONFIG = {
|
| 199 |
+
enabled: true, // 是否启用聚合搜索
|
| 200 |
+
timeout: 8000, // 单个源超时时间(毫秒)
|
| 201 |
+
maxResults: 10000, // 最大结果数量
|
| 202 |
+
parallelRequests: true, // 是否并行请求所有源
|
| 203 |
+
showSourceBadges: true // 是否显示来源徽章
|
| 204 |
+
};
|
| 205 |
+
|
| 206 |
+
// 抽象API请求配置
|
| 207 |
+
const API_CONFIG = {
|
| 208 |
+
search: {
|
| 209 |
+
// 只拼接参数部分,不再包含 /api.php/provide/vod/
|
| 210 |
+
path: '?ac=videolist&wd=',
|
| 211 |
+
pagePath: '?ac=videolist&wd={query}&pg={page}',
|
| 212 |
+
maxPages: 50, // 最大获取页数
|
| 213 |
+
headers: {
|
| 214 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
| 215 |
+
'Accept': 'application/json'
|
| 216 |
+
}
|
| 217 |
+
},
|
| 218 |
+
detail: {
|
| 219 |
+
// 只拼接参数部分
|
| 220 |
+
path: '?ac=videolist&ids=',
|
| 221 |
+
headers: {
|
| 222 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
| 223 |
+
'Accept': 'application/json'
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
};
|
| 227 |
+
|
| 228 |
+
// 优化后的正则表达式模式
|
| 229 |
+
const M3U8_PATTERN = /\$https?:\/\/[^"'\s]+?\.m3u8/g;
|
| 230 |
+
|
| 231 |
+
// 添加自定义播放器URL
|
| 232 |
+
const CUSTOM_PLAYER_URL = 'player.html'; // 使用相对路径引用本地player.html
|
| 233 |
+
|
| 234 |
+
// 增加视频播放相关配置
|
| 235 |
+
const PLAYER_CONFIG = {
|
| 236 |
+
autoplay: true,
|
| 237 |
+
allowFullscreen: true,
|
| 238 |
+
width: '100%',
|
| 239 |
+
height: '600',
|
| 240 |
+
timeout: 15000, // 播放器加载超时时间
|
| 241 |
+
filterAds: true, // 是否启用广告过滤
|
| 242 |
+
autoPlayNext: true, // 默认启用自动连播功能
|
| 243 |
+
adFilteringEnabled: true, // 默认开启分片广告过滤
|
| 244 |
+
adFilteringStorage: 'adFilteringEnabled' // 存储广告过滤设置的键名
|
| 245 |
+
};
|
| 246 |
+
|
| 247 |
+
// 增加错误信息本地化
|
| 248 |
+
const ERROR_MESSAGES = {
|
| 249 |
+
NETWORK_ERROR: '网络连接错误,请检查网络设置',
|
| 250 |
+
TIMEOUT_ERROR: '请求超时,服务器响应时间过长',
|
| 251 |
+
API_ERROR: 'API接口返回错误,请尝试更换数据源',
|
| 252 |
+
PLAYER_ERROR: '播放器加载失败,请尝试其他视频源',
|
| 253 |
+
UNKNOWN_ERROR: '发生未知错误,请刷新页面重试'
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
// 添加进一步安全设置
|
| 257 |
+
const SECURITY_CONFIG = {
|
| 258 |
+
enableXSSProtection: true, // 是否启用XSS保护
|
| 259 |
+
sanitizeUrls: true, // 是否清理URL
|
| 260 |
+
maxQueryLength: 100, // 最大搜索长度
|
| 261 |
+
// allowedApiDomains 不再需要,因为所有请求都通过内部代理
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
// 添加多个自定义API源的配置
|
| 265 |
+
const CUSTOM_API_CONFIG = {
|
| 266 |
+
separator: ',', // 分隔符
|
| 267 |
+
maxSources: 5, // 最大允许的自定义源数量
|
| 268 |
+
testTimeout: 5000, // 测试超时时间(毫秒)
|
| 269 |
+
namePrefix: 'Custom-', // 自定义源名称前缀
|
| 270 |
+
validateUrl: true, // 验证URL格式
|
| 271 |
+
cacheResults: true, // 缓存测试结果
|
| 272 |
+
cacheExpiry: 5184000000, // 缓存过期时间(2个月)
|
| 273 |
+
adultPropName: 'isAdult' // 用于标记成人内容的属性名
|
| 274 |
+
};
|
| 275 |
+
|
| 276 |
+
// 隐藏内置黄色采集站API的变量
|
| 277 |
+
const HIDE_BUILTIN_ADULT_APIS = false;
|
js/customer_site.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CUSTOMER_SITES = {
|
| 2 |
+
qiqi: {
|
| 3 |
+
api: 'https://www.qiqidys.com/api.php/provide/vod',
|
| 4 |
+
name: '七七资源',
|
| 5 |
+
}
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
// 调用全局方法合并
|
| 9 |
+
if (window.extendAPISites) {
|
| 10 |
+
window.extendAPISites(CUSTOMER_SITES);
|
| 11 |
+
} else {
|
| 12 |
+
console.error("错误:请先加载 config.js!");
|
| 13 |
+
}
|
js/douban.js
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 豆瓣热门电影电视剧推荐功能
|
| 2 |
+
|
| 3 |
+
// 豆瓣标签列表 - 修改为默认标签
|
| 4 |
+
let defaultMovieTags = ['热门', '最新', '经典', '豆瓣高分', '冷门佳片', '华语', '欧美', '韩国', '日本', '动作', '喜剧', '爱情', '科幻', '悬疑', '恐怖', '治愈'];
|
| 5 |
+
let defaultTvTags = ['热门', '美剧', '英剧', '韩剧', '日剧', '国产剧', '港剧', '日本动画', '综艺', '纪录片'];
|
| 6 |
+
|
| 7 |
+
// 用户标签列表 - 存储用户实际使用的标签(包含保留的系统标签和用户添加的自定义标签)
|
| 8 |
+
let movieTags = [];
|
| 9 |
+
let tvTags = [];
|
| 10 |
+
|
| 11 |
+
// 加载用户标签
|
| 12 |
+
function loadUserTags() {
|
| 13 |
+
try {
|
| 14 |
+
// 尝试从本地存储加载用户保存的标签
|
| 15 |
+
const savedMovieTags = localStorage.getItem('userMovieTags');
|
| 16 |
+
const savedTvTags = localStorage.getItem('userTvTags');
|
| 17 |
+
|
| 18 |
+
// 如果本地存储中有标签数据,则使用它
|
| 19 |
+
if (savedMovieTags) {
|
| 20 |
+
movieTags = JSON.parse(savedMovieTags);
|
| 21 |
+
} else {
|
| 22 |
+
// 否则使用默认标签
|
| 23 |
+
movieTags = [...defaultMovieTags];
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
if (savedTvTags) {
|
| 27 |
+
tvTags = JSON.parse(savedTvTags);
|
| 28 |
+
} else {
|
| 29 |
+
// 否则使用默认标签
|
| 30 |
+
tvTags = [...defaultTvTags];
|
| 31 |
+
}
|
| 32 |
+
} catch (e) {
|
| 33 |
+
console.error('加载标签失败:', e);
|
| 34 |
+
// 初始化为默认值,防止错误
|
| 35 |
+
movieTags = [...defaultMovieTags];
|
| 36 |
+
tvTags = [...defaultTvTags];
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// 保存用户标签
|
| 41 |
+
function saveUserTags() {
|
| 42 |
+
try {
|
| 43 |
+
localStorage.setItem('userMovieTags', JSON.stringify(movieTags));
|
| 44 |
+
localStorage.setItem('userTvTags', JSON.stringify(tvTags));
|
| 45 |
+
} catch (e) {
|
| 46 |
+
console.error('保存标签失败:', e);
|
| 47 |
+
showToast('保存标签失败', 'error');
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
let doubanMovieTvCurrentSwitch = 'movie';
|
| 52 |
+
let doubanCurrentTag = '热门';
|
| 53 |
+
let doubanPageStart = 0;
|
| 54 |
+
const doubanPageSize = 16; // 一次显示的项目数量
|
| 55 |
+
|
| 56 |
+
// 初始化豆瓣功能
|
| 57 |
+
function initDouban() {
|
| 58 |
+
// 设置豆瓣开关的初始状态
|
| 59 |
+
const doubanToggle = document.getElementById('doubanToggle');
|
| 60 |
+
if (doubanToggle) {
|
| 61 |
+
const isEnabled = localStorage.getItem('doubanEnabled') === 'true';
|
| 62 |
+
doubanToggle.checked = isEnabled;
|
| 63 |
+
|
| 64 |
+
// 设置开关外观
|
| 65 |
+
const toggleBg = doubanToggle.nextElementSibling;
|
| 66 |
+
const toggleDot = toggleBg.nextElementSibling;
|
| 67 |
+
if (isEnabled) {
|
| 68 |
+
toggleBg.classList.add('bg-pink-600');
|
| 69 |
+
toggleDot.classList.add('translate-x-6');
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// 添加事件监听
|
| 73 |
+
doubanToggle.addEventListener('change', function(e) {
|
| 74 |
+
const isChecked = e.target.checked;
|
| 75 |
+
localStorage.setItem('doubanEnabled', isChecked);
|
| 76 |
+
|
| 77 |
+
// 更新开关外观
|
| 78 |
+
if (isChecked) {
|
| 79 |
+
toggleBg.classList.add('bg-pink-600');
|
| 80 |
+
toggleDot.classList.add('translate-x-6');
|
| 81 |
+
} else {
|
| 82 |
+
toggleBg.classList.remove('bg-pink-600');
|
| 83 |
+
toggleDot.classList.remove('translate-x-6');
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// 更新显示状态
|
| 87 |
+
updateDoubanVisibility();
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
// 初始更新显示状态
|
| 91 |
+
updateDoubanVisibility();
|
| 92 |
+
|
| 93 |
+
// 滚动到页面顶部
|
| 94 |
+
window.scrollTo(0, 0);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// 加载用户标签
|
| 98 |
+
loadUserTags();
|
| 99 |
+
|
| 100 |
+
// 渲染电影/电视剧切换
|
| 101 |
+
renderDoubanMovieTvSwitch();
|
| 102 |
+
|
| 103 |
+
// 渲染豆瓣标签
|
| 104 |
+
renderDoubanTags();
|
| 105 |
+
|
| 106 |
+
// 换一批按钮事件监听
|
| 107 |
+
setupDoubanRefreshBtn();
|
| 108 |
+
|
| 109 |
+
// 初始加载热门内容
|
| 110 |
+
if (localStorage.getItem('doubanEnabled') === 'true') {
|
| 111 |
+
renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// 根据设置更新豆瓣区域的显示状态
|
| 116 |
+
function updateDoubanVisibility() {
|
| 117 |
+
const doubanArea = document.getElementById('doubanArea');
|
| 118 |
+
if (!doubanArea) return;
|
| 119 |
+
|
| 120 |
+
const isEnabled = localStorage.getItem('doubanEnabled') === 'true';
|
| 121 |
+
const isSearching = document.getElementById('resultsArea') &&
|
| 122 |
+
!document.getElementById('resultsArea').classList.contains('hidden');
|
| 123 |
+
|
| 124 |
+
// 只有在启用且没有搜索结果显示时才显示豆瓣区域
|
| 125 |
+
if (isEnabled && !isSearching) {
|
| 126 |
+
doubanArea.classList.remove('hidden');
|
| 127 |
+
// 如果豆瓣结果为空,重新加载
|
| 128 |
+
if (document.getElementById('douban-results').children.length === 0) {
|
| 129 |
+
renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
|
| 130 |
+
}
|
| 131 |
+
} else {
|
| 132 |
+
doubanArea.classList.add('hidden');
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// 只填充搜索框,不执行搜索,让用户自主决定搜索时机
|
| 137 |
+
function fillSearchInput(title) {
|
| 138 |
+
if (!title) return;
|
| 139 |
+
|
| 140 |
+
// 安全处理标题,防止XSS
|
| 141 |
+
const safeTitle = title
|
| 142 |
+
.replace(/</g, '<')
|
| 143 |
+
.replace(/>/g, '>')
|
| 144 |
+
.replace(/"/g, '"');
|
| 145 |
+
|
| 146 |
+
const input = document.getElementById('searchInput');
|
| 147 |
+
if (input) {
|
| 148 |
+
input.value = safeTitle;
|
| 149 |
+
|
| 150 |
+
// 聚焦搜索框,便于用户立即使用键盘操作
|
| 151 |
+
input.focus();
|
| 152 |
+
|
| 153 |
+
// 显示一个提示,告知用户点击搜索按钮进行搜索
|
| 154 |
+
showToast('已填充搜索内容,点击搜索按钮开始搜索', 'info');
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// 填充搜索框并执行搜索
|
| 159 |
+
function fillAndSearch(title) {
|
| 160 |
+
if (!title) return;
|
| 161 |
+
|
| 162 |
+
// 安全处理标题,防止XSS
|
| 163 |
+
const safeTitle = title
|
| 164 |
+
.replace(/</g, '<')
|
| 165 |
+
.replace(/>/g, '>')
|
| 166 |
+
.replace(/"/g, '"');
|
| 167 |
+
|
| 168 |
+
const input = document.getElementById('searchInput');
|
| 169 |
+
if (input) {
|
| 170 |
+
input.value = safeTitle;
|
| 171 |
+
search(); // 使用已有的search函数执行搜索
|
| 172 |
+
|
| 173 |
+
// 同时更新浏览器URL,使其反映当前的搜索状态
|
| 174 |
+
try {
|
| 175 |
+
// 使用URI编码确保特殊字符能够正确显示
|
| 176 |
+
const encodedQuery = encodeURIComponent(safeTitle);
|
| 177 |
+
// 使用HTML5 History API更新URL,不刷新页面
|
| 178 |
+
window.history.pushState(
|
| 179 |
+
{ search: safeTitle },
|
| 180 |
+
`搜索: ${safeTitle} - LibreTV`,
|
| 181 |
+
`/s=${encodedQuery}`
|
| 182 |
+
);
|
| 183 |
+
// 更新页面标题
|
| 184 |
+
document.title = `搜索: ${safeTitle} - LibreTV`;
|
| 185 |
+
} catch (e) {
|
| 186 |
+
console.error('更新浏览器历史失败:', e);
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// 填充搜索框,确保豆瓣资源API被选中,然后执行搜索
|
| 192 |
+
async function fillAndSearchWithDouban(title) {
|
| 193 |
+
if (!title) return;
|
| 194 |
+
|
| 195 |
+
// 安全处理标题,防止XSS
|
| 196 |
+
const safeTitle = title
|
| 197 |
+
.replace(/</g, '<')
|
| 198 |
+
.replace(/>/g, '>')
|
| 199 |
+
.replace(/"/g, '"');
|
| 200 |
+
|
| 201 |
+
// 确保豆瓣资源API被选中
|
| 202 |
+
if (typeof selectedAPIs !== 'undefined' && !selectedAPIs.includes('dbzy')) {
|
| 203 |
+
// 在设置中勾选豆瓣资源API复选框
|
| 204 |
+
const doubanCheckbox = document.querySelector('input[id="api_dbzy"]');
|
| 205 |
+
if (doubanCheckbox) {
|
| 206 |
+
doubanCheckbox.checked = true;
|
| 207 |
+
|
| 208 |
+
// 触发updateSelectedAPIs函数以更新状态
|
| 209 |
+
if (typeof updateSelectedAPIs === 'function') {
|
| 210 |
+
updateSelectedAPIs();
|
| 211 |
+
} else {
|
| 212 |
+
// 如果函数不可用,则手动添加到selectedAPIs
|
| 213 |
+
selectedAPIs.push('dbzy');
|
| 214 |
+
localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
|
| 215 |
+
|
| 216 |
+
// 更新选中API计数(如果有这个元素)
|
| 217 |
+
const countEl = document.getElementById('selectedAPICount');
|
| 218 |
+
if (countEl) {
|
| 219 |
+
countEl.textContent = selectedAPIs.length;
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
showToast('已自动选择豆瓣资源API', 'info');
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// 填充搜索框并执行搜索
|
| 228 |
+
const input = document.getElementById('searchInput');
|
| 229 |
+
if (input) {
|
| 230 |
+
input.value = safeTitle;
|
| 231 |
+
await search(); // 使用已有的search函数执行搜索
|
| 232 |
+
|
| 233 |
+
// 更新浏览器URL,使其反映当前的搜索状态
|
| 234 |
+
try {
|
| 235 |
+
// 使用URI编码确保特殊字符能够正确显示
|
| 236 |
+
const encodedQuery = encodeURIComponent(safeTitle);
|
| 237 |
+
// 使用HTML5 History API更新URL,不刷新页面
|
| 238 |
+
window.history.pushState(
|
| 239 |
+
{ search: safeTitle },
|
| 240 |
+
`搜索: ${safeTitle} - LibreTV`,
|
| 241 |
+
`/s=${encodedQuery}`
|
| 242 |
+
);
|
| 243 |
+
// 更新页面标题
|
| 244 |
+
document.title = `搜索: ${safeTitle} - LibreTV`;
|
| 245 |
+
} catch (e) {
|
| 246 |
+
console.error('更新浏览器历史失败:', e);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
if (window.innerWidth <= 768) {
|
| 250 |
+
window.scrollTo({
|
| 251 |
+
top: 0,
|
| 252 |
+
behavior: 'smooth'
|
| 253 |
+
});
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// 渲染电影/电视剧切换器
|
| 259 |
+
function renderDoubanMovieTvSwitch() {
|
| 260 |
+
// 获取切换按钮元素
|
| 261 |
+
const movieToggle = document.getElementById('douban-movie-toggle');
|
| 262 |
+
const tvToggle = document.getElementById('douban-tv-toggle');
|
| 263 |
+
|
| 264 |
+
if (!movieToggle ||!tvToggle) return;
|
| 265 |
+
|
| 266 |
+
movieToggle.addEventListener('click', function() {
|
| 267 |
+
if (doubanMovieTvCurrentSwitch !== 'movie') {
|
| 268 |
+
// 更新按钮样式
|
| 269 |
+
movieToggle.classList.add('bg-pink-600', 'text-white');
|
| 270 |
+
movieToggle.classList.remove('text-gray-300');
|
| 271 |
+
|
| 272 |
+
tvToggle.classList.remove('bg-pink-600', 'text-white');
|
| 273 |
+
tvToggle.classList.add('text-gray-300');
|
| 274 |
+
|
| 275 |
+
doubanMovieTvCurrentSwitch = 'movie';
|
| 276 |
+
doubanCurrentTag = '热门';
|
| 277 |
+
|
| 278 |
+
// 重新加载豆瓣内容
|
| 279 |
+
renderDoubanTags(movieTags);
|
| 280 |
+
|
| 281 |
+
// 换一批按钮事件监听
|
| 282 |
+
setupDoubanRefreshBtn();
|
| 283 |
+
|
| 284 |
+
// 初始加载热门内容
|
| 285 |
+
if (localStorage.getItem('doubanEnabled') === 'true') {
|
| 286 |
+
renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
// 电视剧按钮点击事件
|
| 292 |
+
tvToggle.addEventListener('click', function() {
|
| 293 |
+
if (doubanMovieTvCurrentSwitch !== 'tv') {
|
| 294 |
+
// 更新按钮样式
|
| 295 |
+
tvToggle.classList.add('bg-pink-600', 'text-white');
|
| 296 |
+
tvToggle.classList.remove('text-gray-300');
|
| 297 |
+
|
| 298 |
+
movieToggle.classList.remove('bg-pink-600', 'text-white');
|
| 299 |
+
movieToggle.classList.add('text-gray-300');
|
| 300 |
+
|
| 301 |
+
doubanMovieTvCurrentSwitch = 'tv';
|
| 302 |
+
doubanCurrentTag = '热门';
|
| 303 |
+
|
| 304 |
+
// 重新加载豆瓣内容
|
| 305 |
+
renderDoubanTags(tvTags);
|
| 306 |
+
|
| 307 |
+
// 换一批按钮事件监听
|
| 308 |
+
setupDoubanRefreshBtn();
|
| 309 |
+
|
| 310 |
+
// 初始加载热门内容
|
| 311 |
+
if (localStorage.getItem('doubanEnabled') === 'true') {
|
| 312 |
+
renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
});
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
// 渲染豆瓣标签选择器
|
| 319 |
+
function renderDoubanTags(tags) {
|
| 320 |
+
const tagContainer = document.getElementById('douban-tags');
|
| 321 |
+
if (!tagContainer) return;
|
| 322 |
+
|
| 323 |
+
// 确定当前应该使用的标签列表
|
| 324 |
+
const currentTags = doubanMovieTvCurrentSwitch === 'movie' ? movieTags : tvTags;
|
| 325 |
+
|
| 326 |
+
// 清空标签容器
|
| 327 |
+
tagContainer.innerHTML = '';
|
| 328 |
+
|
| 329 |
+
// 先添加标签管理按钮
|
| 330 |
+
const manageBtn = document.createElement('button');
|
| 331 |
+
manageBtn.className = 'py-1.5 px-3.5 rounded text-sm font-medium transition-all duration-300 bg-[#1a1a1a] text-gray-300 hover:bg-pink-700 hover:text-white border border-[#333] hover:border-white';
|
| 332 |
+
manageBtn.innerHTML = '<span class="flex items-center"><svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>管理标签</span>';
|
| 333 |
+
manageBtn.onclick = function() {
|
| 334 |
+
showTagManageModal();
|
| 335 |
+
};
|
| 336 |
+
tagContainer.appendChild(manageBtn);
|
| 337 |
+
|
| 338 |
+
// 添加所有标签
|
| 339 |
+
currentTags.forEach(tag => {
|
| 340 |
+
const btn = document.createElement('button');
|
| 341 |
+
|
| 342 |
+
// 设置样式
|
| 343 |
+
let btnClass = 'py-1.5 px-3.5 rounded text-sm font-medium transition-all duration-300 border ';
|
| 344 |
+
|
| 345 |
+
// 当前选中的标签使用高亮样式
|
| 346 |
+
if (tag === doubanCurrentTag) {
|
| 347 |
+
btnClass += 'bg-pink-600 text-white shadow-md border-white';
|
| 348 |
+
} else {
|
| 349 |
+
btnClass += 'bg-[#1a1a1a] text-gray-300 hover:bg-pink-700 hover:text-white border-[#333] hover:border-white';
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
btn.className = btnClass;
|
| 353 |
+
btn.textContent = tag;
|
| 354 |
+
|
| 355 |
+
btn.onclick = function() {
|
| 356 |
+
if (doubanCurrentTag !== tag) {
|
| 357 |
+
doubanCurrentTag = tag;
|
| 358 |
+
doubanPageStart = 0;
|
| 359 |
+
renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
|
| 360 |
+
renderDoubanTags();
|
| 361 |
+
}
|
| 362 |
+
};
|
| 363 |
+
|
| 364 |
+
tagContainer.appendChild(btn);
|
| 365 |
+
});
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
// 设置换一批按钮事件
|
| 369 |
+
function setupDoubanRefreshBtn() {
|
| 370 |
+
// 修复ID,使用正确的ID douban-refresh 而不是 douban-refresh-btn
|
| 371 |
+
const btn = document.getElementById('douban-refresh');
|
| 372 |
+
if (!btn) return;
|
| 373 |
+
|
| 374 |
+
btn.onclick = function() {
|
| 375 |
+
doubanPageStart += doubanPageSize;
|
| 376 |
+
if (doubanPageStart > 9 * doubanPageSize) {
|
| 377 |
+
doubanPageStart = 0;
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
|
| 381 |
+
};
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
function fetchDoubanTags() {
|
| 385 |
+
const movieTagsTarget = `https://movie.douban.com/j/search_tags?type=movie`
|
| 386 |
+
fetchDoubanData(movieTagsTarget)
|
| 387 |
+
.then(data => {
|
| 388 |
+
movieTags = data.tags;
|
| 389 |
+
if (doubanMovieTvCurrentSwitch === 'movie') {
|
| 390 |
+
renderDoubanTags(movieTags);
|
| 391 |
+
}
|
| 392 |
+
})
|
| 393 |
+
.catch(error => {
|
| 394 |
+
console.error("获取豆瓣热门电影标签失败:", error);
|
| 395 |
+
});
|
| 396 |
+
const tvTagsTarget = `https://movie.douban.com/j/search_tags?type=tv`
|
| 397 |
+
fetchDoubanData(tvTagsTarget)
|
| 398 |
+
.then(data => {
|
| 399 |
+
tvTags = data.tags;
|
| 400 |
+
if (doubanMovieTvCurrentSwitch === 'tv') {
|
| 401 |
+
renderDoubanTags(tvTags);
|
| 402 |
+
}
|
| 403 |
+
})
|
| 404 |
+
.catch(error => {
|
| 405 |
+
console.error("获取豆瓣热门电视剧标签失败:", error);
|
| 406 |
+
});
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
// 渲染热门推荐内容
|
| 410 |
+
function renderRecommend(tag, pageLimit, pageStart) {
|
| 411 |
+
const container = document.getElementById("douban-results");
|
| 412 |
+
if (!container) return;
|
| 413 |
+
|
| 414 |
+
const loadingOverlayHTML = `
|
| 415 |
+
<div class="absolute inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-10">
|
| 416 |
+
<div class="flex items-center justify-center">
|
| 417 |
+
<div class="w-6 h-6 border-2 border-pink-500 border-t-transparent rounded-full animate-spin inline-block"></div>
|
| 418 |
+
<span class="text-pink-500 ml-4">加载中...</span>
|
| 419 |
+
</div>
|
| 420 |
+
</div>
|
| 421 |
+
`;
|
| 422 |
+
|
| 423 |
+
container.classList.add("relative");
|
| 424 |
+
container.insertAdjacentHTML('beforeend', loadingOverlayHTML);
|
| 425 |
+
|
| 426 |
+
const target = `https://movie.douban.com/j/search_subjects?type=${doubanMovieTvCurrentSwitch}&tag=${tag}&sort=recommend&page_limit=${pageLimit}&page_start=${pageStart}`;
|
| 427 |
+
|
| 428 |
+
// 使用通用请求函数
|
| 429 |
+
fetchDoubanData(target)
|
| 430 |
+
.then(data => {
|
| 431 |
+
renderDoubanCards(data, container);
|
| 432 |
+
})
|
| 433 |
+
.catch(error => {
|
| 434 |
+
console.error("获取豆瓣数据失败:", error);
|
| 435 |
+
container.innerHTML = `
|
| 436 |
+
<div class="col-span-full text-center py-8">
|
| 437 |
+
<div class="text-red-400">❌ 获取豆瓣数据失败,请稍后重试</div>
|
| 438 |
+
<div class="text-gray-500 text-sm mt-2">提示:使用VPN可能有助于解决此问题</div>
|
| 439 |
+
</div>
|
| 440 |
+
`;
|
| 441 |
+
});
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
async function fetchDoubanData(url) {
|
| 445 |
+
// 添加超时控制
|
| 446 |
+
const controller = new AbortController();
|
| 447 |
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
|
| 448 |
+
|
| 449 |
+
// 设置请求选项,包括信号和头部
|
| 450 |
+
const fetchOptions = {
|
| 451 |
+
signal: controller.signal,
|
| 452 |
+
headers: {
|
| 453 |
+
'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',
|
| 454 |
+
'Referer': 'https://movie.douban.com/',
|
| 455 |
+
'Accept': 'application/json, text/plain, */*',
|
| 456 |
+
}
|
| 457 |
+
};
|
| 458 |
+
|
| 459 |
+
try {
|
| 460 |
+
// 尝试直接访问(豆瓣API可能允许部分CORS请求)
|
| 461 |
+
const response = await fetch(PROXY_URL + encodeURIComponent(url), fetchOptions);
|
| 462 |
+
clearTimeout(timeoutId);
|
| 463 |
+
|
| 464 |
+
if (!response.ok) {
|
| 465 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
return await response.json();
|
| 469 |
+
} catch (err) {
|
| 470 |
+
console.error("豆瓣 API 请求失败(直接代理):", err);
|
| 471 |
+
|
| 472 |
+
// 失败后尝试备用方法:作为备选
|
| 473 |
+
const fallbackUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
|
| 474 |
+
|
| 475 |
+
try {
|
| 476 |
+
const fallbackResponse = await fetch(fallbackUrl);
|
| 477 |
+
|
| 478 |
+
if (!fallbackResponse.ok) {
|
| 479 |
+
throw new Error(`备用API请求失败! 状态: ${fallbackResponse.status}`);
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
const data = await fallbackResponse.json();
|
| 483 |
+
|
| 484 |
+
// 解析原始内容
|
| 485 |
+
if (data && data.contents) {
|
| 486 |
+
return JSON.parse(data.contents);
|
| 487 |
+
} else {
|
| 488 |
+
throw new Error("无法获取有效数据");
|
| 489 |
+
}
|
| 490 |
+
} catch (fallbackErr) {
|
| 491 |
+
console.error("豆瓣 API 备用请求也失败:", fallbackErr);
|
| 492 |
+
throw fallbackErr; // 向上抛出错误,让调用者处理
|
| 493 |
+
}
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
// 抽取渲染豆瓣卡片的逻辑到单独函数
|
| 498 |
+
function renderDoubanCards(data, container) {
|
| 499 |
+
// 创建文档片段以提高性能
|
| 500 |
+
const fragment = document.createDocumentFragment();
|
| 501 |
+
|
| 502 |
+
// 如果没有数据
|
| 503 |
+
if (!data.subjects || data.subjects.length === 0) {
|
| 504 |
+
const emptyEl = document.createElement("div");
|
| 505 |
+
emptyEl.className = "col-span-full text-center py-8";
|
| 506 |
+
emptyEl.innerHTML = `
|
| 507 |
+
<div class="text-pink-500">❌ 暂无数据,请尝试其他分类或刷新</div>
|
| 508 |
+
`;
|
| 509 |
+
fragment.appendChild(emptyEl);
|
| 510 |
+
} else {
|
| 511 |
+
// 循环创建每个影视卡片
|
| 512 |
+
data.subjects.forEach(item => {
|
| 513 |
+
const card = document.createElement("div");
|
| 514 |
+
card.className = "bg-[#111] hover:bg-[#222] transition-all duration-300 rounded-lg overflow-hidden flex flex-col transform hover:scale-105 shadow-md hover:shadow-lg";
|
| 515 |
+
|
| 516 |
+
// 生成卡片内容,确保安全显示(防止XSS)
|
| 517 |
+
const safeTitle = item.title
|
| 518 |
+
.replace(/</g, '<')
|
| 519 |
+
.replace(/>/g, '>')
|
| 520 |
+
.replace(/"/g, '"');
|
| 521 |
+
|
| 522 |
+
const safeRate = (item.rate || "暂无")
|
| 523 |
+
.replace(/</g, '<')
|
| 524 |
+
.replace(/>/g, '>');
|
| 525 |
+
|
| 526 |
+
// 处理图片URL
|
| 527 |
+
// 1. 直接使用豆瓣图片URL (添加no-referrer属性)
|
| 528 |
+
const originalCoverUrl = item.cover;
|
| 529 |
+
|
| 530 |
+
// 2. 也准备代理URL作为备选
|
| 531 |
+
const proxiedCoverUrl = PROXY_URL + encodeURIComponent(originalCoverUrl);
|
| 532 |
+
|
| 533 |
+
// 为不同设备优化卡片布局
|
| 534 |
+
card.innerHTML = `
|
| 535 |
+
<div class="relative w-full aspect-[2/3] overflow-hidden cursor-pointer" onclick="fillAndSearchWithDouban('${safeTitle}')">
|
| 536 |
+
<img src="${originalCoverUrl}" alt="${safeTitle}"
|
| 537 |
+
class="w-full h-full object-cover transition-transform duration-500 hover:scale-110"
|
| 538 |
+
onerror="this.onerror=null; this.src='${proxiedCoverUrl}'; this.classList.add('object-contain');"
|
| 539 |
+
loading="lazy" referrerpolicy="no-referrer">
|
| 540 |
+
<div class="absolute inset-0 bg-gradient-to-t from-black to-transparent opacity-60"></div>
|
| 541 |
+
<div class="absolute bottom-2 left-2 bg-black/70 text-white text-xs px-2 py-1 rounded-sm">
|
| 542 |
+
<span class="text-yellow-400">★</span> ${safeRate}
|
| 543 |
+
</div>
|
| 544 |
+
<div class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded-sm hover:bg-[#333] transition-colors">
|
| 545 |
+
<a href="${item.url}" target="_blank" rel="noopener noreferrer" title="在豆瓣查看" onclick="event.stopPropagation();">
|
| 546 |
+
🔗
|
| 547 |
+
</a>
|
| 548 |
+
</div>
|
| 549 |
+
</div>
|
| 550 |
+
<div class="p-2 text-center bg-[#111]">
|
| 551 |
+
<button onclick="fillAndSearchWithDouban('${safeTitle}')"
|
| 552 |
+
class="text-sm font-medium text-white truncate w-full hover:text-pink-400 transition"
|
| 553 |
+
title="${safeTitle}">
|
| 554 |
+
${safeTitle}
|
| 555 |
+
</button>
|
| 556 |
+
</div>
|
| 557 |
+
`;
|
| 558 |
+
|
| 559 |
+
fragment.appendChild(card);
|
| 560 |
+
});
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// 清空并添加所有新元素
|
| 564 |
+
container.innerHTML = "";
|
| 565 |
+
container.appendChild(fragment);
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
// 重置到首页
|
| 569 |
+
function resetToHome() {
|
| 570 |
+
resetSearchArea();
|
| 571 |
+
updateDoubanVisibility();
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
// 加载豆瓣首页内容
|
| 575 |
+
document.addEventListener('DOMContentLoaded', initDouban);
|
| 576 |
+
|
| 577 |
+
// 显示标签管理模态框
|
| 578 |
+
function showTagManageModal() {
|
| 579 |
+
// 确保模态框在页面上只有一个实例
|
| 580 |
+
let modal = document.getElementById('tagManageModal');
|
| 581 |
+
if (modal) {
|
| 582 |
+
document.body.removeChild(modal);
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
// 创建模态框元素
|
| 586 |
+
modal = document.createElement('div');
|
| 587 |
+
modal.id = 'tagManageModal';
|
| 588 |
+
modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40';
|
| 589 |
+
|
| 590 |
+
// 当前使用的标签类型和默认标签
|
| 591 |
+
const isMovie = doubanMovieTvCurrentSwitch === 'movie';
|
| 592 |
+
const currentTags = isMovie ? movieTags : tvTags;
|
| 593 |
+
const defaultTags = isMovie ? defaultMovieTags : defaultTvTags;
|
| 594 |
+
|
| 595 |
+
// 模态框内容
|
| 596 |
+
modal.innerHTML = `
|
| 597 |
+
<div class="bg-[#191919] rounded-lg p-6 max-w-md w-full max-h-[90vh] overflow-y-auto relative">
|
| 598 |
+
<button id="closeTagModal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-xl">×</button>
|
| 599 |
+
|
| 600 |
+
<h3 class="text-xl font-bold text-white mb-4">标签管理 (${isMovie ? '电影' : '电视剧'})</h3>
|
| 601 |
+
|
| 602 |
+
<div class="mb-4">
|
| 603 |
+
<div class="flex justify-between items-center mb-2">
|
| 604 |
+
<h4 class="text-lg font-medium text-gray-300">标签列表</h4>
|
| 605 |
+
<button id="resetTagsBtn" class="text-xs px-2 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded">
|
| 606 |
+
恢复默认标签
|
| 607 |
+
</button>
|
| 608 |
+
</div>
|
| 609 |
+
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-4" id="tagsGrid">
|
| 610 |
+
${currentTags.length ? currentTags.map(tag => {
|
| 611 |
+
// "热门"标签不能删除
|
| 612 |
+
const canDelete = tag !== '热门';
|
| 613 |
+
return `
|
| 614 |
+
<div class="bg-[#1a1a1a] text-gray-300 py-1.5 px-3 rounded text-sm font-medium flex justify-between items-center group">
|
| 615 |
+
<span>${tag}</span>
|
| 616 |
+
${canDelete ?
|
| 617 |
+
`<button class="delete-tag-btn text-gray-500 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
| 618 |
+
data-tag="${tag}">✕</button>` :
|
| 619 |
+
`<span class="text-gray-500 text-xs italic opacity-0 group-hover:opacity-100">必需</span>`
|
| 620 |
+
}
|
| 621 |
+
</div>
|
| 622 |
+
`;
|
| 623 |
+
}).join('') :
|
| 624 |
+
`<div class="col-span-full text-center py-4 text-gray-500">无标签,请添加或恢复默认</div>`}
|
| 625 |
+
</div>
|
| 626 |
+
</div>
|
| 627 |
+
|
| 628 |
+
<div class="border-t border-gray-700 pt-4">
|
| 629 |
+
<h4 class="text-lg font-medium text-gray-300 mb-3">添加新标签</h4>
|
| 630 |
+
<form id="addTagForm" class="flex items-center">
|
| 631 |
+
<input type="text" id="newTagInput" placeholder="输入标签名称..."
|
| 632 |
+
class="flex-1 bg-[#222] text-white border border-gray-700 rounded px-3 py-2 focus:outline-none focus:border-pink-500">
|
| 633 |
+
<button type="submit" class="ml-2 bg-pink-600 hover:bg-pink-700 text-white px-4 py-2 rounded">添加</button>
|
| 634 |
+
</form>
|
| 635 |
+
<p class="text-xs text-gray-500 mt-2">提示:标签名称不能为空,不能重复,不能包含特殊字符</p>
|
| 636 |
+
</div>
|
| 637 |
+
</div>
|
| 638 |
+
`;
|
| 639 |
+
|
| 640 |
+
// 添加模态框到页面
|
| 641 |
+
document.body.appendChild(modal);
|
| 642 |
+
|
| 643 |
+
// 焦点放在输入框上
|
| 644 |
+
setTimeout(() => {
|
| 645 |
+
document.getElementById('newTagInput').focus();
|
| 646 |
+
}, 100);
|
| 647 |
+
|
| 648 |
+
// 添加事件监听器 - 关闭按钮
|
| 649 |
+
document.getElementById('closeTagModal').addEventListener('click', function() {
|
| 650 |
+
document.body.removeChild(modal);
|
| 651 |
+
});
|
| 652 |
+
|
| 653 |
+
// 添加事件监听器 - 点击模态框外部关闭
|
| 654 |
+
modal.addEventListener('click', function(e) {
|
| 655 |
+
if (e.target === modal) {
|
| 656 |
+
document.body.removeChild(modal);
|
| 657 |
+
}
|
| 658 |
+
});
|
| 659 |
+
|
| 660 |
+
// 添加事件监听器 - 恢复默认标签按钮
|
| 661 |
+
document.getElementById('resetTagsBtn').addEventListener('click', function() {
|
| 662 |
+
resetTagsToDefault();
|
| 663 |
+
showTagManageModal(); // 重新加载模态框
|
| 664 |
+
});
|
| 665 |
+
|
| 666 |
+
// 添加事件监听器 - 删除标签按钮
|
| 667 |
+
const deleteButtons = document.querySelectorAll('.delete-tag-btn');
|
| 668 |
+
deleteButtons.forEach(btn => {
|
| 669 |
+
btn.addEventListener('click', function() {
|
| 670 |
+
const tagToDelete = this.getAttribute('data-tag');
|
| 671 |
+
deleteTag(tagToDelete);
|
| 672 |
+
showTagManageModal(); // 重新加载模态框
|
| 673 |
+
});
|
| 674 |
+
});
|
| 675 |
+
|
| 676 |
+
// 添加事件监听器 - 表单提交
|
| 677 |
+
document.getElementById('addTagForm').addEventListener('submit', function(e) {
|
| 678 |
+
e.preventDefault();
|
| 679 |
+
const input = document.getElementById('newTagInput');
|
| 680 |
+
const newTag = input.value.trim();
|
| 681 |
+
|
| 682 |
+
if (newTag) {
|
| 683 |
+
addTag(newTag);
|
| 684 |
+
input.value = '';
|
| 685 |
+
showTagManageModal(); // 重新加载模态框
|
| 686 |
+
}
|
| 687 |
+
});
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
// 添加标签
|
| 691 |
+
function addTag(tag) {
|
| 692 |
+
// 安全处理标签名,防止XSS
|
| 693 |
+
const safeTag = tag
|
| 694 |
+
.replace(/</g, '<')
|
| 695 |
+
.replace(/>/g, '>')
|
| 696 |
+
.replace(/"/g, '"');
|
| 697 |
+
|
| 698 |
+
// 确定当前使用的是电影还是电视剧标签
|
| 699 |
+
const isMovie = doubanMovieTvCurrentSwitch === 'movie';
|
| 700 |
+
const currentTags = isMovie ? movieTags : tvTags;
|
| 701 |
+
|
| 702 |
+
// 检查是否已存在(忽略大小写)
|
| 703 |
+
const exists = currentTags.some(
|
| 704 |
+
existingTag => existingTag.toLowerCase() === safeTag.toLowerCase()
|
| 705 |
+
);
|
| 706 |
+
|
| 707 |
+
if (exists) {
|
| 708 |
+
showToast('标签已存在', 'warning');
|
| 709 |
+
return;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
// 添加到对应的标签数组
|
| 713 |
+
if (isMovie) {
|
| 714 |
+
movieTags.push(safeTag);
|
| 715 |
+
} else {
|
| 716 |
+
tvTags.push(safeTag);
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
// 保存到本地存储
|
| 720 |
+
saveUserTags();
|
| 721 |
+
|
| 722 |
+
// 重新渲染标签
|
| 723 |
+
renderDoubanTags();
|
| 724 |
+
|
| 725 |
+
showToast('标签添加成功', 'success');
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
// 删除标签
|
| 729 |
+
function deleteTag(tag) {
|
| 730 |
+
// 热门标签不能删除
|
| 731 |
+
if (tag === '热门') {
|
| 732 |
+
showToast('热门标签不能删除', 'warning');
|
| 733 |
+
return;
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
// 确定当前使用的是电影还是电视剧标签
|
| 737 |
+
const isMovie = doubanMovieTvCurrentSwitch === 'movie';
|
| 738 |
+
const currentTags = isMovie ? movieTags : tvTags;
|
| 739 |
+
|
| 740 |
+
// 寻找标签索引
|
| 741 |
+
const index = currentTags.indexOf(tag);
|
| 742 |
+
|
| 743 |
+
// 如果找到标签,则删除
|
| 744 |
+
if (index !== -1) {
|
| 745 |
+
currentTags.splice(index, 1);
|
| 746 |
+
|
| 747 |
+
// 保存到本地存储
|
| 748 |
+
saveUserTags();
|
| 749 |
+
|
| 750 |
+
// 如果当前选中的是被删除的标签,则重置为"热门"
|
| 751 |
+
if (doubanCurrentTag === tag) {
|
| 752 |
+
doubanCurrentTag = '热门';
|
| 753 |
+
doubanPageStart = 0;
|
| 754 |
+
renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
// 重新渲染标签
|
| 758 |
+
renderDoubanTags();
|
| 759 |
+
|
| 760 |
+
showToast('标签删除成功', 'success');
|
| 761 |
+
}
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
// 重置为默认标签
|
| 765 |
+
function resetTagsToDefault() {
|
| 766 |
+
// 确定当前使用的是电影还是电视剧
|
| 767 |
+
const isMovie = doubanMovieTvCurrentSwitch === 'movie';
|
| 768 |
+
|
| 769 |
+
// 重置为默认标签
|
| 770 |
+
if (isMovie) {
|
| 771 |
+
movieTags = [...defaultMovieTags];
|
| 772 |
+
} else {
|
| 773 |
+
tvTags = [...defaultTvTags];
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
// 设置当前标签为热门
|
| 777 |
+
doubanCurrentTag = '热门';
|
| 778 |
+
doubanPageStart = 0;
|
| 779 |
+
|
| 780 |
+
// 保存到本地存储
|
| 781 |
+
saveUserTags();
|
| 782 |
+
|
| 783 |
+
// 重新渲染标签和内容
|
| 784 |
+
renderDoubanTags();
|
| 785 |
+
renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
|
| 786 |
+
|
| 787 |
+
showToast('已恢复默认标签', 'success');
|
| 788 |
+
}
|
js/index-page.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 页面加载后显示弹窗脚本
|
| 2 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 3 |
+
// 弹窗显示脚本
|
| 4 |
+
// 检查用户是否已经看过声明
|
| 5 |
+
const hasSeenDisclaimer = localStorage.getItem('hasSeenDisclaimer');
|
| 6 |
+
|
| 7 |
+
if (!hasSeenDisclaimer) {
|
| 8 |
+
// 显示弹窗
|
| 9 |
+
const disclaimerModal = document.getElementById('disclaimerModal');
|
| 10 |
+
disclaimerModal.style.display = 'flex';
|
| 11 |
+
|
| 12 |
+
// 添加接受按钮事件
|
| 13 |
+
document.getElementById('acceptDisclaimerBtn').addEventListener('click', function() {
|
| 14 |
+
// 保存用户已看过声明的状态
|
| 15 |
+
localStorage.setItem('hasSeenDisclaimer', 'true');
|
| 16 |
+
// 隐藏弹窗
|
| 17 |
+
disclaimerModal.style.display = 'none';
|
| 18 |
+
});
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// URL搜索参数处理脚本
|
| 22 |
+
// 首先检查是否是播放URL格式 (/watch 开头的路径)
|
| 23 |
+
if (window.location.pathname.startsWith('/watch')) {
|
| 24 |
+
// 播放URL,不做额外处理,watch.html会处理重定向
|
| 25 |
+
return;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// 检查页面路径中的搜索参数 (格式: /s=keyword)
|
| 29 |
+
const path = window.location.pathname;
|
| 30 |
+
const searchPrefix = '/s=';
|
| 31 |
+
|
| 32 |
+
if (path.startsWith(searchPrefix)) {
|
| 33 |
+
// 提取搜索关键词
|
| 34 |
+
const keyword = decodeURIComponent(path.substring(searchPrefix.length));
|
| 35 |
+
if (keyword) {
|
| 36 |
+
// 设置搜索框的值
|
| 37 |
+
document.getElementById('searchInput').value = keyword;
|
| 38 |
+
// 显示清空按钮
|
| 39 |
+
toggleClearButton();
|
| 40 |
+
// 执行搜索
|
| 41 |
+
setTimeout(() => {
|
| 42 |
+
// 使用setTimeout确保其他DOM加载和初始化完成
|
| 43 |
+
search();
|
| 44 |
+
// 更新浏览器历史,不改变URL (保持搜索参数在地址栏)
|
| 45 |
+
try {
|
| 46 |
+
window.history.replaceState(
|
| 47 |
+
{ search: keyword },
|
| 48 |
+
`搜索: ${keyword} - LibreTV`,
|
| 49 |
+
window.location.href
|
| 50 |
+
);
|
| 51 |
+
} catch (e) {
|
| 52 |
+
console.error('更新浏览器历史失败:', e);
|
| 53 |
+
}
|
| 54 |
+
}, 300);
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// 也检查查询字符串中的搜索参数 (格式: ?s=keyword)
|
| 59 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 60 |
+
const searchQuery = urlParams.get('s');
|
| 61 |
+
|
| 62 |
+
if (searchQuery) {
|
| 63 |
+
// 设置搜索框的值
|
| 64 |
+
document.getElementById('searchInput').value = searchQuery;
|
| 65 |
+
// 执行搜索
|
| 66 |
+
setTimeout(() => {
|
| 67 |
+
search();
|
| 68 |
+
// 更新URL为规范格式
|
| 69 |
+
try {
|
| 70 |
+
window.history.replaceState(
|
| 71 |
+
{ search: searchQuery },
|
| 72 |
+
`搜索: ${searchQuery} - LibreTV`,
|
| 73 |
+
`/s=${encodeURIComponent(searchQuery)}`
|
| 74 |
+
);
|
| 75 |
+
} catch (e) {
|
| 76 |
+
console.error('更新浏览器历史失败:', e);
|
| 77 |
+
}
|
| 78 |
+
}, 300);
|
| 79 |
+
}
|
| 80 |
+
});
|
js/password.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 密码保护功能
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* 检查是否设置了密码保护
|
| 5 |
+
* 通过读取页面上嵌入的环境变量来检查
|
| 6 |
+
*/
|
| 7 |
+
function isPasswordProtected() {
|
| 8 |
+
// 检查页面上嵌入的环境变量
|
| 9 |
+
const pwd = window.__ENV__ && window.__ENV__.PASSWORD;
|
| 10 |
+
const adminPwd = window.__ENV__ && window.__ENV__.ADMINPASSWORD;
|
| 11 |
+
|
| 12 |
+
// 检查普通密码或管理员密码是否有效
|
| 13 |
+
const isPwdValid = typeof pwd === 'string' && pwd.length === 64 && !/^0+$/.test(pwd);
|
| 14 |
+
const isAdminPwdValid = typeof adminPwd === 'string' && adminPwd.length === 64 && !/^0+$/.test(adminPwd);
|
| 15 |
+
|
| 16 |
+
// 任意一个密码有效即认为启用了密码保护
|
| 17 |
+
return isPwdValid || isAdminPwdValid;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
window.isPasswordProtected = isPasswordProtected;
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* 验证用户输入的密码是否正确(异步,使用SHA-256哈希)
|
| 24 |
+
*/
|
| 25 |
+
// 统一验证函数
|
| 26 |
+
async function verifyPassword(password, passwordType = 'PASSWORD') {
|
| 27 |
+
try {
|
| 28 |
+
const correctHash = window.__ENV__?.[passwordType];
|
| 29 |
+
if (!correctHash) return false;
|
| 30 |
+
|
| 31 |
+
const inputHash = await sha256(password);
|
| 32 |
+
const isValid = inputHash === correctHash;
|
| 33 |
+
|
| 34 |
+
if (isValid) {
|
| 35 |
+
const storageKey = passwordType === 'PASSWORD'
|
| 36 |
+
? PASSWORD_CONFIG.localStorageKey
|
| 37 |
+
: PASSWORD_CONFIG.adminLocalStorageKey;
|
| 38 |
+
|
| 39 |
+
localStorage.setItem(storageKey, JSON.stringify({
|
| 40 |
+
verified: true,
|
| 41 |
+
timestamp: Date.now(),
|
| 42 |
+
passwordHash: correctHash
|
| 43 |
+
}));
|
| 44 |
+
}
|
| 45 |
+
return isValid;
|
| 46 |
+
} catch (error) {
|
| 47 |
+
console.error(`验证${passwordType}密码时出错:`, error);
|
| 48 |
+
return false;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// 统一验证状态检查
|
| 53 |
+
function isVerified(passwordType = 'PASSWORD') {
|
| 54 |
+
try {
|
| 55 |
+
if (!isPasswordProtected()) return true;
|
| 56 |
+
|
| 57 |
+
const storageKey = passwordType === 'PASSWORD'
|
| 58 |
+
? PASSWORD_CONFIG.localStorageKey
|
| 59 |
+
: PASSWORD_CONFIG.adminLocalStorageKey;
|
| 60 |
+
|
| 61 |
+
const stored = localStorage.getItem(storageKey);
|
| 62 |
+
if (!stored) return false;
|
| 63 |
+
|
| 64 |
+
const { timestamp, passwordHash } = JSON.parse(stored);
|
| 65 |
+
const currentHash = window.__ENV__?.[passwordType];
|
| 66 |
+
|
| 67 |
+
return timestamp && passwordHash === currentHash &&
|
| 68 |
+
Date.now() - timestamp < PASSWORD_CONFIG.verificationTTL;
|
| 69 |
+
} catch (error) {
|
| 70 |
+
console.error(`检查${passwordType}验证状态时出错:`, error);
|
| 71 |
+
return false;
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// 更新全局导出
|
| 76 |
+
window.isPasswordProtected = isPasswordProtected;
|
| 77 |
+
window.isPasswordVerified = () => isVerified('PASSWORD');
|
| 78 |
+
window.isAdminVerified = () => isVerified('ADMINPASSWORD');
|
| 79 |
+
window.verifyPassword = verifyPassword;
|
| 80 |
+
|
| 81 |
+
// SHA-256实现,可用Web Crypto API
|
| 82 |
+
async function sha256(message) {
|
| 83 |
+
if (window.crypto && crypto.subtle && crypto.subtle.digest) {
|
| 84 |
+
const msgBuffer = new TextEncoder().encode(message);
|
| 85 |
+
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
| 86 |
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
| 87 |
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
| 88 |
+
}
|
| 89 |
+
// HTTP 下调用原始 js‑sha256
|
| 90 |
+
if (typeof window._jsSha256 === 'function') {
|
| 91 |
+
return window._jsSha256(message);
|
| 92 |
+
}
|
| 93 |
+
throw new Error('No SHA-256 implementation available.');
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* 显示密码验证弹窗
|
| 98 |
+
*/
|
| 99 |
+
function showPasswordModal() {
|
| 100 |
+
const passwordModal = document.getElementById('passwordModal');
|
| 101 |
+
if (passwordModal) {
|
| 102 |
+
// 防止出现豆瓣区域滚动条
|
| 103 |
+
document.getElementById('doubanArea').classList.add('hidden');
|
| 104 |
+
document.getElementById('passwordCancelBtn').classList.add('hidden');
|
| 105 |
+
|
| 106 |
+
passwordModal.style.display = 'flex';
|
| 107 |
+
|
| 108 |
+
// 确保输入框获取焦点
|
| 109 |
+
setTimeout(() => {
|
| 110 |
+
const passwordInput = document.getElementById('passwordInput');
|
| 111 |
+
if (passwordInput) {
|
| 112 |
+
passwordInput.focus();
|
| 113 |
+
}
|
| 114 |
+
}, 100);
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* 隐藏密码验证弹窗
|
| 120 |
+
*/
|
| 121 |
+
function hidePasswordModal() {
|
| 122 |
+
const passwordModal = document.getElementById('passwordModal');
|
| 123 |
+
if (passwordModal) {
|
| 124 |
+
// 隐藏密码错误提示
|
| 125 |
+
hidePasswordError();
|
| 126 |
+
|
| 127 |
+
// 清空密码输入框
|
| 128 |
+
const passwordInput = document.getElementById('passwordInput');
|
| 129 |
+
if (passwordInput) passwordInput.value = '';
|
| 130 |
+
|
| 131 |
+
passwordModal.style.display = 'none';
|
| 132 |
+
|
| 133 |
+
// 如果启用豆瓣区域则显示豆瓣区域
|
| 134 |
+
if (localStorage.getItem('doubanEnabled') === 'true') {
|
| 135 |
+
document.getElementById('doubanArea').classList.remove('hidden');
|
| 136 |
+
initDouban();
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* 显示密码错误信息
|
| 143 |
+
*/
|
| 144 |
+
function showPasswordError() {
|
| 145 |
+
const errorElement = document.getElementById('passwordError');
|
| 146 |
+
if (errorElement) {
|
| 147 |
+
errorElement.classList.remove('hidden');
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
/**
|
| 152 |
+
* 隐藏密码错误信息
|
| 153 |
+
*/
|
| 154 |
+
function hidePasswordError() {
|
| 155 |
+
const errorElement = document.getElementById('passwordError');
|
| 156 |
+
if (errorElement) {
|
| 157 |
+
errorElement.classList.add('hidden');
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/**
|
| 162 |
+
* 处理密码提交事件(异步)
|
| 163 |
+
*/
|
| 164 |
+
async function handlePasswordSubmit() {
|
| 165 |
+
const passwordInput = document.getElementById('passwordInput');
|
| 166 |
+
const password = passwordInput ? passwordInput.value.trim() : '';
|
| 167 |
+
if (await verifyPassword(password)) {
|
| 168 |
+
hidePasswordModal();
|
| 169 |
+
|
| 170 |
+
// 触发密码验证成功事件
|
| 171 |
+
document.dispatchEvent(new CustomEvent('passwordVerified'));
|
| 172 |
+
} else {
|
| 173 |
+
showPasswordError();
|
| 174 |
+
if (passwordInput) {
|
| 175 |
+
passwordInput.value = '';
|
| 176 |
+
passwordInput.focus();
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* 初始化密码验证系统(需适配异步事件)
|
| 183 |
+
*/
|
| 184 |
+
// 修改initPasswordProtection函数
|
| 185 |
+
function initPasswordProtection() {
|
| 186 |
+
if (!isPasswordProtected()) {
|
| 187 |
+
return;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// 检查是否有普通密码
|
| 191 |
+
const hasNormalPassword = window.__ENV__?.PASSWORD &&
|
| 192 |
+
window.__ENV__.PASSWORD.length === 64 &&
|
| 193 |
+
!/^0+$/.test(window.__ENV__.PASSWORD);
|
| 194 |
+
|
| 195 |
+
// 只有当设置了普通密码且未验证时才显示密码框
|
| 196 |
+
if (hasNormalPassword && !isPasswordVerified()) {
|
| 197 |
+
showPasswordModal();
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
// 设置按钮事件监听
|
| 201 |
+
const settingsBtn = document.querySelector('[onclick="toggleSettings(event)"]');
|
| 202 |
+
if (settingsBtn) {
|
| 203 |
+
settingsBtn.addEventListener('click', function(e) {
|
| 204 |
+
// 只有当设置了普通密码且未验证时才拦截点击
|
| 205 |
+
if (hasNormalPassword && !isPasswordVerified()) {
|
| 206 |
+
e.preventDefault();
|
| 207 |
+
e.stopPropagation();
|
| 208 |
+
showPasswordModal();
|
| 209 |
+
return;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
});
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
// 设置按钮密码框验证
|
| 217 |
+
function showAdminPasswordModal() {
|
| 218 |
+
const passwordModal = document.getElementById('passwordModal');
|
| 219 |
+
if (!passwordModal) return;
|
| 220 |
+
|
| 221 |
+
// 清空密码输入框
|
| 222 |
+
const passwordInput = document.getElementById('passwordInput');
|
| 223 |
+
if (passwordInput) passwordInput.value = '';
|
| 224 |
+
|
| 225 |
+
// 修改标题为管理员验证
|
| 226 |
+
const title = passwordModal.querySelector('h2');
|
| 227 |
+
if (title) title.textContent = '管理员验证';
|
| 228 |
+
|
| 229 |
+
document.getElementById('passwordCancelBtn').classList.remove('hidden');
|
| 230 |
+
passwordModal.style.display = 'flex';
|
| 231 |
+
|
| 232 |
+
// 设置表单提交处理
|
| 233 |
+
const form = document.getElementById('passwordForm');
|
| 234 |
+
if (form) {
|
| 235 |
+
form.onsubmit = async function (e) {
|
| 236 |
+
e.preventDefault();
|
| 237 |
+
const password = document.getElementById('passwordInput').value.trim();
|
| 238 |
+
if (await verifyPassword(password, 'ADMINPASSWORD')) {
|
| 239 |
+
passwordModal.style.display = 'none';
|
| 240 |
+
document.getElementById('settingsPanel').classList.add('show');
|
| 241 |
+
} else {
|
| 242 |
+
showPasswordError();
|
| 243 |
+
}
|
| 244 |
+
};
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// 在页面加载完成后初始化密码保护
|
| 249 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 250 |
+
initPasswordProtection();
|
| 251 |
+
});
|
| 252 |
+
|
| 253 |
+
|
js/player.js
ADDED
|
@@ -0,0 +1,1661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const selectedAPIs = JSON.parse(localStorage.getItem('selectedAPIs') || '[]');
|
| 2 |
+
const customAPIs = JSON.parse(localStorage.getItem('customAPIs') || '[]'); // 存储自定义API列表
|
| 3 |
+
|
| 4 |
+
// 改进返回功能
|
| 5 |
+
function goBack(event) {
|
| 6 |
+
// 防止默认链接行为
|
| 7 |
+
if (event) event.preventDefault();
|
| 8 |
+
|
| 9 |
+
// 1. 优先检查URL参数中的returnUrl
|
| 10 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 11 |
+
const returnUrl = urlParams.get('returnUrl');
|
| 12 |
+
|
| 13 |
+
if (returnUrl) {
|
| 14 |
+
// 如果URL中有returnUrl参数,优先使用
|
| 15 |
+
window.location.href = decodeURIComponent(returnUrl);
|
| 16 |
+
return;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// 2. 检查localStorage中保存的lastPageUrl
|
| 20 |
+
const lastPageUrl = localStorage.getItem('lastPageUrl');
|
| 21 |
+
if (lastPageUrl && lastPageUrl !== window.location.href) {
|
| 22 |
+
window.location.href = lastPageUrl;
|
| 23 |
+
return;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// 3. 检查是否是从搜索页面进入的播放器
|
| 27 |
+
const referrer = document.referrer;
|
| 28 |
+
|
| 29 |
+
// 检查 referrer 是否包含搜索参数
|
| 30 |
+
if (referrer && (referrer.includes('/s=') || referrer.includes('?s='))) {
|
| 31 |
+
// 如果是从搜索页面来的,返回到搜索页面
|
| 32 |
+
window.location.href = referrer;
|
| 33 |
+
return;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// 4. 如果是在iframe中打开的,尝试关闭iframe
|
| 37 |
+
if (window.self !== window.top) {
|
| 38 |
+
try {
|
| 39 |
+
// 尝试调用父窗口的关闭播放器函数
|
| 40 |
+
window.parent.closeVideoPlayer && window.parent.closeVideoPlayer();
|
| 41 |
+
return;
|
| 42 |
+
} catch (e) {
|
| 43 |
+
console.error('调用父窗口closeVideoPlayer失败:', e);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// 5. 无法确定上一页,则返回首页
|
| 48 |
+
if (!referrer || referrer === '') {
|
| 49 |
+
window.location.href = '/';
|
| 50 |
+
return;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// 6. 以上都不满足,使用默认行为:返回上一页
|
| 54 |
+
window.history.back();
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// 页面加载时保存当前URL到localStorage,作为返回目标
|
| 58 |
+
window.addEventListener('load', function () {
|
| 59 |
+
// 保存前一页面URL
|
| 60 |
+
if (document.referrer && document.referrer !== window.location.href) {
|
| 61 |
+
localStorage.setItem('lastPageUrl', document.referrer);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// 提取当前URL中的重要参数,以便在需要时能够恢复当前页面
|
| 65 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 66 |
+
const videoId = urlParams.get('id');
|
| 67 |
+
const sourceCode = urlParams.get('source');
|
| 68 |
+
|
| 69 |
+
if (videoId && sourceCode) {
|
| 70 |
+
// 保存当前播放状态,以便其他页面可以返回
|
| 71 |
+
localStorage.setItem('currentPlayingId', videoId);
|
| 72 |
+
localStorage.setItem('currentPlayingSource', sourceCode);
|
| 73 |
+
}
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
// =================================
|
| 78 |
+
// ============== PLAYER ==========
|
| 79 |
+
// =================================
|
| 80 |
+
// 全局变量
|
| 81 |
+
let currentVideoTitle = '';
|
| 82 |
+
let currentEpisodeIndex = 0;
|
| 83 |
+
let art = null; // 用于 ArtPlayer 实例
|
| 84 |
+
let currentHls = null; // 跟踪当前HLS实例
|
| 85 |
+
let currentEpisodes = [];
|
| 86 |
+
let episodesReversed = false;
|
| 87 |
+
let autoplayEnabled = true; // 默认开启自动连播
|
| 88 |
+
let videoHasEnded = false; // 跟踪视频是否已经自然结束
|
| 89 |
+
let userClickedPosition = null; // 记录用户点击的位置
|
| 90 |
+
let shortcutHintTimeout = null; // 用于控制快捷键提示显示时间
|
| 91 |
+
let adFilteringEnabled = true; // 默认开启广告过滤
|
| 92 |
+
let progressSaveInterval = null; // 定期保存进度的计时器
|
| 93 |
+
let currentVideoUrl = ''; // 记录当前实际的视频URL
|
| 94 |
+
const isWebkit = (typeof window.webkitConvertPointFromNodeToPage === 'function')
|
| 95 |
+
Artplayer.FULLSCREEN_WEB_IN_BODY = true;
|
| 96 |
+
|
| 97 |
+
// 页面加载
|
| 98 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 99 |
+
// 先检查用户是否已通过密码验证
|
| 100 |
+
if (!isPasswordVerified()) {
|
| 101 |
+
// 隐藏加载提示
|
| 102 |
+
document.getElementById('player-loading').style.display = 'none';
|
| 103 |
+
return;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
initializePageContent();
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
// 监听密码验证成功事件
|
| 110 |
+
document.addEventListener('passwordVerified', () => {
|
| 111 |
+
document.getElementById('player-loading').style.display = 'block';
|
| 112 |
+
|
| 113 |
+
initializePageContent();
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
// 初始化页面内容
|
| 117 |
+
function initializePageContent() {
|
| 118 |
+
|
| 119 |
+
// 解析URL参数
|
| 120 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 121 |
+
let videoUrl = urlParams.get('url');
|
| 122 |
+
const title = urlParams.get('title');
|
| 123 |
+
const sourceCode = urlParams.get('source');
|
| 124 |
+
let index = parseInt(urlParams.get('index') || '0');
|
| 125 |
+
const episodesList = urlParams.get('episodes'); // 从URL获取集数信息
|
| 126 |
+
const savedPosition = parseInt(urlParams.get('position') || '0'); // 获取保存的播放位置
|
| 127 |
+
// 解决历史记录问题:检查URL是否是player.html开头的链接
|
| 128 |
+
// 如果是,说明这是历史记录重定向,需要解析真实的视频URL
|
| 129 |
+
if (videoUrl && videoUrl.includes('player.html')) {
|
| 130 |
+
try {
|
| 131 |
+
// 尝试从嵌套URL中提取真实的视频链接
|
| 132 |
+
const nestedUrlParams = new URLSearchParams(videoUrl.split('?')[1]);
|
| 133 |
+
// 从嵌套参数中获取真实视频URL
|
| 134 |
+
const nestedVideoUrl = nestedUrlParams.get('url');
|
| 135 |
+
// 检查嵌套URL是否包含播放位置信息
|
| 136 |
+
const nestedPosition = nestedUrlParams.get('position');
|
| 137 |
+
const nestedIndex = nestedUrlParams.get('index');
|
| 138 |
+
const nestedTitle = nestedUrlParams.get('title');
|
| 139 |
+
|
| 140 |
+
if (nestedVideoUrl) {
|
| 141 |
+
videoUrl = nestedVideoUrl;
|
| 142 |
+
|
| 143 |
+
// 更新当前URL参数
|
| 144 |
+
const url = new URL(window.location.href);
|
| 145 |
+
if (!urlParams.has('position') && nestedPosition) {
|
| 146 |
+
url.searchParams.set('position', nestedPosition);
|
| 147 |
+
}
|
| 148 |
+
if (!urlParams.has('index') && nestedIndex) {
|
| 149 |
+
url.searchParams.set('index', nestedIndex);
|
| 150 |
+
}
|
| 151 |
+
if (!urlParams.has('title') && nestedTitle) {
|
| 152 |
+
url.searchParams.set('title', nestedTitle);
|
| 153 |
+
}
|
| 154 |
+
// 替换当前URL
|
| 155 |
+
window.history.replaceState({}, '', url);
|
| 156 |
+
} else {
|
| 157 |
+
showError('历史记录链接无效,请返回首页重新访问');
|
| 158 |
+
}
|
| 159 |
+
} catch (e) {
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// 保存当前视频URL
|
| 164 |
+
currentVideoUrl = videoUrl || '';
|
| 165 |
+
|
| 166 |
+
// 从localStorage获取数据
|
| 167 |
+
currentVideoTitle = title || localStorage.getItem('currentVideoTitle') || '未知视频';
|
| 168 |
+
currentEpisodeIndex = index;
|
| 169 |
+
|
| 170 |
+
// 设置自动连播开关状态
|
| 171 |
+
autoplayEnabled = localStorage.getItem('autoplayEnabled') !== 'false'; // 默认为true
|
| 172 |
+
document.getElementById('autoplayToggle').checked = autoplayEnabled;
|
| 173 |
+
|
| 174 |
+
// 获取广告过滤设置
|
| 175 |
+
adFilteringEnabled = localStorage.getItem(PLAYER_CONFIG.adFilteringStorage) !== 'false'; // 默认为true
|
| 176 |
+
|
| 177 |
+
// 监听自动连播开关变化
|
| 178 |
+
document.getElementById('autoplayToggle').addEventListener('change', function (e) {
|
| 179 |
+
autoplayEnabled = e.target.checked;
|
| 180 |
+
localStorage.setItem('autoplayEnabled', autoplayEnabled);
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
// 优先使用URL传递的集数信息,否则从localStorage获取
|
| 184 |
+
try {
|
| 185 |
+
if (episodesList) {
|
| 186 |
+
// 如果URL中有集数数据,优先使用它
|
| 187 |
+
currentEpisodes = JSON.parse(decodeURIComponent(episodesList));
|
| 188 |
+
|
| 189 |
+
} else {
|
| 190 |
+
// 否则从localStorage获取
|
| 191 |
+
currentEpisodes = JSON.parse(localStorage.getItem('currentEpisodes') || '[]');
|
| 192 |
+
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// 检查集数索引是否有效,如果无效则调整为0
|
| 196 |
+
if (index < 0 || (currentEpisodes.length > 0 && index >= currentEpisodes.length)) {
|
| 197 |
+
// 如果索引太大,则使用最大有效索引
|
| 198 |
+
if (index >= currentEpisodes.length && currentEpisodes.length > 0) {
|
| 199 |
+
index = currentEpisodes.length - 1;
|
| 200 |
+
} else {
|
| 201 |
+
index = 0;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// 更新URL以反映修正后的索引
|
| 205 |
+
const newUrl = new URL(window.location.href);
|
| 206 |
+
newUrl.searchParams.set('index', index);
|
| 207 |
+
window.history.replaceState({}, '', newUrl);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// 更新当前索引为验证过的值
|
| 211 |
+
currentEpisodeIndex = index;
|
| 212 |
+
|
| 213 |
+
episodesReversed = localStorage.getItem('episodesReversed') === 'true';
|
| 214 |
+
} catch (e) {
|
| 215 |
+
currentEpisodes = [];
|
| 216 |
+
currentEpisodeIndex = 0;
|
| 217 |
+
episodesReversed = false;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// 设置页面标题
|
| 221 |
+
document.title = currentVideoTitle + ' - LibreTV播放器';
|
| 222 |
+
document.getElementById('videoTitle').textContent = currentVideoTitle;
|
| 223 |
+
|
| 224 |
+
// 初始化播放器
|
| 225 |
+
if (videoUrl) {
|
| 226 |
+
initPlayer(videoUrl);
|
| 227 |
+
} else {
|
| 228 |
+
showError('无效的视频链接');
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// 渲染源信息
|
| 232 |
+
renderResourceInfoBar();
|
| 233 |
+
|
| 234 |
+
// 更新集数信息
|
| 235 |
+
updateEpisodeInfo();
|
| 236 |
+
|
| 237 |
+
// 渲染集数列表
|
| 238 |
+
renderEpisodes();
|
| 239 |
+
|
| 240 |
+
// 更新按钮状态
|
| 241 |
+
updateButtonStates();
|
| 242 |
+
|
| 243 |
+
// 更新排序按钮状态
|
| 244 |
+
updateOrderButton();
|
| 245 |
+
|
| 246 |
+
// 添加对进度条的监听,确保点击准确跳转
|
| 247 |
+
setTimeout(() => {
|
| 248 |
+
setupProgressBarPreciseClicks();
|
| 249 |
+
}, 1000);
|
| 250 |
+
|
| 251 |
+
// 添加键盘快捷键事件监听
|
| 252 |
+
document.addEventListener('keydown', handleKeyboardShortcuts);
|
| 253 |
+
|
| 254 |
+
// 添加页面离开事件监听,保存播放位置
|
| 255 |
+
window.addEventListener('beforeunload', saveCurrentProgress);
|
| 256 |
+
|
| 257 |
+
// 新增:页面隐藏(切后台/切标签)时也保存
|
| 258 |
+
document.addEventListener('visibilitychange', function () {
|
| 259 |
+
if (document.visibilityState === 'hidden') {
|
| 260 |
+
saveCurrentProgress();
|
| 261 |
+
}
|
| 262 |
+
});
|
| 263 |
+
|
| 264 |
+
// 视频暂停时也保存
|
| 265 |
+
const waitForVideo = setInterval(() => {
|
| 266 |
+
if (art && art.video) {
|
| 267 |
+
art.video.addEventListener('pause', saveCurrentProgress);
|
| 268 |
+
|
| 269 |
+
// 新增:播放进度变化时节流保存
|
| 270 |
+
let lastSave = 0;
|
| 271 |
+
art.video.addEventListener('timeupdate', function() {
|
| 272 |
+
const now = Date.now();
|
| 273 |
+
if (now - lastSave > 5000) { // 每5秒最多保存一次
|
| 274 |
+
saveCurrentProgress();
|
| 275 |
+
lastSave = now;
|
| 276 |
+
}
|
| 277 |
+
});
|
| 278 |
+
|
| 279 |
+
clearInterval(waitForVideo);
|
| 280 |
+
}
|
| 281 |
+
}, 200);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// 处理键盘快捷键
|
| 285 |
+
function handleKeyboardShortcuts(e) {
|
| 286 |
+
// 忽略输入框中的按键事件
|
| 287 |
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
| 288 |
+
|
| 289 |
+
// Alt + 左箭头 = 上一集
|
| 290 |
+
if (e.altKey && e.key === 'ArrowLeft') {
|
| 291 |
+
if (currentEpisodeIndex > 0) {
|
| 292 |
+
playPreviousEpisode();
|
| 293 |
+
showShortcutHint('上一集', 'left');
|
| 294 |
+
e.preventDefault();
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// Alt + 右箭头 = 下一集
|
| 299 |
+
if (e.altKey && e.key === 'ArrowRight') {
|
| 300 |
+
if (currentEpisodeIndex < currentEpisodes.length - 1) {
|
| 301 |
+
playNextEpisode();
|
| 302 |
+
showShortcutHint('下一集', 'right');
|
| 303 |
+
e.preventDefault();
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// 左箭头 = 快退
|
| 308 |
+
if (!e.altKey && e.key === 'ArrowLeft') {
|
| 309 |
+
if (art && art.currentTime > 5) {
|
| 310 |
+
art.currentTime -= 5;
|
| 311 |
+
showShortcutHint('快退', 'left');
|
| 312 |
+
e.preventDefault();
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// 右箭头 = 快进
|
| 317 |
+
if (!e.altKey && e.key === 'ArrowRight') {
|
| 318 |
+
if (art && art.currentTime < art.duration - 5) {
|
| 319 |
+
art.currentTime += 5;
|
| 320 |
+
showShortcutHint('快进', 'right');
|
| 321 |
+
e.preventDefault();
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// 上箭头 = 音量+
|
| 326 |
+
if (e.key === 'ArrowUp') {
|
| 327 |
+
if (art && art.volume < 1) {
|
| 328 |
+
art.volume += 0.1;
|
| 329 |
+
showShortcutHint('音量+', 'up');
|
| 330 |
+
e.preventDefault();
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
// 下箭头 = 音量-
|
| 335 |
+
if (e.key === 'ArrowDown') {
|
| 336 |
+
if (art && art.volume > 0) {
|
| 337 |
+
art.volume -= 0.1;
|
| 338 |
+
showShortcutHint('音量-', 'down');
|
| 339 |
+
e.preventDefault();
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// 空格 = 播放/暂停
|
| 344 |
+
if (e.key === ' ') {
|
| 345 |
+
if (art) {
|
| 346 |
+
art.toggle();
|
| 347 |
+
showShortcutHint('播放/暂停', 'play');
|
| 348 |
+
e.preventDefault();
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
// f 键 = 切换全屏
|
| 353 |
+
if (e.key === 'f' || e.key === 'F') {
|
| 354 |
+
if (art) {
|
| 355 |
+
art.fullscreen = !art.fullscreen;
|
| 356 |
+
showShortcutHint('切换全屏', 'fullscreen');
|
| 357 |
+
e.preventDefault();
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// 显示快捷键提示
|
| 363 |
+
function showShortcutHint(text, direction) {
|
| 364 |
+
const hintElement = document.getElementById('shortcutHint');
|
| 365 |
+
const textElement = document.getElementById('shortcutText');
|
| 366 |
+
const iconElement = document.getElementById('shortcutIcon');
|
| 367 |
+
|
| 368 |
+
// 清除之前的超时
|
| 369 |
+
if (shortcutHintTimeout) {
|
| 370 |
+
clearTimeout(shortcutHintTimeout);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
// 设置文本和图标方向
|
| 374 |
+
textElement.textContent = text;
|
| 375 |
+
|
| 376 |
+
if (direction === 'left') {
|
| 377 |
+
iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>';
|
| 378 |
+
} else if (direction === 'right') {
|
| 379 |
+
iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>';
|
| 380 |
+
} else if (direction === 'up') {
|
| 381 |
+
iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>';
|
| 382 |
+
} else if (direction === 'down') {
|
| 383 |
+
iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>';
|
| 384 |
+
} else if (direction === 'fullscreen') {
|
| 385 |
+
iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5"></path>';
|
| 386 |
+
} else if (direction === 'play') {
|
| 387 |
+
iconElement.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3l14 9-14 9V3z"></path>';
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
// 显示提示
|
| 391 |
+
hintElement.classList.add('show');
|
| 392 |
+
|
| 393 |
+
// 两秒后隐藏
|
| 394 |
+
shortcutHintTimeout = setTimeout(() => {
|
| 395 |
+
hintElement.classList.remove('show');
|
| 396 |
+
}, 2000);
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
// 初始化播放器
|
| 400 |
+
function initPlayer(videoUrl) {
|
| 401 |
+
if (!videoUrl) {
|
| 402 |
+
return
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
// 销毁旧实例
|
| 406 |
+
if (art) {
|
| 407 |
+
art.destroy();
|
| 408 |
+
art = null;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
// 配置HLS.js选项
|
| 412 |
+
const hlsConfig = {
|
| 413 |
+
debug: false,
|
| 414 |
+
loader: adFilteringEnabled ? CustomHlsJsLoader : Hls.DefaultConfig.loader,
|
| 415 |
+
enableWorker: true,
|
| 416 |
+
lowLatencyMode: false,
|
| 417 |
+
backBufferLength: 90,
|
| 418 |
+
maxBufferLength: 30,
|
| 419 |
+
maxMaxBufferLength: 60,
|
| 420 |
+
maxBufferSize: 30 * 1000 * 1000,
|
| 421 |
+
maxBufferHole: 0.5,
|
| 422 |
+
fragLoadingMaxRetry: 6,
|
| 423 |
+
fragLoadingMaxRetryTimeout: 64000,
|
| 424 |
+
fragLoadingRetryDelay: 1000,
|
| 425 |
+
manifestLoadingMaxRetry: 3,
|
| 426 |
+
manifestLoadingRetryDelay: 1000,
|
| 427 |
+
levelLoadingMaxRetry: 4,
|
| 428 |
+
levelLoadingRetryDelay: 1000,
|
| 429 |
+
startLevel: -1,
|
| 430 |
+
abrEwmaDefaultEstimate: 500000,
|
| 431 |
+
abrBandWidthFactor: 0.95,
|
| 432 |
+
abrBandWidthUpFactor: 0.7,
|
| 433 |
+
abrMaxWithRealBitrate: true,
|
| 434 |
+
stretchShortVideoTrack: true,
|
| 435 |
+
appendErrorMaxRetry: 5, // 增加尝试次数
|
| 436 |
+
liveSyncDurationCount: 3,
|
| 437 |
+
liveDurationInfinity: false
|
| 438 |
+
};
|
| 439 |
+
|
| 440 |
+
// Create new ArtPlayer instance
|
| 441 |
+
art = new Artplayer({
|
| 442 |
+
container: '#player',
|
| 443 |
+
url: videoUrl,
|
| 444 |
+
type: 'm3u8',
|
| 445 |
+
title: videoTitle,
|
| 446 |
+
volume: 0.8,
|
| 447 |
+
isLive: false,
|
| 448 |
+
muted: false,
|
| 449 |
+
autoplay: true,
|
| 450 |
+
pip: true,
|
| 451 |
+
autoSize: false,
|
| 452 |
+
autoMini: true,
|
| 453 |
+
screenshot: true,
|
| 454 |
+
setting: true,
|
| 455 |
+
loop: false,
|
| 456 |
+
flip: false,
|
| 457 |
+
playbackRate: true,
|
| 458 |
+
aspectRatio: false,
|
| 459 |
+
fullscreen: true,
|
| 460 |
+
fullscreenWeb: true,
|
| 461 |
+
subtitleOffset: false,
|
| 462 |
+
miniProgressBar: true,
|
| 463 |
+
mutex: true,
|
| 464 |
+
backdrop: true,
|
| 465 |
+
playsInline: true,
|
| 466 |
+
autoPlayback: false,
|
| 467 |
+
airplay: true,
|
| 468 |
+
hotkey: false,
|
| 469 |
+
theme: '#23ade5',
|
| 470 |
+
lang: navigator.language.toLowerCase(),
|
| 471 |
+
moreVideoAttr: {
|
| 472 |
+
crossOrigin: 'anonymous',
|
| 473 |
+
},
|
| 474 |
+
customType: {
|
| 475 |
+
m3u8: function (video, url) {
|
| 476 |
+
// 清理之前的HLS实例
|
| 477 |
+
if (currentHls && currentHls.destroy) {
|
| 478 |
+
try {
|
| 479 |
+
currentHls.destroy();
|
| 480 |
+
} catch (e) {
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
// 创建新的HLS实例
|
| 485 |
+
const hls = new Hls(hlsConfig);
|
| 486 |
+
currentHls = hls;
|
| 487 |
+
|
| 488 |
+
// 跟踪是否已经显示错误
|
| 489 |
+
let errorDisplayed = false;
|
| 490 |
+
// 跟踪是否有错误发生
|
| 491 |
+
let errorCount = 0;
|
| 492 |
+
// 跟踪视频是否开始播放
|
| 493 |
+
let playbackStarted = false;
|
| 494 |
+
// 跟踪视频是否出现bufferAppendError
|
| 495 |
+
let bufferAppendErrorCount = 0;
|
| 496 |
+
|
| 497 |
+
// 监听视频播放事件
|
| 498 |
+
video.addEventListener('playing', function () {
|
| 499 |
+
playbackStarted = true;
|
| 500 |
+
document.getElementById('player-loading').style.display = 'none';
|
| 501 |
+
document.getElementById('error').style.display = 'none';
|
| 502 |
+
});
|
| 503 |
+
|
| 504 |
+
// 监听视频进度事件
|
| 505 |
+
video.addEventListener('timeupdate', function () {
|
| 506 |
+
if (video.currentTime > 1) {
|
| 507 |
+
// 视频进度超过1秒,隐藏错误(如果存在)
|
| 508 |
+
document.getElementById('error').style.display = 'none';
|
| 509 |
+
}
|
| 510 |
+
});
|
| 511 |
+
|
| 512 |
+
hls.loadSource(url);
|
| 513 |
+
hls.attachMedia(video);
|
| 514 |
+
|
| 515 |
+
// enable airplay, from https://github.com/video-dev/hls.js/issues/5989
|
| 516 |
+
// 检查是否已存在source元素,如果存在则更新,不存在则创建
|
| 517 |
+
let sourceElement = video.querySelector('source');
|
| 518 |
+
if (sourceElement) {
|
| 519 |
+
// 更新现有source元素的URL
|
| 520 |
+
sourceElement.src = videoUrl;
|
| 521 |
+
} else {
|
| 522 |
+
// 创建新的source元素
|
| 523 |
+
sourceElement = document.createElement('source');
|
| 524 |
+
sourceElement.src = videoUrl;
|
| 525 |
+
video.appendChild(sourceElement);
|
| 526 |
+
}
|
| 527 |
+
video.disableRemotePlayback = false;
|
| 528 |
+
|
| 529 |
+
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
| 530 |
+
video.play().catch(e => {
|
| 531 |
+
});
|
| 532 |
+
});
|
| 533 |
+
|
| 534 |
+
hls.on(Hls.Events.ERROR, function (event, data) {
|
| 535 |
+
// 增加错误计数
|
| 536 |
+
errorCount++;
|
| 537 |
+
|
| 538 |
+
// 处理bufferAppendError
|
| 539 |
+
if (data.details === 'bufferAppendError') {
|
| 540 |
+
bufferAppendErrorCount++;
|
| 541 |
+
// 如果视频已经开始播放,则忽略这个错误
|
| 542 |
+
if (playbackStarted) {
|
| 543 |
+
return;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
// 如果出现多次bufferAppendError但视频未播放,尝试恢复
|
| 547 |
+
if (bufferAppendErrorCount >= 3) {
|
| 548 |
+
hls.recoverMediaError();
|
| 549 |
+
}
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
// 如果是致命错误,且视频未播放
|
| 553 |
+
if (data.fatal && !playbackStarted) {
|
| 554 |
+
// 尝试恢复错误
|
| 555 |
+
switch (data.type) {
|
| 556 |
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
| 557 |
+
hls.startLoad();
|
| 558 |
+
break;
|
| 559 |
+
case Hls.ErrorTypes.MEDIA_ERROR:
|
| 560 |
+
hls.recoverMediaError();
|
| 561 |
+
break;
|
| 562 |
+
default:
|
| 563 |
+
// 仅在多次恢复尝试后显示错误
|
| 564 |
+
if (errorCount > 3 && !errorDisplayed) {
|
| 565 |
+
errorDisplayed = true;
|
| 566 |
+
showError('视频加载失败,可能是格式不兼容或源不可用');
|
| 567 |
+
}
|
| 568 |
+
break;
|
| 569 |
+
}
|
| 570 |
+
}
|
| 571 |
+
});
|
| 572 |
+
|
| 573 |
+
// 监听分段加载事件
|
| 574 |
+
hls.on(Hls.Events.FRAG_LOADED, function () {
|
| 575 |
+
document.getElementById('player-loading').style.display = 'none';
|
| 576 |
+
});
|
| 577 |
+
|
| 578 |
+
// 监听级别加载事件
|
| 579 |
+
hls.on(Hls.Events.LEVEL_LOADED, function () {
|
| 580 |
+
document.getElementById('player-loading').style.display = 'none';
|
| 581 |
+
});
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
});
|
| 585 |
+
|
| 586 |
+
// artplayer 没有 'fullscreenWeb:enter', 'fullscreenWeb:exit' 等事件
|
| 587 |
+
// 所以原控制栏隐藏代码并没有起作用
|
| 588 |
+
// 实际起作用的是 artplayer 默认行为,它支持自动隐藏工具栏
|
| 589 |
+
// 但有一个 bug: 在副屏全屏时,鼠标移出副屏后不会自动隐藏工具栏
|
| 590 |
+
// 下面进一并重构和修复:
|
| 591 |
+
let hideTimer;
|
| 592 |
+
|
| 593 |
+
// 隐藏控制栏
|
| 594 |
+
function hideControls() {
|
| 595 |
+
if (art && art.controls) {
|
| 596 |
+
art.controls.show = false;
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
// 重置计时器,计时器超时时间与 artplayer 保持一致
|
| 601 |
+
function resetHideTimer() {
|
| 602 |
+
clearTimeout(hideTimer);
|
| 603 |
+
hideTimer = setTimeout(() => {
|
| 604 |
+
hideControls();
|
| 605 |
+
}, Artplayer.CONTROL_HIDE_TIME);
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
// 处理鼠标离开浏览器窗口
|
| 609 |
+
function handleMouseOut(e) {
|
| 610 |
+
if (e && !e.relatedTarget) {
|
| 611 |
+
resetHideTimer();
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
// 全屏状态切换时注册/移除 mouseout 事件,监听鼠标移出屏幕事件
|
| 616 |
+
// 从而对播放器状态栏进行隐藏倒计时
|
| 617 |
+
function handleFullScreen(isFullScreen, isWeb) {
|
| 618 |
+
if (isFullScreen) {
|
| 619 |
+
document.addEventListener('mouseout', handleMouseOut);
|
| 620 |
+
} else {
|
| 621 |
+
document.removeEventListener('mouseout', handleMouseOut);
|
| 622 |
+
// 退出全屏时清理计时器
|
| 623 |
+
clearTimeout(hideTimer);
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
if (!isWeb) {
|
| 627 |
+
if (window.screen.orientation && window.screen.orientation.lock) {
|
| 628 |
+
window.screen.orientation.lock('landscape')
|
| 629 |
+
.then(() => {
|
| 630 |
+
})
|
| 631 |
+
.catch((error) => {
|
| 632 |
+
});
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
// 播放器加载完成后初始隐藏工具栏
|
| 638 |
+
art.on('ready', () => {
|
| 639 |
+
hideControls();
|
| 640 |
+
});
|
| 641 |
+
|
| 642 |
+
// 全屏 Web 模式处理
|
| 643 |
+
art.on('fullscreenWeb', function (isFullScreen) {
|
| 644 |
+
handleFullScreen(isFullScreen, true);
|
| 645 |
+
});
|
| 646 |
+
|
| 647 |
+
// 全屏模式处理
|
| 648 |
+
art.on('fullscreen', function (isFullScreen) {
|
| 649 |
+
handleFullScreen(isFullScreen, false);
|
| 650 |
+
});
|
| 651 |
+
|
| 652 |
+
art.on('video:loadedmetadata', function() {
|
| 653 |
+
document.getElementById('player-loading').style.display = 'none';
|
| 654 |
+
videoHasEnded = false; // 视频加载时重置结束标志
|
| 655 |
+
// 优先使用URL传递的position参数
|
| 656 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 657 |
+
const savedPosition = parseInt(urlParams.get('position') || '0');
|
| 658 |
+
|
| 659 |
+
if (savedPosition > 10 && savedPosition < art.duration - 2) {
|
| 660 |
+
// 如果URL中有有效的播放位置参数,直接使用它
|
| 661 |
+
art.currentTime = savedPosition;
|
| 662 |
+
showPositionRestoreHint(savedPosition);
|
| 663 |
+
} else {
|
| 664 |
+
// 否则尝试从本地存储恢复播放进度
|
| 665 |
+
try {
|
| 666 |
+
const progressKey = 'videoProgress_' + getVideoId();
|
| 667 |
+
const progressStr = localStorage.getItem(progressKey);
|
| 668 |
+
if (progressStr && art.duration > 0) {
|
| 669 |
+
const progress = JSON.parse(progressStr);
|
| 670 |
+
if (
|
| 671 |
+
progress &&
|
| 672 |
+
typeof progress.position === 'number' &&
|
| 673 |
+
progress.position > 10 &&
|
| 674 |
+
progress.position < art.duration - 2
|
| 675 |
+
) {
|
| 676 |
+
art.currentTime = progress.position;
|
| 677 |
+
showPositionRestoreHint(progress.position);
|
| 678 |
+
}
|
| 679 |
+
}
|
| 680 |
+
} catch (e) {
|
| 681 |
+
}
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
// 设置进度条点击监听
|
| 685 |
+
setupProgressBarPreciseClicks();
|
| 686 |
+
|
| 687 |
+
// 视频加载成功后,在稍微延迟后将其添加到观看历史
|
| 688 |
+
setTimeout(saveToHistory, 3000);
|
| 689 |
+
|
| 690 |
+
// 启动定期保存播放进度
|
| 691 |
+
startProgressSaveInterval();
|
| 692 |
+
})
|
| 693 |
+
|
| 694 |
+
// 错误处理
|
| 695 |
+
art.on('video:error', function (error) {
|
| 696 |
+
// 如果正在切换视频,忽略错误
|
| 697 |
+
if (window.isSwitchingVideo) {
|
| 698 |
+
return;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
// 隐藏所有加载指示器
|
| 702 |
+
const loadingElements = document.querySelectorAll('#player-loading, .player-loading-container');
|
| 703 |
+
loadingElements.forEach(el => {
|
| 704 |
+
if (el) el.style.display = 'none';
|
| 705 |
+
});
|
| 706 |
+
|
| 707 |
+
showError('视频播放失败: ' + (error.message || '未知错误'));
|
| 708 |
+
});
|
| 709 |
+
|
| 710 |
+
// 添加移动端长按三倍速播放功能
|
| 711 |
+
setupLongPressSpeedControl();
|
| 712 |
+
|
| 713 |
+
// 视频播放结束事件
|
| 714 |
+
art.on('video:ended', function () {
|
| 715 |
+
videoHasEnded = true;
|
| 716 |
+
|
| 717 |
+
clearVideoProgress();
|
| 718 |
+
|
| 719 |
+
// 如果自动播放下一集开启,且确实有下一集
|
| 720 |
+
if (autoplayEnabled && currentEpisodeIndex < currentEpisodes.length - 1) {
|
| 721 |
+
// 稍长延迟以确保所有事件处理完成
|
| 722 |
+
setTimeout(() => {
|
| 723 |
+
// 确认不是因为用户拖拽导致的假结束事件
|
| 724 |
+
playNextEpisode();
|
| 725 |
+
videoHasEnded = false; // 重置标志
|
| 726 |
+
}, 1000);
|
| 727 |
+
} else {
|
| 728 |
+
art.fullscreen = false;
|
| 729 |
+
}
|
| 730 |
+
});
|
| 731 |
+
|
| 732 |
+
// 添加双击全屏支持
|
| 733 |
+
art.on('video:playing', () => {
|
| 734 |
+
// 绑定双击事件到视频容器
|
| 735 |
+
if (art.video) {
|
| 736 |
+
art.video.addEventListener('dblclick', () => {
|
| 737 |
+
art.fullscreen = !art.fullscreen;
|
| 738 |
+
art.play();
|
| 739 |
+
});
|
| 740 |
+
}
|
| 741 |
+
});
|
| 742 |
+
|
| 743 |
+
// 10秒后如果仍在加载,但不立即显示错误
|
| 744 |
+
setTimeout(function () {
|
| 745 |
+
// 如果视频已经播放开始,则不显示错误
|
| 746 |
+
if (art && art.video && art.video.currentTime > 0) {
|
| 747 |
+
return;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
const loadingElement = document.getElementById('player-loading');
|
| 751 |
+
if (loadingElement && loadingElement.style.display !== 'none') {
|
| 752 |
+
loadingElement.innerHTML = `
|
| 753 |
+
<div class="loading-spinner"></div>
|
| 754 |
+
<div>视频加载时间较长,请耐心等待...</div>
|
| 755 |
+
<div style="font-size: 12px; color: #aaa; margin-top: 10px;">如长时间无响应,请尝试其他视频源</div>
|
| 756 |
+
`;
|
| 757 |
+
}
|
| 758 |
+
}, 10000);
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
// 自定义M3U8 Loader用于过滤广告
|
| 762 |
+
class CustomHlsJsLoader extends Hls.DefaultConfig.loader {
|
| 763 |
+
constructor(config) {
|
| 764 |
+
super(config);
|
| 765 |
+
const load = this.load.bind(this);
|
| 766 |
+
this.load = function (context, config, callbacks) {
|
| 767 |
+
// 拦截manifest和level请求
|
| 768 |
+
if (context.type === 'manifest' || context.type === 'level') {
|
| 769 |
+
const onSuccess = callbacks.onSuccess;
|
| 770 |
+
callbacks.onSuccess = function (response, stats, context) {
|
| 771 |
+
// 如果是m3u8文件,处理内容以移除广告分段
|
| 772 |
+
if (response.data && typeof response.data === 'string') {
|
| 773 |
+
// 过滤掉广告段 - 实现更精确的广告过滤逻辑
|
| 774 |
+
response.data = filterAdsFromM3U8(response.data, true);
|
| 775 |
+
}
|
| 776 |
+
return onSuccess(response, stats, context);
|
| 777 |
+
};
|
| 778 |
+
}
|
| 779 |
+
// 执行原始load方法
|
| 780 |
+
load(context, config, callbacks);
|
| 781 |
+
};
|
| 782 |
+
}
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
// 过滤可疑的广告内容
|
| 786 |
+
function filterAdsFromM3U8(m3u8Content, strictMode = false) {
|
| 787 |
+
if (!m3u8Content) return '';
|
| 788 |
+
|
| 789 |
+
// 按行分割M3U8内容
|
| 790 |
+
const lines = m3u8Content.split('\n');
|
| 791 |
+
const filteredLines = [];
|
| 792 |
+
|
| 793 |
+
for (let i = 0; i < lines.length; i++) {
|
| 794 |
+
const line = lines[i];
|
| 795 |
+
|
| 796 |
+
// 只过滤#EXT-X-DISCONTINUITY标识
|
| 797 |
+
if (!line.includes('#EXT-X-DISCONTINUITY')) {
|
| 798 |
+
filteredLines.push(line);
|
| 799 |
+
}
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
return filteredLines.join('\n');
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
// 显示错误
|
| 807 |
+
function showError(message) {
|
| 808 |
+
// 在视频已经播放的情况下不显示错误
|
| 809 |
+
if (art && art.video && art.video.currentTime > 1) {
|
| 810 |
+
return;
|
| 811 |
+
}
|
| 812 |
+
const loadingEl = document.getElementById('player-loading');
|
| 813 |
+
if (loadingEl) loadingEl.style.display = 'none';
|
| 814 |
+
const errorEl = document.getElementById('error');
|
| 815 |
+
if (errorEl) errorEl.style.display = 'flex';
|
| 816 |
+
const errorMsgEl = document.getElementById('error-message');
|
| 817 |
+
if (errorMsgEl) errorMsgEl.textContent = message;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
// 更新集数信息
|
| 821 |
+
function updateEpisodeInfo() {
|
| 822 |
+
if (currentEpisodes.length > 0) {
|
| 823 |
+
document.getElementById('episodeInfo').textContent = `第 ${currentEpisodeIndex + 1}/${currentEpisodes.length} 集`;
|
| 824 |
+
} else {
|
| 825 |
+
document.getElementById('episodeInfo').textContent = '无集数信息';
|
| 826 |
+
}
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
// 更新按钮状态
|
| 830 |
+
function updateButtonStates() {
|
| 831 |
+
const prevButton = document.getElementById('prevButton');
|
| 832 |
+
const nextButton = document.getElementById('nextButton');
|
| 833 |
+
|
| 834 |
+
// 处理上一集按钮
|
| 835 |
+
if (currentEpisodeIndex > 0) {
|
| 836 |
+
prevButton.classList.remove('bg-gray-700', 'cursor-not-allowed');
|
| 837 |
+
prevButton.classList.add('bg-[#222]', 'hover:bg-[#333]');
|
| 838 |
+
prevButton.removeAttribute('disabled');
|
| 839 |
+
} else {
|
| 840 |
+
prevButton.classList.add('bg-gray-700', 'cursor-not-allowed');
|
| 841 |
+
prevButton.classList.remove('bg-[#222]', 'hover:bg-[#333]');
|
| 842 |
+
prevButton.setAttribute('disabled', '');
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
// 处理下一集按钮
|
| 846 |
+
if (currentEpisodeIndex < currentEpisodes.length - 1) {
|
| 847 |
+
nextButton.classList.remove('bg-gray-700', 'cursor-not-allowed');
|
| 848 |
+
nextButton.classList.add('bg-[#222]', 'hover:bg-[#333]');
|
| 849 |
+
nextButton.removeAttribute('disabled');
|
| 850 |
+
} else {
|
| 851 |
+
nextButton.classList.add('bg-gray-700', 'cursor-not-allowed');
|
| 852 |
+
nextButton.classList.remove('bg-[#222]', 'hover:bg-[#333]');
|
| 853 |
+
nextButton.setAttribute('disabled', '');
|
| 854 |
+
}
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
// 渲染集数按钮
|
| 858 |
+
function renderEpisodes() {
|
| 859 |
+
const episodesList = document.getElementById('episodesList');
|
| 860 |
+
if (!episodesList) return;
|
| 861 |
+
|
| 862 |
+
if (!currentEpisodes || currentEpisodes.length === 0) {
|
| 863 |
+
episodesList.innerHTML = '<div class="col-span-full text-center text-gray-400 py-8">没有可用的集数</div>';
|
| 864 |
+
return;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
const episodes = episodesReversed ? [...currentEpisodes].reverse() : currentEpisodes;
|
| 868 |
+
let html = '';
|
| 869 |
+
|
| 870 |
+
episodes.forEach((episode, index) => {
|
| 871 |
+
// 根据倒序状态计算真实的剧集索引
|
| 872 |
+
const realIndex = episodesReversed ? currentEpisodes.length - 1 - index : index;
|
| 873 |
+
const isActive = realIndex === currentEpisodeIndex;
|
| 874 |
+
|
| 875 |
+
html += `
|
| 876 |
+
<button id="episode-${realIndex}"
|
| 877 |
+
onclick="playEpisode(${realIndex})"
|
| 878 |
+
class="px-4 py-2 ${isActive ? 'episode-active' : '!bg-[#222] hover:!bg-[#333] hover:!shadow-none'} !border ${isActive ? '!border-blue-500' : '!border-[#333]'} rounded-lg transition-colors text-center episode-btn">
|
| 879 |
+
${realIndex + 1}
|
| 880 |
+
</button>
|
| 881 |
+
`;
|
| 882 |
+
});
|
| 883 |
+
|
| 884 |
+
episodesList.innerHTML = html;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
// 播放指定集数
|
| 888 |
+
function playEpisode(index) {
|
| 889 |
+
// 确保index在有效范围内
|
| 890 |
+
if (index < 0 || index >= currentEpisodes.length) {
|
| 891 |
+
return;
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
// 保存当前播放进度(如果正在播放)
|
| 895 |
+
if (art && art.video && !art.video.paused && !videoHasEnded) {
|
| 896 |
+
saveCurrentProgress();
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
// 清除进度保存计时器
|
| 900 |
+
if (progressSaveInterval) {
|
| 901 |
+
clearInterval(progressSaveInterval);
|
| 902 |
+
progressSaveInterval = null;
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
// 首先隐藏之前可能显示的错误
|
| 906 |
+
document.getElementById('error').style.display = 'none';
|
| 907 |
+
// 显示加载指示器
|
| 908 |
+
document.getElementById('player-loading').style.display = 'flex';
|
| 909 |
+
document.getElementById('player-loading').innerHTML = `
|
| 910 |
+
<div class="loading-spinner"></div>
|
| 911 |
+
<div>正在加载视频...</div>
|
| 912 |
+
`;
|
| 913 |
+
|
| 914 |
+
// 获取 sourceCode
|
| 915 |
+
const urlParams2 = new URLSearchParams(window.location.search);
|
| 916 |
+
const sourceCode = urlParams2.get('source_code');
|
| 917 |
+
|
| 918 |
+
// 准备切换剧集的URL
|
| 919 |
+
const url = currentEpisodes[index];
|
| 920 |
+
|
| 921 |
+
// 更新当前剧集索引
|
| 922 |
+
currentEpisodeIndex = index;
|
| 923 |
+
currentVideoUrl = url;
|
| 924 |
+
videoHasEnded = false; // 重置视频结束标志
|
| 925 |
+
|
| 926 |
+
clearVideoProgress();
|
| 927 |
+
|
| 928 |
+
// 更新URL参数(不刷新页面)
|
| 929 |
+
const currentUrl = new URL(window.location.href);
|
| 930 |
+
currentUrl.searchParams.set('index', index);
|
| 931 |
+
currentUrl.searchParams.set('url', url);
|
| 932 |
+
currentUrl.searchParams.delete('position');
|
| 933 |
+
window.history.replaceState({}, '', currentUrl.toString());
|
| 934 |
+
|
| 935 |
+
if (isWebkit) {
|
| 936 |
+
initPlayer(url);
|
| 937 |
+
} else {
|
| 938 |
+
art.switch = url;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
// 更新UI
|
| 942 |
+
updateEpisodeInfo();
|
| 943 |
+
updateButtonStates();
|
| 944 |
+
renderEpisodes();
|
| 945 |
+
|
| 946 |
+
// 重置用户点击位置记录
|
| 947 |
+
userClickedPosition = null;
|
| 948 |
+
|
| 949 |
+
// 三秒后保存到历史记录
|
| 950 |
+
setTimeout(() => saveToHistory(), 3000);
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
// 播放上一集
|
| 954 |
+
function playPreviousEpisode() {
|
| 955 |
+
if (currentEpisodeIndex > 0) {
|
| 956 |
+
playEpisode(currentEpisodeIndex - 1);
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
// 播放下一集
|
| 961 |
+
function playNextEpisode() {
|
| 962 |
+
if (currentEpisodeIndex < currentEpisodes.length - 1) {
|
| 963 |
+
playEpisode(currentEpisodeIndex + 1);
|
| 964 |
+
}
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
// 复制播放链接
|
| 968 |
+
function copyLinks() {
|
| 969 |
+
// 尝试从URL中获取参数
|
| 970 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 971 |
+
const linkUrl = urlParams.get('url') || '';
|
| 972 |
+
if (linkUrl !== '') {
|
| 973 |
+
navigator.clipboard.writeText(linkUrl).then(() => {
|
| 974 |
+
showToast('播放链接已复制', 'success');
|
| 975 |
+
}).catch(err => {
|
| 976 |
+
showToast('复制失败,请检查浏览器权限', 'error');
|
| 977 |
+
});
|
| 978 |
+
}
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
// 切换集数排序
|
| 982 |
+
function toggleEpisodeOrder() {
|
| 983 |
+
episodesReversed = !episodesReversed;
|
| 984 |
+
|
| 985 |
+
// 保存到localStorage
|
| 986 |
+
localStorage.setItem('episodesReversed', episodesReversed);
|
| 987 |
+
|
| 988 |
+
// 重新渲染集数列表
|
| 989 |
+
renderEpisodes();
|
| 990 |
+
|
| 991 |
+
// 更新排序按钮
|
| 992 |
+
updateOrderButton();
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
// 更新排序按钮状态
|
| 996 |
+
function updateOrderButton() {
|
| 997 |
+
const orderText = document.getElementById('orderText');
|
| 998 |
+
const orderIcon = document.getElementById('orderIcon');
|
| 999 |
+
|
| 1000 |
+
if (orderText && orderIcon) {
|
| 1001 |
+
orderText.textContent = episodesReversed ? '正序排列' : '倒序排列';
|
| 1002 |
+
orderIcon.style.transform = episodesReversed ? 'rotate(180deg)' : '';
|
| 1003 |
+
}
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
// 设置进度条准确点击处理
|
| 1007 |
+
function setupProgressBarPreciseClicks() {
|
| 1008 |
+
// 查找DPlayer的进度条元素
|
| 1009 |
+
const progressBar = document.querySelector('.dplayer-bar-wrap');
|
| 1010 |
+
if (!progressBar || !art || !art.video) return;
|
| 1011 |
+
|
| 1012 |
+
// 移除可能存在的旧事件监听器
|
| 1013 |
+
progressBar.removeEventListener('mousedown', handleProgressBarClick);
|
| 1014 |
+
|
| 1015 |
+
// 添加新的事件监听器
|
| 1016 |
+
progressBar.addEventListener('mousedown', handleProgressBarClick);
|
| 1017 |
+
|
| 1018 |
+
// 在移动端也添加触摸事件支持
|
| 1019 |
+
progressBar.removeEventListener('touchstart', handleProgressBarTouch);
|
| 1020 |
+
progressBar.addEventListener('touchstart', handleProgressBarTouch);
|
| 1021 |
+
|
| 1022 |
+
// 处理进度条点击
|
| 1023 |
+
function handleProgressBarClick(e) {
|
| 1024 |
+
if (!art || !art.video) return;
|
| 1025 |
+
|
| 1026 |
+
// 计算点击位置相对于进度条的比例
|
| 1027 |
+
const rect = e.currentTarget.getBoundingClientRect();
|
| 1028 |
+
const percentage = (e.clientX - rect.left) / rect.width;
|
| 1029 |
+
|
| 1030 |
+
// 计算点击位置对应的视频时间
|
| 1031 |
+
const duration = art.video.duration;
|
| 1032 |
+
let clickTime = percentage * duration;
|
| 1033 |
+
|
| 1034 |
+
// 处理视频接近结尾的情况
|
| 1035 |
+
if (duration - clickTime < 1) {
|
| 1036 |
+
// 如果点击位置非常接近结尾,稍微往前移一点
|
| 1037 |
+
clickTime = Math.min(clickTime, duration - 1.5);
|
| 1038 |
+
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
// 记录用户点击的位置
|
| 1042 |
+
userClickedPosition = clickTime;
|
| 1043 |
+
|
| 1044 |
+
// 阻止默认事件传播,避免DPlayer内部逻辑将视频跳至末尾
|
| 1045 |
+
e.stopPropagation();
|
| 1046 |
+
|
| 1047 |
+
// 直接设置视频时间
|
| 1048 |
+
art.seek(clickTime);
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
// 处理移动端触摸事件
|
| 1052 |
+
function handleProgressBarTouch(e) {
|
| 1053 |
+
if (!art || !art.video || !e.touches[0]) return;
|
| 1054 |
+
|
| 1055 |
+
const touch = e.touches[0];
|
| 1056 |
+
const rect = e.currentTarget.getBoundingClientRect();
|
| 1057 |
+
const percentage = (touch.clientX - rect.left) / rect.width;
|
| 1058 |
+
|
| 1059 |
+
const duration = art.video.duration;
|
| 1060 |
+
let clickTime = percentage * duration;
|
| 1061 |
+
|
| 1062 |
+
// 处理视频接近结尾的情况
|
| 1063 |
+
if (duration - clickTime < 1) {
|
| 1064 |
+
clickTime = Math.min(clickTime, duration - 1.5);
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
// 记录用户点击的位置
|
| 1068 |
+
userClickedPosition = clickTime;
|
| 1069 |
+
|
| 1070 |
+
e.stopPropagation();
|
| 1071 |
+
art.seek(clickTime);
|
| 1072 |
+
}
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
// 在播放器初始化后添加视频到历史记录
|
| 1076 |
+
function saveToHistory() {
|
| 1077 |
+
// 确保 currentEpisodes 非空且有当前视频URL
|
| 1078 |
+
if (!currentEpisodes || currentEpisodes.length === 0 || !currentVideoUrl) {
|
| 1079 |
+
return;
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
// 尝试从URL中获取参数
|
| 1083 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 1084 |
+
const sourceName = urlParams.get('source') || '';
|
| 1085 |
+
const sourceCode = urlParams.get('source') || '';
|
| 1086 |
+
const id_from_params = urlParams.get('id'); // Get video ID from player URL (passed as 'id')
|
| 1087 |
+
|
| 1088 |
+
// 获取当前播放进度
|
| 1089 |
+
let currentPosition = 0;
|
| 1090 |
+
let videoDuration = 0;
|
| 1091 |
+
|
| 1092 |
+
if (art && art.video) {
|
| 1093 |
+
currentPosition = art.video.currentTime;
|
| 1094 |
+
videoDuration = art.video.duration;
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
// Define a show identifier: Prioritize sourceName_id, fallback to first episode URL or current video URL
|
| 1098 |
+
let show_identifier_for_video_info;
|
| 1099 |
+
if (sourceName && id_from_params) {
|
| 1100 |
+
show_identifier_for_video_info = `${sourceName}_${id_from_params}`;
|
| 1101 |
+
} else {
|
| 1102 |
+
show_identifier_for_video_info = (currentEpisodes && currentEpisodes.length > 0) ? currentEpisodes[0] : currentVideoUrl;
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
// 构建要保存的视频信息对象
|
| 1106 |
+
const videoInfo = {
|
| 1107 |
+
title: currentVideoTitle,
|
| 1108 |
+
directVideoUrl: currentVideoUrl, // Current episode's direct URL
|
| 1109 |
+
url: `player.html?url=${encodeURIComponent(currentVideoUrl)}&title=${encodeURIComponent(currentVideoTitle)}&source=${encodeURIComponent(sourceName)}&source_code=${encodeURIComponent(sourceCode)}&id=${encodeURIComponent(id_from_params || '')}&index=${currentEpisodeIndex}&position=${Math.floor(currentPosition || 0)}`,
|
| 1110 |
+
episodeIndex: currentEpisodeIndex,
|
| 1111 |
+
sourceName: sourceName,
|
| 1112 |
+
vod_id: id_from_params || '', // Store the ID from params as vod_id in history item
|
| 1113 |
+
sourceCode: sourceCode,
|
| 1114 |
+
showIdentifier: show_identifier_for_video_info, // Identifier for the show/series
|
| 1115 |
+
timestamp: Date.now(),
|
| 1116 |
+
playbackPosition: currentPosition,
|
| 1117 |
+
duration: videoDuration,
|
| 1118 |
+
episodes: currentEpisodes && currentEpisodes.length > 0 ? [...currentEpisodes] : []
|
| 1119 |
+
};
|
| 1120 |
+
|
| 1121 |
+
try {
|
| 1122 |
+
const history = JSON.parse(localStorage.getItem('viewingHistory') || '[]');
|
| 1123 |
+
|
| 1124 |
+
// 检查是否已经存在相同的系列记录 (基于标题、来源和 showIdentifier)
|
| 1125 |
+
const existingIndex = history.findIndex(item =>
|
| 1126 |
+
item.title === videoInfo.title &&
|
| 1127 |
+
item.sourceName === videoInfo.sourceName &&
|
| 1128 |
+
item.showIdentifier === videoInfo.showIdentifier
|
| 1129 |
+
);
|
| 1130 |
+
|
| 1131 |
+
if (existingIndex !== -1) {
|
| 1132 |
+
// 存在则更新现有记录的当前集数、时间戳、播放进度和URL等
|
| 1133 |
+
const existingItem = history[existingIndex];
|
| 1134 |
+
existingItem.episodeIndex = videoInfo.episodeIndex;
|
| 1135 |
+
existingItem.timestamp = videoInfo.timestamp;
|
| 1136 |
+
existingItem.sourceName = videoInfo.sourceName; // Should be consistent, but update just in case
|
| 1137 |
+
existingItem.sourceCode = videoInfo.sourceCode;
|
| 1138 |
+
existingItem.vod_id = videoInfo.vod_id;
|
| 1139 |
+
|
| 1140 |
+
// Update URLs to reflect the current episode being watched
|
| 1141 |
+
existingItem.directVideoUrl = videoInfo.directVideoUrl; // Current episode's direct URL
|
| 1142 |
+
existingItem.url = videoInfo.url; // Player link for the current episode
|
| 1143 |
+
|
| 1144 |
+
// 更新播放进度信息
|
| 1145 |
+
existingItem.playbackPosition = videoInfo.playbackPosition > 10 ? videoInfo.playbackPosition : (existingItem.playbackPosition || 0);
|
| 1146 |
+
existingItem.duration = videoInfo.duration || existingItem.duration;
|
| 1147 |
+
|
| 1148 |
+
// 更新集数列表(如果新的集数列表与存储的不同,例如集数增加了)
|
| 1149 |
+
if (videoInfo.episodes && videoInfo.episodes.length > 0) {
|
| 1150 |
+
if (!existingItem.episodes ||
|
| 1151 |
+
!Array.isArray(existingItem.episodes) ||
|
| 1152 |
+
existingItem.episodes.length !== videoInfo.episodes.length ||
|
| 1153 |
+
!videoInfo.episodes.every((ep, i) => ep === existingItem.episodes[i])) { // Basic check for content change
|
| 1154 |
+
existingItem.episodes = [...videoInfo.episodes]; // Deep copy
|
| 1155 |
+
}
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
// 移到最前面
|
| 1159 |
+
const updatedItem = history.splice(existingIndex, 1)[0];
|
| 1160 |
+
history.unshift(updatedItem);
|
| 1161 |
+
} else {
|
| 1162 |
+
// 添加新记录到最前面
|
| 1163 |
+
history.unshift(videoInfo);
|
| 1164 |
+
}
|
| 1165 |
+
|
| 1166 |
+
// 限制历史记录数量为50条
|
| 1167 |
+
if (history.length > 50) history.splice(50);
|
| 1168 |
+
|
| 1169 |
+
localStorage.setItem('viewingHistory', JSON.stringify(history));
|
| 1170 |
+
} catch (e) {
|
| 1171 |
+
}
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
// 显示恢复位置提示
|
| 1175 |
+
function showPositionRestoreHint(position) {
|
| 1176 |
+
if (!position || position < 10) return;
|
| 1177 |
+
|
| 1178 |
+
// 创建提示元素
|
| 1179 |
+
const hint = document.createElement('div');
|
| 1180 |
+
hint.className = 'position-restore-hint';
|
| 1181 |
+
hint.innerHTML = `
|
| 1182 |
+
<div class="hint-content">
|
| 1183 |
+
已从 ${formatTime(position)} 继续播放
|
| 1184 |
+
</div>
|
| 1185 |
+
`;
|
| 1186 |
+
|
| 1187 |
+
// 添加到播放器容器
|
| 1188 |
+
const playerContainer = document.querySelector('.player-container'); // Ensure this selector is correct
|
| 1189 |
+
if (playerContainer) { // Check if playerContainer exists
|
| 1190 |
+
playerContainer.appendChild(hint);
|
| 1191 |
+
} else {
|
| 1192 |
+
return; // Exit if container not found
|
| 1193 |
+
}
|
| 1194 |
+
|
| 1195 |
+
// 显示提示
|
| 1196 |
+
setTimeout(() => {
|
| 1197 |
+
hint.classList.add('show');
|
| 1198 |
+
|
| 1199 |
+
// 3秒后隐藏
|
| 1200 |
+
setTimeout(() => {
|
| 1201 |
+
hint.classList.remove('show');
|
| 1202 |
+
setTimeout(() => hint.remove(), 300);
|
| 1203 |
+
}, 3000);
|
| 1204 |
+
}, 100);
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
// 格式化时间为 mm:ss 格式
|
| 1208 |
+
function formatTime(seconds) {
|
| 1209 |
+
if (isNaN(seconds)) return '00:00';
|
| 1210 |
+
|
| 1211 |
+
const minutes = Math.floor(seconds / 60);
|
| 1212 |
+
const remainingSeconds = Math.floor(seconds % 60);
|
| 1213 |
+
|
| 1214 |
+
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
// 开始定期保存播放进度
|
| 1218 |
+
function startProgressSaveInterval() {
|
| 1219 |
+
// 清除可能存在的旧计时器
|
| 1220 |
+
if (progressSaveInterval) {
|
| 1221 |
+
clearInterval(progressSaveInterval);
|
| 1222 |
+
}
|
| 1223 |
+
|
| 1224 |
+
// 每30秒保存一次播放进度
|
| 1225 |
+
progressSaveInterval = setInterval(saveCurrentProgress, 30000);
|
| 1226 |
+
}
|
| 1227 |
+
|
| 1228 |
+
// 保存当前播放进度
|
| 1229 |
+
function saveCurrentProgress() {
|
| 1230 |
+
if (!art || !art.video) return;
|
| 1231 |
+
const currentTime = art.video.currentTime;
|
| 1232 |
+
const duration = art.video.duration;
|
| 1233 |
+
if (!duration || currentTime < 1) return;
|
| 1234 |
+
|
| 1235 |
+
// 在localStorage中保存进度
|
| 1236 |
+
const progressKey = `videoProgress_${getVideoId()}`;
|
| 1237 |
+
const progressData = {
|
| 1238 |
+
position: currentTime,
|
| 1239 |
+
duration: duration,
|
| 1240 |
+
timestamp: Date.now()
|
| 1241 |
+
};
|
| 1242 |
+
try {
|
| 1243 |
+
localStorage.setItem(progressKey, JSON.stringify(progressData));
|
| 1244 |
+
// --- 新增:同步更新 viewingHistory 中的进度 ---
|
| 1245 |
+
try {
|
| 1246 |
+
const historyRaw = localStorage.getItem('viewingHistory');
|
| 1247 |
+
if (historyRaw) {
|
| 1248 |
+
const history = JSON.parse(historyRaw);
|
| 1249 |
+
// 用 title + 集数索引唯一标识
|
| 1250 |
+
const idx = history.findIndex(item =>
|
| 1251 |
+
item.title === currentVideoTitle &&
|
| 1252 |
+
(item.episodeIndex === undefined || item.episodeIndex === currentEpisodeIndex)
|
| 1253 |
+
);
|
| 1254 |
+
if (idx !== -1) {
|
| 1255 |
+
// 只在进度有明显变化时才更新��减少写入
|
| 1256 |
+
if (
|
| 1257 |
+
Math.abs((history[idx].playbackPosition || 0) - currentTime) > 2 ||
|
| 1258 |
+
Math.abs((history[idx].duration || 0) - duration) > 2
|
| 1259 |
+
) {
|
| 1260 |
+
history[idx].playbackPosition = currentTime;
|
| 1261 |
+
history[idx].duration = duration;
|
| 1262 |
+
history[idx].timestamp = Date.now();
|
| 1263 |
+
localStorage.setItem('viewingHistory', JSON.stringify(history));
|
| 1264 |
+
}
|
| 1265 |
+
}
|
| 1266 |
+
}
|
| 1267 |
+
} catch (e) {
|
| 1268 |
+
}
|
| 1269 |
+
} catch (e) {
|
| 1270 |
+
}
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
// 设置移动端长按三倍速播放功能
|
| 1274 |
+
function setupLongPressSpeedControl() {
|
| 1275 |
+
if (!art || !art.video) return;
|
| 1276 |
+
|
| 1277 |
+
const playerElement = document.getElementById('player');
|
| 1278 |
+
let longPressTimer = null;
|
| 1279 |
+
let originalPlaybackRate = 1.0;
|
| 1280 |
+
let isLongPress = false;
|
| 1281 |
+
|
| 1282 |
+
// 显示快速提示
|
| 1283 |
+
function showSpeedHint(speed) {
|
| 1284 |
+
showShortcutHint(`${speed}倍速`, 'right');
|
| 1285 |
+
}
|
| 1286 |
+
|
| 1287 |
+
// 禁用右键
|
| 1288 |
+
playerElement.oncontextmenu = () => {
|
| 1289 |
+
// 检测是否为移动设备
|
| 1290 |
+
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
| 1291 |
+
|
| 1292 |
+
// 只在移动设备上禁用右键
|
| 1293 |
+
if (isMobile) {
|
| 1294 |
+
const dplayerMenu = document.querySelector(".dplayer-menu");
|
| 1295 |
+
const dplayerMask = document.querySelector(".dplayer-mask");
|
| 1296 |
+
if (dplayerMenu) dplayerMenu.style.display = "none";
|
| 1297 |
+
if (dplayerMask) dplayerMask.style.display = "none";
|
| 1298 |
+
return false;
|
| 1299 |
+
}
|
| 1300 |
+
return true; // 在桌面设备上允许右键菜单
|
| 1301 |
+
};
|
| 1302 |
+
|
| 1303 |
+
// 触摸开始事件
|
| 1304 |
+
playerElement.addEventListener('touchstart', function (e) {
|
| 1305 |
+
// 检查视频是否正在播放,如果没有播放则不触发长按功能
|
| 1306 |
+
if (art.video.paused) {
|
| 1307 |
+
return; // 视频暂停时不触发长按功能
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
// 保存原始播放速度
|
| 1311 |
+
originalPlaybackRate = art.video.playbackRate;
|
| 1312 |
+
|
| 1313 |
+
// 设置长按计时器
|
| 1314 |
+
longPressTimer = setTimeout(() => {
|
| 1315 |
+
// 再次检查视频是否仍在播放
|
| 1316 |
+
if (art.video.paused) {
|
| 1317 |
+
clearTimeout(longPressTimer);
|
| 1318 |
+
longPressTimer = null;
|
| 1319 |
+
return;
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
// 长按超过500ms,设置为3倍速
|
| 1323 |
+
art.video.playbackRate = 3.0;
|
| 1324 |
+
isLongPress = true;
|
| 1325 |
+
showSpeedHint(3.0);
|
| 1326 |
+
|
| 1327 |
+
// 只在确认为长按时阻止默认行为
|
| 1328 |
+
e.preventDefault();
|
| 1329 |
+
}, 500);
|
| 1330 |
+
}, { passive: false });
|
| 1331 |
+
|
| 1332 |
+
// 触摸结束事件
|
| 1333 |
+
playerElement.addEventListener('touchend', function (e) {
|
| 1334 |
+
// 清除长按计时器
|
| 1335 |
+
if (longPressTimer) {
|
| 1336 |
+
clearTimeout(longPressTimer);
|
| 1337 |
+
longPressTimer = null;
|
| 1338 |
+
}
|
| 1339 |
+
|
| 1340 |
+
// 如果是长按状态,恢复原始播放速度
|
| 1341 |
+
if (isLongPress) {
|
| 1342 |
+
art.video.playbackRate = originalPlaybackRate;
|
| 1343 |
+
isLongPress = false;
|
| 1344 |
+
showSpeedHint(originalPlaybackRate);
|
| 1345 |
+
|
| 1346 |
+
// 阻止长按后的点击事件
|
| 1347 |
+
e.preventDefault();
|
| 1348 |
+
}
|
| 1349 |
+
// 如果不是长按,则允许正常的点击事件(暂停/播放)
|
| 1350 |
+
});
|
| 1351 |
+
|
| 1352 |
+
// 触摸取消事件
|
| 1353 |
+
playerElement.addEventListener('touchcancel', function () {
|
| 1354 |
+
// 清除长按计时器
|
| 1355 |
+
if (longPressTimer) {
|
| 1356 |
+
clearTimeout(longPressTimer);
|
| 1357 |
+
longPressTimer = null;
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
// 如果是长按状态,恢复原始播放速度
|
| 1361 |
+
if (isLongPress) {
|
| 1362 |
+
art.video.playbackRate = originalPlaybackRate;
|
| 1363 |
+
isLongPress = false;
|
| 1364 |
+
}
|
| 1365 |
+
});
|
| 1366 |
+
|
| 1367 |
+
// 触摸移动事件 - 防止在长按时触发页面滚动
|
| 1368 |
+
playerElement.addEventListener('touchmove', function (e) {
|
| 1369 |
+
if (isLongPress) {
|
| 1370 |
+
e.preventDefault();
|
| 1371 |
+
}
|
| 1372 |
+
}, { passive: false });
|
| 1373 |
+
|
| 1374 |
+
// 视频暂停时取消长按状态
|
| 1375 |
+
art.video.addEventListener('pause', function () {
|
| 1376 |
+
if (isLongPress) {
|
| 1377 |
+
art.video.playbackRate = originalPlaybackRate;
|
| 1378 |
+
isLongPress = false;
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
if (longPressTimer) {
|
| 1382 |
+
clearTimeout(longPressTimer);
|
| 1383 |
+
longPressTimer = null;
|
| 1384 |
+
}
|
| 1385 |
+
});
|
| 1386 |
+
}
|
| 1387 |
+
|
| 1388 |
+
// 清除视频进度记录
|
| 1389 |
+
function clearVideoProgress() {
|
| 1390 |
+
const progressKey = `videoProgress_${getVideoId()}`;
|
| 1391 |
+
try {
|
| 1392 |
+
localStorage.removeItem(progressKey);
|
| 1393 |
+
} catch (e) {
|
| 1394 |
+
}
|
| 1395 |
+
}
|
| 1396 |
+
|
| 1397 |
+
// 获取视频唯一标识
|
| 1398 |
+
function getVideoId() {
|
| 1399 |
+
// 使用视频标题和集数索引作为唯一标识
|
| 1400 |
+
// If currentVideoUrl is available and more unique, prefer it. Otherwise, fallback.
|
| 1401 |
+
if (currentVideoUrl) {
|
| 1402 |
+
return `${encodeURIComponent(currentVideoUrl)}`;
|
| 1403 |
+
}
|
| 1404 |
+
return `${encodeURIComponent(currentVideoTitle)}_${currentEpisodeIndex}`;
|
| 1405 |
+
}
|
| 1406 |
+
|
| 1407 |
+
let controlsLocked = false;
|
| 1408 |
+
function toggleControlsLock() {
|
| 1409 |
+
const container = document.getElementById('playerContainer');
|
| 1410 |
+
controlsLocked = !controlsLocked;
|
| 1411 |
+
container.classList.toggle('controls-locked', controlsLocked);
|
| 1412 |
+
const icon = document.getElementById('lockIcon');
|
| 1413 |
+
// 切换图标:锁 / 解锁
|
| 1414 |
+
icon.innerHTML = controlsLocked
|
| 1415 |
+
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d=\"M12 15v2m0-8V7a4 4 0 00-8 0v2m8 0H4v8h16v-8H6v-6z\"/>'
|
| 1416 |
+
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d=\"M15 11V7a3 3 0 00-6 0v4m-3 4h12v6H6v-6z\"/>';
|
| 1417 |
+
}
|
| 1418 |
+
|
| 1419 |
+
// 支持在iframe中关闭播放器
|
| 1420 |
+
function closeEmbeddedPlayer() {
|
| 1421 |
+
try {
|
| 1422 |
+
if (window.self !== window.top) {
|
| 1423 |
+
// 如果在iframe中,尝试调用父窗口的关闭方法
|
| 1424 |
+
if (window.parent && typeof window.parent.closeVideoPlayer === 'function') {
|
| 1425 |
+
window.parent.closeVideoPlayer();
|
| 1426 |
+
return true;
|
| 1427 |
+
}
|
| 1428 |
+
}
|
| 1429 |
+
} catch (e) {
|
| 1430 |
+
console.error('尝试关闭嵌入式播放器失败:', e);
|
| 1431 |
+
}
|
| 1432 |
+
return false;
|
| 1433 |
+
}
|
| 1434 |
+
|
| 1435 |
+
function renderResourceInfoBar() {
|
| 1436 |
+
// 获取容器元素
|
| 1437 |
+
const container = document.getElementById('resourceInfoBarContainer');
|
| 1438 |
+
if (!container) {
|
| 1439 |
+
console.error('找不到资源信息卡片容器');
|
| 1440 |
+
return;
|
| 1441 |
+
}
|
| 1442 |
+
|
| 1443 |
+
// 获取当前视频 source_code
|
| 1444 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 1445 |
+
const currentSource = urlParams.get('source') || '';
|
| 1446 |
+
|
| 1447 |
+
// 显示临时加载状态
|
| 1448 |
+
container.innerHTML = `
|
| 1449 |
+
<div class="resource-info-bar-left flex">
|
| 1450 |
+
<span>加载中...</span>
|
| 1451 |
+
<span class="resource-info-bar-videos">-</span>
|
| 1452 |
+
</div>
|
| 1453 |
+
<button class="resource-switch-btn flex" id="switchResourceBtn" onclick="showSwitchResourceModal()">
|
| 1454 |
+
<span class="resource-switch-icon">
|
| 1455 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 4v16m0 0l-6-6m6 6l6-6" stroke="#a67c2d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
| 1456 |
+
</span>
|
| 1457 |
+
切换资源
|
| 1458 |
+
</button>
|
| 1459 |
+
`;
|
| 1460 |
+
|
| 1461 |
+
// 查找当前源名称,从 API_SITES 和 custom_api 中查找即可
|
| 1462 |
+
let resourceName = currentSource
|
| 1463 |
+
if (currentSource && API_SITES[currentSource]) {
|
| 1464 |
+
resourceName = API_SITES[currentSource].name;
|
| 1465 |
+
}
|
| 1466 |
+
if (resourceName === currentSource) {
|
| 1467 |
+
const customAPIs = JSON.parse(localStorage.getItem('customAPIs') || '[]');
|
| 1468 |
+
const customIndex = parseInt(currentSource.replace('custom_', ''), 10);
|
| 1469 |
+
if (customAPIs[customIndex]) {
|
| 1470 |
+
resourceName = customAPIs[customIndex].name || '自定义资源';
|
| 1471 |
+
}
|
| 1472 |
+
}
|
| 1473 |
+
|
| 1474 |
+
container.innerHTML = `
|
| 1475 |
+
<div class="resource-info-bar-left flex">
|
| 1476 |
+
<span>${resourceName}</span>
|
| 1477 |
+
<span class="resource-info-bar-videos">${currentEpisodes.length} 个视频</span>
|
| 1478 |
+
</div>
|
| 1479 |
+
<button class="resource-switch-btn flex" id="switchResourceBtn" onclick="showSwitchResourceModal()">
|
| 1480 |
+
<span class="resource-switch-icon">
|
| 1481 |
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 4v16m0 0l-6-6m6 6l6-6" stroke="#a67c2d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
| 1482 |
+
</span>
|
| 1483 |
+
切换资源
|
| 1484 |
+
</button>
|
| 1485 |
+
`;
|
| 1486 |
+
}
|
| 1487 |
+
|
| 1488 |
+
async function showSwitchResourceModal() {
|
| 1489 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 1490 |
+
const currentSourceCode = urlParams.get('source');
|
| 1491 |
+
const currentVideoId = urlParams.get('id');
|
| 1492 |
+
|
| 1493 |
+
const modal = document.getElementById('modal');
|
| 1494 |
+
const modalTitle = document.getElementById('modalTitle');
|
| 1495 |
+
const modalContent = document.getElementById('modalContent');
|
| 1496 |
+
|
| 1497 |
+
modalTitle.innerHTML = `<span class="break-words">${currentVideoTitle}</span>`;
|
| 1498 |
+
modalContent.innerHTML = '<div style="text-align:center;padding:20px;color:#aaa;grid-column:1/-1;">正在加载资源列表...</div>';
|
| 1499 |
+
modal.classList.remove('hidden');
|
| 1500 |
+
|
| 1501 |
+
// 搜索
|
| 1502 |
+
const resourceOptions = selectedAPIs.map((curr) => {
|
| 1503 |
+
if (API_SITES[curr]) {
|
| 1504 |
+
return { key: curr, name: API_SITES[curr].name };
|
| 1505 |
+
}
|
| 1506 |
+
const customIndex = parseInt(curr.replace('custom_', ''), 10);
|
| 1507 |
+
if (customAPIs[customIndex]) {
|
| 1508 |
+
return { key: curr, name: customAPIs[customIndex].name || '自定义资源' };
|
| 1509 |
+
}
|
| 1510 |
+
return { key: curr, name: '未知资源' };
|
| 1511 |
+
});
|
| 1512 |
+
let allResults = {};
|
| 1513 |
+
await Promise.all(resourceOptions.map(async (opt) => {
|
| 1514 |
+
let queryResult = await searchByAPIAndKeyWord(opt.key, currentVideoTitle);
|
| 1515 |
+
if (queryResult.length == 0) {
|
| 1516 |
+
return
|
| 1517 |
+
}
|
| 1518 |
+
// 优先取完全同名资源,否则默认取第一个
|
| 1519 |
+
let result = queryResult[0]
|
| 1520 |
+
queryResult.forEach((res) => {
|
| 1521 |
+
if (res.vod_name == currentVideoTitle) {
|
| 1522 |
+
result = res;
|
| 1523 |
+
}
|
| 1524 |
+
})
|
| 1525 |
+
allResults[opt.key] = result;
|
| 1526 |
+
}));
|
| 1527 |
+
|
| 1528 |
+
// 对结果进行排序
|
| 1529 |
+
const sortedResults = Object.entries(allResults).sort(([keyA, resultA], [keyB, resultB]) => {
|
| 1530 |
+
// 当前播放的源放在最前面
|
| 1531 |
+
const isCurrentA = String(keyA) === String(currentSourceCode) && String(resultA.vod_id) === String(currentVideoId);
|
| 1532 |
+
const isCurrentB = String(keyB) === String(currentSourceCode) && String(resultB.vod_id) === String(currentVideoId);
|
| 1533 |
+
|
| 1534 |
+
if (isCurrentA && !isCurrentB) return -1;
|
| 1535 |
+
if (!isCurrentA && isCurrentB) return 1;
|
| 1536 |
+
|
| 1537 |
+
// 其余按照 selectedAPIs 的顺序排列
|
| 1538 |
+
const indexA = selectedAPIs.indexOf(keyA);
|
| 1539 |
+
const indexB = selectedAPIs.indexOf(keyB);
|
| 1540 |
+
|
| 1541 |
+
return indexA - indexB;
|
| 1542 |
+
});
|
| 1543 |
+
|
| 1544 |
+
// 渲染资源列表
|
| 1545 |
+
let html = '<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3 p-4">';
|
| 1546 |
+
|
| 1547 |
+
for (const [sourceKey, result] of sortedResults) {
|
| 1548 |
+
if (!result) continue;
|
| 1549 |
+
|
| 1550 |
+
// 修复 isCurrentSource 判断,确保类型一致
|
| 1551 |
+
const isCurrentSource = String(sourceKey) === String(currentSourceCode) && String(result.vod_id) === String(currentVideoId);
|
| 1552 |
+
const sourceName = resourceOptions.find(opt => opt.key === sourceKey)?.name || '未知资源';
|
| 1553 |
+
|
| 1554 |
+
html += `
|
| 1555 |
+
<div class="relative group ${isCurrentSource ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:scale-105 transition-transform'}"
|
| 1556 |
+
${!isCurrentSource ? `onclick="switchToResource('${sourceKey}', '${result.vod_id}')"` : ''}>
|
| 1557 |
+
<div class="aspect-[2/3] rounded-lg overflow-hidden bg-gray-800">
|
| 1558 |
+
<img src="${result.vod_pic}"
|
| 1559 |
+
alt="${result.vod_name}"
|
| 1560 |
+
class="w-full h-full object-cover"
|
| 1561 |
+
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjY2IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHJlY3QgeD0iMyIgeT0iMyIgd2lkdGg9IjE4IiBoZWlnaHQ9IjE4IiByeD0iMiIgcnk9IjIiPjwvcmVjdD48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtNCI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjE3IDggMTIgMyA3IDgiPjwvcG9seWxpbmU+PHBhdGggZD0iTTEyIDN2MTIiPjwvcGF0aD48L3N2Zz4='">
|
| 1562 |
+
</div>
|
| 1563 |
+
<div class="mt-1">
|
| 1564 |
+
<div class="text-xs font-medium text-gray-200 truncate">${result.vod_name}</div>
|
| 1565 |
+
<div class="text-[10px] text-gray-400">${sourceName}</div>
|
| 1566 |
+
</div>
|
| 1567 |
+
${isCurrentSource ? `
|
| 1568 |
+
<div class="absolute inset-0 flex items-center justify-center">
|
| 1569 |
+
<div class="bg-black bg-opacity-50 rounded-lg px-2 py-0.5 text-xs text-white">
|
| 1570 |
+
当前播放
|
| 1571 |
+
</div>
|
| 1572 |
+
</div>
|
| 1573 |
+
` : ''}
|
| 1574 |
+
</div>
|
| 1575 |
+
`;
|
| 1576 |
+
}
|
| 1577 |
+
|
| 1578 |
+
html += '</div>';
|
| 1579 |
+
modalContent.innerHTML = html;
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
+
// 切换资源的函数
|
| 1583 |
+
async function switchToResource(sourceKey, vodId) {
|
| 1584 |
+
// 关闭模态框
|
| 1585 |
+
document.getElementById('modal').classList.add('hidden');
|
| 1586 |
+
|
| 1587 |
+
showLoading();
|
| 1588 |
+
try {
|
| 1589 |
+
// 构建API参数
|
| 1590 |
+
let apiParams = '';
|
| 1591 |
+
|
| 1592 |
+
// 处理自定义API源
|
| 1593 |
+
if (sourceKey.startsWith('custom_')) {
|
| 1594 |
+
const customIndex = sourceKey.replace('custom_', '');
|
| 1595 |
+
const customApi = getCustomApiInfo(customIndex);
|
| 1596 |
+
if (!customApi) {
|
| 1597 |
+
showToast('自定义API配置无效', 'error');
|
| 1598 |
+
hideLoading();
|
| 1599 |
+
return;
|
| 1600 |
+
}
|
| 1601 |
+
// 传递 detail 字段
|
| 1602 |
+
if (customApi.detail) {
|
| 1603 |
+
apiParams = '&customApi=' + encodeURIComponent(customApi.url) + '&customDetail=' + encodeURIComponent(customApi.detail) + '&source=custom';
|
| 1604 |
+
} else {
|
| 1605 |
+
apiParams = '&customApi=' + encodeURIComponent(customApi.url) + '&source=custom';
|
| 1606 |
+
}
|
| 1607 |
+
} else {
|
| 1608 |
+
// 内置API
|
| 1609 |
+
apiParams = '&source=' + sourceKey;
|
| 1610 |
+
}
|
| 1611 |
+
|
| 1612 |
+
// Add a timestamp to prevent caching
|
| 1613 |
+
const timestamp = new Date().getTime();
|
| 1614 |
+
const cacheBuster = `&_t=${timestamp}`;
|
| 1615 |
+
const response = await fetch(`/api/detail?id=${encodeURIComponent(vodId)}${apiParams}${cacheBuster}`);
|
| 1616 |
+
|
| 1617 |
+
const data = await response.json();
|
| 1618 |
+
|
| 1619 |
+
if (!data.episodes || data.episodes.length === 0) {
|
| 1620 |
+
showToast('未找到播放资源', 'error');
|
| 1621 |
+
hideLoading();
|
| 1622 |
+
return;
|
| 1623 |
+
}
|
| 1624 |
+
|
| 1625 |
+
// 获取当前播放的集数索引
|
| 1626 |
+
const currentIndex = currentEpisodeIndex;
|
| 1627 |
+
|
| 1628 |
+
// 确定要播放的集数索引
|
| 1629 |
+
let targetIndex = 0;
|
| 1630 |
+
if (currentIndex < data.episodes.length) {
|
| 1631 |
+
// 如果当前集数在新资源中存在,则使用相同集数
|
| 1632 |
+
targetIndex = currentIndex;
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
// 获取目标集数的URL
|
| 1636 |
+
const targetUrl = data.episodes[targetIndex];
|
| 1637 |
+
|
| 1638 |
+
// 构建播放页面URL
|
| 1639 |
+
const watchUrl = `player.html?id=${vodId}&source=${sourceKey}&url=${encodeURIComponent(targetUrl)}&index=${targetIndex}&title=${encodeURIComponent(currentVideoTitle)}`;
|
| 1640 |
+
|
| 1641 |
+
// 保存当前状态到localStorage
|
| 1642 |
+
try {
|
| 1643 |
+
localStorage.setItem('currentVideoTitle', data.vod_name || '未知视频');
|
| 1644 |
+
localStorage.setItem('currentEpisodes', JSON.stringify(data.episodes));
|
| 1645 |
+
localStorage.setItem('currentEpisodeIndex', targetIndex);
|
| 1646 |
+
localStorage.setItem('currentSourceCode', sourceKey);
|
| 1647 |
+
localStorage.setItem('lastPlayTime', Date.now());
|
| 1648 |
+
} catch (e) {
|
| 1649 |
+
console.error('保存播放状态失败:', e);
|
| 1650 |
+
}
|
| 1651 |
+
|
| 1652 |
+
// 跳转到播放页面
|
| 1653 |
+
window.location.href = watchUrl;
|
| 1654 |
+
|
| 1655 |
+
} catch (error) {
|
| 1656 |
+
console.error('切换资源失败:', error);
|
| 1657 |
+
showToast('切换资源失败,请稍后重试', 'error');
|
| 1658 |
+
} finally {
|
| 1659 |
+
hideLoading();
|
| 1660 |
+
}
|
| 1661 |
+
}
|
js/pwa-register.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// PWA 注册
|
| 2 |
+
if ('serviceWorker' in navigator) {
|
| 3 |
+
window.addEventListener('load', () => {
|
| 4 |
+
navigator.serviceWorker.register('/service-worker.js');
|
| 5 |
+
});
|
| 6 |
+
}
|
js/search.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
async function searchByAPIAndKeyWord(apiId, query) {
|
| 2 |
+
try {
|
| 3 |
+
let apiUrl, apiName, apiBaseUrl;
|
| 4 |
+
|
| 5 |
+
// 处理自定义API
|
| 6 |
+
if (apiId.startsWith('custom_')) {
|
| 7 |
+
const customIndex = apiId.replace('custom_', '');
|
| 8 |
+
const customApi = getCustomApiInfo(customIndex);
|
| 9 |
+
if (!customApi) return [];
|
| 10 |
+
|
| 11 |
+
apiBaseUrl = customApi.url;
|
| 12 |
+
apiUrl = apiBaseUrl + API_CONFIG.search.path + encodeURIComponent(query);
|
| 13 |
+
apiName = customApi.name;
|
| 14 |
+
} else {
|
| 15 |
+
// 内置API
|
| 16 |
+
if (!API_SITES[apiId]) return [];
|
| 17 |
+
apiBaseUrl = API_SITES[apiId].api;
|
| 18 |
+
apiUrl = apiBaseUrl + API_CONFIG.search.path + encodeURIComponent(query);
|
| 19 |
+
apiName = API_SITES[apiId].name;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// 添加超时处理
|
| 23 |
+
const controller = new AbortController();
|
| 24 |
+
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
| 25 |
+
|
| 26 |
+
const response = await fetch(PROXY_URL + encodeURIComponent(apiUrl), {
|
| 27 |
+
headers: API_CONFIG.search.headers,
|
| 28 |
+
signal: controller.signal
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
clearTimeout(timeoutId);
|
| 32 |
+
|
| 33 |
+
if (!response.ok) {
|
| 34 |
+
return [];
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const data = await response.json();
|
| 38 |
+
|
| 39 |
+
if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) {
|
| 40 |
+
return [];
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// 处理第一页结果
|
| 44 |
+
const results = data.list.map(item => ({
|
| 45 |
+
...item,
|
| 46 |
+
source_name: apiName,
|
| 47 |
+
source_code: apiId,
|
| 48 |
+
api_url: apiId.startsWith('custom_') ? getCustomApiInfo(apiId.replace('custom_', ''))?.url : undefined
|
| 49 |
+
}));
|
| 50 |
+
|
| 51 |
+
// 获取总页数
|
| 52 |
+
const pageCount = data.pagecount || 1;
|
| 53 |
+
// 确定需要获取的额外页数 (最多获取maxPages页)
|
| 54 |
+
const pagesToFetch = Math.min(pageCount - 1, API_CONFIG.search.maxPages - 1);
|
| 55 |
+
|
| 56 |
+
// 如果有额外页数,获取更多页的结果
|
| 57 |
+
if (pagesToFetch > 0) {
|
| 58 |
+
const additionalPagePromises = [];
|
| 59 |
+
|
| 60 |
+
for (let page = 2; page <= pagesToFetch + 1; page++) {
|
| 61 |
+
// 构建分页URL
|
| 62 |
+
const pageUrl = apiBaseUrl + API_CONFIG.search.pagePath
|
| 63 |
+
.replace('{query}', encodeURIComponent(query))
|
| 64 |
+
.replace('{page}', page);
|
| 65 |
+
|
| 66 |
+
// 创建获取额外页的Promise
|
| 67 |
+
const pagePromise = (async () => {
|
| 68 |
+
try {
|
| 69 |
+
const pageController = new AbortController();
|
| 70 |
+
const pageTimeoutId = setTimeout(() => pageController.abort(), 8000);
|
| 71 |
+
|
| 72 |
+
const pageResponse = await fetch(PROXY_URL + encodeURIComponent(pageUrl), {
|
| 73 |
+
headers: API_CONFIG.search.headers,
|
| 74 |
+
signal: pageController.signal
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
clearTimeout(pageTimeoutId);
|
| 78 |
+
|
| 79 |
+
if (!pageResponse.ok) return [];
|
| 80 |
+
|
| 81 |
+
const pageData = await pageResponse.json();
|
| 82 |
+
|
| 83 |
+
if (!pageData || !pageData.list || !Array.isArray(pageData.list)) return [];
|
| 84 |
+
|
| 85 |
+
// 处理当前页结果
|
| 86 |
+
return pageData.list.map(item => ({
|
| 87 |
+
...item,
|
| 88 |
+
source_name: apiName,
|
| 89 |
+
source_code: apiId,
|
| 90 |
+
api_url: apiId.startsWith('custom_') ? getCustomApiInfo(apiId.replace('custom_', ''))?.url : undefined
|
| 91 |
+
}));
|
| 92 |
+
} catch (error) {
|
| 93 |
+
console.warn(`API ${apiId} 第${page}页搜索失败:`, error);
|
| 94 |
+
return [];
|
| 95 |
+
}
|
| 96 |
+
})();
|
| 97 |
+
|
| 98 |
+
additionalPagePromises.push(pagePromise);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// 等待所有额外页的结果
|
| 102 |
+
const additionalResults = await Promise.all(additionalPagePromises);
|
| 103 |
+
|
| 104 |
+
// 合并所有页的结果
|
| 105 |
+
additionalResults.forEach(pageResults => {
|
| 106 |
+
if (pageResults.length > 0) {
|
| 107 |
+
results.push(...pageResults);
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return results;
|
| 113 |
+
} catch (error) {
|
| 114 |
+
console.warn(`API ${apiId} 搜索失败:`, error);
|
| 115 |
+
return [];
|
| 116 |
+
}
|
| 117 |
+
}
|
js/sha256.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export async function sha256(message) {
|
| 2 |
+
const msgBuffer = new TextEncoder().encode(message);
|
| 3 |
+
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
| 4 |
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
| 5 |
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
| 6 |
+
}
|
js/ui.js
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// UI相关函数
|
| 2 |
+
function toggleSettings(e) {
|
| 3 |
+
// 密码保护校验
|
| 4 |
+
if (window.isPasswordProtected && window.isPasswordVerified) {
|
| 5 |
+
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
|
| 6 |
+
showPasswordModal && showPasswordModal();
|
| 7 |
+
return;
|
| 8 |
+
}
|
| 9 |
+
}
|
| 10 |
+
// 阻止事件冒泡,防止触发document的点击事件
|
| 11 |
+
e && e.stopPropagation();
|
| 12 |
+
const panel = document.getElementById('settingsPanel');
|
| 13 |
+
panel.classList.toggle('show');
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// 改进的Toast显示函数 - 支持队列显示多个Toast
|
| 17 |
+
const toastQueue = [];
|
| 18 |
+
let isShowingToast = false;
|
| 19 |
+
|
| 20 |
+
function showToast(message, type = 'error') {
|
| 21 |
+
// 首先确保toast元素存在
|
| 22 |
+
let toast = document.getElementById('toast');
|
| 23 |
+
let toastMessage = document.getElementById('toastMessage');
|
| 24 |
+
|
| 25 |
+
// 如果toast元素不存在,创建它
|
| 26 |
+
if (!toast) {
|
| 27 |
+
toast = document.createElement('div');
|
| 28 |
+
toast.id = 'toast';
|
| 29 |
+
toast.className = 'fixed top-4 left-1/2 -translate-x-1/2 px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 z-50 opacity-0';
|
| 30 |
+
toast.style = 'z-index: 2147483647'
|
| 31 |
+
toastMessage = document.createElement('p');
|
| 32 |
+
toastMessage.id = 'toastMessage';
|
| 33 |
+
toast.appendChild(toastMessage);
|
| 34 |
+
|
| 35 |
+
document.body.appendChild(toast);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// 将新的toast添加到队列
|
| 39 |
+
toastQueue.push({ message, type });
|
| 40 |
+
|
| 41 |
+
// 如果当前没有显示中的toast,则开始显示
|
| 42 |
+
if (!isShowingToast) {
|
| 43 |
+
showNextToast();
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function showNextToast() {
|
| 48 |
+
if (toastQueue.length === 0) {
|
| 49 |
+
isShowingToast = false;
|
| 50 |
+
return;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
isShowingToast = true;
|
| 54 |
+
const { message, type } = toastQueue.shift();
|
| 55 |
+
|
| 56 |
+
const toast = document.getElementById('toast');
|
| 57 |
+
const toastMessage = document.getElementById('toastMessage');
|
| 58 |
+
|
| 59 |
+
const bgColors = {
|
| 60 |
+
'error': 'bg-red-500',
|
| 61 |
+
'success': 'bg-green-500',
|
| 62 |
+
'info': 'bg-blue-500',
|
| 63 |
+
'warning': 'bg-yellow-500'
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
const bgColor = bgColors[type] || bgColors.error;
|
| 67 |
+
toast.className = `fixed top-4 left-1/2 -translate-x-1/2 px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 ${bgColor} text-white z-50`;
|
| 68 |
+
toastMessage.textContent = message;
|
| 69 |
+
|
| 70 |
+
// 显示提示
|
| 71 |
+
toast.style.opacity = '1';
|
| 72 |
+
toast.style.transform = 'translateX(-50%) translateY(0)';
|
| 73 |
+
|
| 74 |
+
// 3秒后自动隐藏
|
| 75 |
+
setTimeout(() => {
|
| 76 |
+
toast.style.opacity = '0';
|
| 77 |
+
toast.style.transform = 'translateX(-50%) translateY(-100%)';
|
| 78 |
+
|
| 79 |
+
// 等待动画完成后显示下一个toast
|
| 80 |
+
setTimeout(() => {
|
| 81 |
+
showNextToast();
|
| 82 |
+
}, 300);
|
| 83 |
+
}, 3000);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// 添加显示/隐藏 loading 的函数
|
| 87 |
+
let loadingTimeoutId = null;
|
| 88 |
+
|
| 89 |
+
function showLoading(message = '加载中...') {
|
| 90 |
+
// 清除任何现有的超时
|
| 91 |
+
if (loadingTimeoutId) {
|
| 92 |
+
clearTimeout(loadingTimeoutId);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
const loading = document.getElementById('loading');
|
| 96 |
+
const messageEl = loading.querySelector('p');
|
| 97 |
+
messageEl.textContent = message;
|
| 98 |
+
loading.style.display = 'flex';
|
| 99 |
+
|
| 100 |
+
// 设置30秒后自动关闭loading,防止无限loading
|
| 101 |
+
loadingTimeoutId = setTimeout(() => {
|
| 102 |
+
hideLoading();
|
| 103 |
+
showToast('操作超时,请稍后重试', 'warning');
|
| 104 |
+
}, 30000);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
function hideLoading() {
|
| 108 |
+
// 清除超时
|
| 109 |
+
if (loadingTimeoutId) {
|
| 110 |
+
clearTimeout(loadingTimeoutId);
|
| 111 |
+
loadingTimeoutId = null;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
const loading = document.getElementById('loading');
|
| 115 |
+
loading.style.display = 'none';
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
function updateSiteStatus(isAvailable) {
|
| 119 |
+
const statusEl = document.getElementById('siteStatus');
|
| 120 |
+
if (isAvailable) {
|
| 121 |
+
statusEl.innerHTML = '<span class="text-green-500">●</span> 可用';
|
| 122 |
+
} else {
|
| 123 |
+
statusEl.innerHTML = '<span class="text-red-500">●</span> 不可用';
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
function closeModal() {
|
| 128 |
+
document.getElementById('modal').classList.add('hidden');
|
| 129 |
+
// 清除 iframe 内容
|
| 130 |
+
document.getElementById('modalContent').innerHTML = '';
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// 获取搜索历史的增强版本 - 支持新旧格式
|
| 134 |
+
function getSearchHistory() {
|
| 135 |
+
try {
|
| 136 |
+
const data = localStorage.getItem(SEARCH_HISTORY_KEY);
|
| 137 |
+
if (!data) return [];
|
| 138 |
+
|
| 139 |
+
const parsed = JSON.parse(data);
|
| 140 |
+
|
| 141 |
+
// 检查是否是数组
|
| 142 |
+
if (!Array.isArray(parsed)) return [];
|
| 143 |
+
|
| 144 |
+
// 支持旧格式(字符串数组)和新格式(对象数组)
|
| 145 |
+
return parsed.map(item => {
|
| 146 |
+
if (typeof item === 'string') {
|
| 147 |
+
return { text: item, timestamp: 0 };
|
| 148 |
+
}
|
| 149 |
+
return item;
|
| 150 |
+
}).filter(item => item && item.text);
|
| 151 |
+
} catch (e) {
|
| 152 |
+
console.error('获取搜索历史出错:', e);
|
| 153 |
+
return [];
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// 保存搜索历史的增强版本 - 添加时间戳和最大数量限制,现在缓存2个月
|
| 158 |
+
function saveSearchHistory(query) {
|
| 159 |
+
if (!query || !query.trim()) return;
|
| 160 |
+
|
| 161 |
+
// 清理输入,防止XSS
|
| 162 |
+
query = query.trim().substring(0, 50).replace(/</g, '<').replace(/>/g, '>');
|
| 163 |
+
|
| 164 |
+
let history = getSearchHistory();
|
| 165 |
+
|
| 166 |
+
// 获取当前时间
|
| 167 |
+
const now = Date.now();
|
| 168 |
+
|
| 169 |
+
// 过滤掉超过2个月的记录(约60天,60*24*60*60*1000 = 5184000000毫秒)
|
| 170 |
+
history = history.filter(item =>
|
| 171 |
+
typeof item === 'object' && item.timestamp && (now - item.timestamp < 5184000000)
|
| 172 |
+
);
|
| 173 |
+
|
| 174 |
+
// 删除已存在的相同项
|
| 175 |
+
history = history.filter(item =>
|
| 176 |
+
typeof item === 'object' ? item.text !== query : item !== query
|
| 177 |
+
);
|
| 178 |
+
|
| 179 |
+
// 新项添加到开头,包含时间戳
|
| 180 |
+
history.unshift({
|
| 181 |
+
text: query,
|
| 182 |
+
timestamp: now
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
// 限制历史记录数量
|
| 186 |
+
if (history.length > MAX_HISTORY_ITEMS) {
|
| 187 |
+
history = history.slice(0, MAX_HISTORY_ITEMS);
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
try {
|
| 191 |
+
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history));
|
| 192 |
+
} catch (e) {
|
| 193 |
+
console.error('保存搜索历史失败:', e);
|
| 194 |
+
// 如果存储失败(可能是localStorage已满),尝试清理旧数据
|
| 195 |
+
try {
|
| 196 |
+
localStorage.removeItem(SEARCH_HISTORY_KEY);
|
| 197 |
+
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history.slice(0, 3)));
|
| 198 |
+
} catch (e2) {
|
| 199 |
+
console.error('再次保存搜索历史失败:', e2);
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
renderSearchHistory();
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// 渲染最近搜索历史的增强版本
|
| 207 |
+
function renderSearchHistory() {
|
| 208 |
+
const historyContainer = document.getElementById('recentSearches');
|
| 209 |
+
if (!historyContainer) return;
|
| 210 |
+
|
| 211 |
+
const history = getSearchHistory();
|
| 212 |
+
|
| 213 |
+
if (history.length === 0) {
|
| 214 |
+
historyContainer.innerHTML = '';
|
| 215 |
+
return;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// 创建一个包含标题和清除按钮的行
|
| 219 |
+
historyContainer.innerHTML = `
|
| 220 |
+
<div class="flex justify-between items-center w-full mb-2">
|
| 221 |
+
<div class="text-gray-500">最近搜索:</div>
|
| 222 |
+
<button id="clearHistoryBtn" class="text-gray-500 hover:text-white transition-colors"
|
| 223 |
+
onclick="clearSearchHistory()" aria-label="清除搜索历史">
|
| 224 |
+
清除搜索历史
|
| 225 |
+
</button>
|
| 226 |
+
</div>
|
| 227 |
+
`;
|
| 228 |
+
|
| 229 |
+
history.forEach(item => {
|
| 230 |
+
const tag = document.createElement('button');
|
| 231 |
+
tag.className = 'search-tag flex items-center gap-1';
|
| 232 |
+
const textSpan = document.createElement('span');
|
| 233 |
+
textSpan.textContent = item.text;
|
| 234 |
+
tag.appendChild(textSpan);
|
| 235 |
+
|
| 236 |
+
// 添加删除按钮
|
| 237 |
+
const deleteButton = document.createElement('span');
|
| 238 |
+
deleteButton.className = 'pl-1 text-gray-500 hover:text-red-500 transition-colors';
|
| 239 |
+
deleteButton.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
|
| 240 |
+
deleteButton.onclick = function(e) {
|
| 241 |
+
// 阻止事件冒泡,避免触发搜索
|
| 242 |
+
e.stopPropagation();
|
| 243 |
+
// 删除对应历史记录
|
| 244 |
+
deleteSingleSearchHistory(item.text);
|
| 245 |
+
// 重新渲染搜索历史
|
| 246 |
+
renderSearchHistory();
|
| 247 |
+
};
|
| 248 |
+
tag.appendChild(deleteButton);
|
| 249 |
+
|
| 250 |
+
// 添加时间提示(如果有时间戳)
|
| 251 |
+
if (item.timestamp) {
|
| 252 |
+
const date = new Date(item.timestamp);
|
| 253 |
+
tag.title = `搜索于: ${date.toLocaleString()}`;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
tag.onclick = function() {
|
| 257 |
+
document.getElementById('searchInput').value = item.text;
|
| 258 |
+
search();
|
| 259 |
+
};
|
| 260 |
+
historyContainer.appendChild(tag);
|
| 261 |
+
});
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
// 删除单条搜索历史记录
|
| 265 |
+
function deleteSingleSearchHistory(query) {
|
| 266 |
+
// 当url中包含删除的关键词时,页面刷新后会自动加入历史记录,导致误认为删除功能有bug。此问题无需修复,功能无实际影响。
|
| 267 |
+
try {
|
| 268 |
+
let history = getSearchHistory();
|
| 269 |
+
// 过滤掉要删除的记录
|
| 270 |
+
history = history.filter(item => item.text !== query);
|
| 271 |
+
console.log('更新后的搜索历史:', history);
|
| 272 |
+
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(history));
|
| 273 |
+
} catch (e) {
|
| 274 |
+
console.error('删除单条搜索历史失败:', e);
|
| 275 |
+
showToast('删除单条搜索历史失败', 'error');
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// 增加清除搜索历史功能
|
| 280 |
+
function clearSearchHistory() {
|
| 281 |
+
// 密码保护校验
|
| 282 |
+
if (window.isPasswordProtected && window.isPasswordVerified) {
|
| 283 |
+
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
|
| 284 |
+
showPasswordModal && showPasswordModal();
|
| 285 |
+
return;
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
try {
|
| 289 |
+
localStorage.removeItem(SEARCH_HISTORY_KEY);
|
| 290 |
+
renderSearchHistory();
|
| 291 |
+
showToast('搜索历史已清除', 'success');
|
| 292 |
+
} catch (e) {
|
| 293 |
+
console.error('清除搜索历史失败:', e);
|
| 294 |
+
showToast('清除搜索历史失败:', 'error');
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// 历史面板相关函数
|
| 299 |
+
function toggleHistory(e) {
|
| 300 |
+
// 密码保护���验
|
| 301 |
+
if (window.isPasswordProtected && window.isPasswordVerified) {
|
| 302 |
+
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
|
| 303 |
+
showPasswordModal && showPasswordModal();
|
| 304 |
+
return;
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
if (e) e.stopPropagation();
|
| 308 |
+
|
| 309 |
+
const panel = document.getElementById('historyPanel');
|
| 310 |
+
if (panel) {
|
| 311 |
+
panel.classList.toggle('show');
|
| 312 |
+
|
| 313 |
+
// 如果打开了历史记录面板,则加载历史数据
|
| 314 |
+
if (panel.classList.contains('show')) {
|
| 315 |
+
loadViewingHistory();
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
// 如果设置面板是打开的,则关闭它
|
| 319 |
+
const settingsPanel = document.getElementById('settingsPanel');
|
| 320 |
+
if (settingsPanel && settingsPanel.classList.contains('show')) {
|
| 321 |
+
settingsPanel.classList.remove('show');
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// 格式化时间戳为友好的日期时间格式
|
| 327 |
+
function formatTimestamp(timestamp) {
|
| 328 |
+
const date = new Date(timestamp);
|
| 329 |
+
const now = new Date();
|
| 330 |
+
const diff = now - date;
|
| 331 |
+
|
| 332 |
+
// 小于1小时,显示"X分钟前"
|
| 333 |
+
if (diff < 3600000) {
|
| 334 |
+
const minutes = Math.floor(diff / 60000);
|
| 335 |
+
return minutes <= 0 ? '刚刚' : `${minutes}分钟前`;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// 小于24小时,显示"X小时前"
|
| 339 |
+
if (diff < 86400000) {
|
| 340 |
+
const hours = Math.floor(diff / 3600000);
|
| 341 |
+
return `${hours}小时前`;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// 小于7天,显示"X天前"
|
| 345 |
+
if (diff < 604800000) {
|
| 346 |
+
const days = Math.floor(diff / 86400000);
|
| 347 |
+
return `${days}天前`;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// 其他情况,显示完整日期
|
| 351 |
+
const year = date.getFullYear();
|
| 352 |
+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
| 353 |
+
const day = date.getDate().toString().padStart(2, '0');
|
| 354 |
+
const hour = date.getHours().toString().padStart(2, '0');
|
| 355 |
+
const minute = date.getMinutes().toString().padStart(2, '0');
|
| 356 |
+
|
| 357 |
+
return `${year}-${month}-${day} ${hour}:${minute}`;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// 获取观看历史记录
|
| 361 |
+
function getViewingHistory() {
|
| 362 |
+
try {
|
| 363 |
+
const data = localStorage.getItem('viewingHistory');
|
| 364 |
+
return data ? JSON.parse(data) : [];
|
| 365 |
+
} catch (e) {
|
| 366 |
+
console.error('获取观看历史失败:', e);
|
| 367 |
+
return [];
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
// 加载观看历史并渲染
|
| 372 |
+
function loadViewingHistory() {
|
| 373 |
+
const historyList = document.getElementById('historyList');
|
| 374 |
+
if (!historyList) return;
|
| 375 |
+
|
| 376 |
+
const history = getViewingHistory();
|
| 377 |
+
|
| 378 |
+
if (history.length === 0) {
|
| 379 |
+
historyList.innerHTML = `<div class="text-center text-gray-500 py-8">暂无观看记录</div>`;
|
| 380 |
+
return;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// 渲染历史记录
|
| 384 |
+
historyList.innerHTML = history.map(item => {
|
| 385 |
+
// 防止XSS
|
| 386 |
+
const safeTitle = item.title
|
| 387 |
+
.replace(/</g, '<')
|
| 388 |
+
.replace(/>/g, '>')
|
| 389 |
+
.replace(/"/g, '"');
|
| 390 |
+
|
| 391 |
+
const safeSource = item.sourceName ?
|
| 392 |
+
item.sourceName.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') :
|
| 393 |
+
'未知来源';
|
| 394 |
+
|
| 395 |
+
const episodeText = item.episodeIndex !== undefined ?
|
| 396 |
+
`第${item.episodeIndex + 1}集` : '';
|
| 397 |
+
|
| 398 |
+
// 格式化剧集信息
|
| 399 |
+
let episodeInfoHtml = '';
|
| 400 |
+
if (item.episodes && Array.isArray(item.episodes) && item.episodes.length > 0) {
|
| 401 |
+
const totalEpisodes = item.episodes.length;
|
| 402 |
+
const syncStatus = item.lastSyncTime ?
|
| 403 |
+
`<span class="text-green-400 text-xs" title="剧集列表已同步">✓</span>` :
|
| 404 |
+
`<span class="text-yellow-400 text-xs" title="使用缓存数据">⚠</span>`;
|
| 405 |
+
episodeInfoHtml = `<span class="text-xs text-gray-400">共${totalEpisodes}集 ${syncStatus}</span>`;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
// 格式化进度信息
|
| 409 |
+
let progressHtml = '';
|
| 410 |
+
if (item.playbackPosition && item.duration && item.playbackPosition > 10 && item.playbackPosition < item.duration * 0.95) {
|
| 411 |
+
const percent = Math.round((item.playbackPosition / item.duration) * 100);
|
| 412 |
+
const formattedTime = formatPlaybackTime(item.playbackPosition);
|
| 413 |
+
const formattedDuration = formatPlaybackTime(item.duration);
|
| 414 |
+
|
| 415 |
+
progressHtml = `
|
| 416 |
+
<div class="history-progress">
|
| 417 |
+
<div class="progress-bar">
|
| 418 |
+
<div class="progress-filled" style="width:${percent}%"></div>
|
| 419 |
+
</div>
|
| 420 |
+
<div class="progress-text">${formattedTime} / ${formattedDuration}</div>
|
| 421 |
+
</div>
|
| 422 |
+
`;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// 为防止XSS,使用encodeURIComponent编码URL
|
| 426 |
+
const safeURL = encodeURIComponent(item.url);
|
| 427 |
+
|
| 428 |
+
// 构建历史记录项HTML,添加删除按钮,需要放在position:relative的容器中
|
| 429 |
+
return `
|
| 430 |
+
<div class="history-item cursor-pointer relative group" onclick="playFromHistory('${item.url}', '${safeTitle}', ${item.episodeIndex || 0}, ${item.playbackPosition || 0})">
|
| 431 |
+
<button onclick="event.stopPropagation(); deleteHistoryItem('${safeURL}')"
|
| 432 |
+
class="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-red-400 p-1 rounded-full hover:bg-gray-800 z-10"
|
| 433 |
+
title="删除记录">
|
| 434 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 435 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
| 436 |
+
</svg>
|
| 437 |
+
</button>
|
| 438 |
+
<div class="history-info">
|
| 439 |
+
<div class="history-title">${safeTitle}</div>
|
| 440 |
+
<div class="history-meta">
|
| 441 |
+
<span class="history-episode">${episodeText}</span>
|
| 442 |
+
${episodeText ? '<span class="history-separator mx-1">·</span>' : ''}
|
| 443 |
+
<span class="history-source">${safeSource}</span>
|
| 444 |
+
${episodeInfoHtml ? '<span class="history-separator mx-1">·</span>' : ''}
|
| 445 |
+
${episodeInfoHtml}
|
| 446 |
+
</div>
|
| 447 |
+
${progressHtml}
|
| 448 |
+
<div class="history-time">${formatTimestamp(item.timestamp)}</div>
|
| 449 |
+
</div>
|
| 450 |
+
</div>
|
| 451 |
+
`;
|
| 452 |
+
}).join('');
|
| 453 |
+
|
| 454 |
+
// 检查是否存在较多历史记录,添加底部边距确保底部按钮不会挡住内容
|
| 455 |
+
if (history.length > 5) {
|
| 456 |
+
historyList.classList.add('pb-4');
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
// 格式化播放时间为 mm:ss 格式
|
| 461 |
+
function formatPlaybackTime(seconds) {
|
| 462 |
+
if (!seconds || isNaN(seconds)) return '00:00';
|
| 463 |
+
|
| 464 |
+
const minutes = Math.floor(seconds / 60);
|
| 465 |
+
const remainingSeconds = Math.floor(seconds % 60);
|
| 466 |
+
|
| 467 |
+
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
// 删除单个历史记录项
|
| 471 |
+
function deleteHistoryItem(encodedUrl) {
|
| 472 |
+
try {
|
| 473 |
+
// 解码URL
|
| 474 |
+
const url = decodeURIComponent(encodedUrl);
|
| 475 |
+
|
| 476 |
+
// 获取当前历史记录
|
| 477 |
+
const history = getViewingHistory();
|
| 478 |
+
|
| 479 |
+
// 过滤掉要删除的项
|
| 480 |
+
const newHistory = history.filter(item => item.url !== url);
|
| 481 |
+
|
| 482 |
+
// 保存回localStorage
|
| 483 |
+
localStorage.setItem('viewingHistory', JSON.stringify(newHistory));
|
| 484 |
+
|
| 485 |
+
// 重新加载历史记录显示
|
| 486 |
+
loadViewingHistory();
|
| 487 |
+
|
| 488 |
+
// 显示成功提示
|
| 489 |
+
showToast('已删除该记录', 'success');
|
| 490 |
+
} catch (e) {
|
| 491 |
+
console.error('删除历史记录项失败:', e);
|
| 492 |
+
showToast('删除记录失败', 'error');
|
| 493 |
+
}
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
// 从历史记录播放
|
| 497 |
+
async function playFromHistory(url, title, episodeIndex, playbackPosition = 0) {
|
| 498 |
+
// console.log('[playFromHistory in ui.js] Called with:', { url, title, episodeIndex, playbackPosition }); // Log 1
|
| 499 |
+
try {
|
| 500 |
+
let episodesList = [];
|
| 501 |
+
let historyItem = null; // To store the full history item
|
| 502 |
+
let syncSuccessful = false;
|
| 503 |
+
|
| 504 |
+
// 检查viewingHistory,查找匹配的项
|
| 505 |
+
const historyRaw = localStorage.getItem('viewingHistory');
|
| 506 |
+
if (historyRaw) {
|
| 507 |
+
const history = JSON.parse(historyRaw);
|
| 508 |
+
historyItem = history.find(item => item.url === url);
|
| 509 |
+
// console.log('[playFromHistory in ui.js] Found historyItem:', historyItem ? JSON.parse(JSON.stringify(historyItem)) : null); // Log 2 (stringify/parse for deep copy)
|
| 510 |
+
if (historyItem) {
|
| 511 |
+
// console.log('[playFromHistory in ui.js] historyItem.vod_id:', historyItem.vod_id, 'historyItem.sourceName:', historyItem.sourceName); // Log 3
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
if (historyItem && historyItem.episodes && Array.isArray(historyItem.episodes)) {
|
| 515 |
+
episodesList = historyItem.episodes; // Default to stored episodes
|
| 516 |
+
// console.log(`从历史记录找到视频 "${title}" 的集数数据 (默认):`, episodesList.length);
|
| 517 |
+
}
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
// Always attempt to fetch fresh episode list if we have the necessary info
|
| 521 |
+
if (historyItem && historyItem.vod_id && historyItem.sourceName) {
|
| 522 |
+
// Show loading toast to indicate syncing
|
| 523 |
+
showToast('正在同步最新剧集列表...', 'info');
|
| 524 |
+
|
| 525 |
+
// console.log(`[playFromHistory in ui.js] Attempting to fetch details for vod_id: ${historyItem.vod_id}, sourceName: ${historyItem.sourceName}`); // Log 4
|
| 526 |
+
try {
|
| 527 |
+
// Construct the API URL for detail fetching
|
| 528 |
+
// historyItem.sourceName is used as the sourceCode here
|
| 529 |
+
// Add a cache buster timestamp
|
| 530 |
+
const timestamp = new Date().getTime();
|
| 531 |
+
const apiUrl = `/api/detail?id=${encodeURIComponent(historyItem.vod_id)}&source=${encodeURIComponent(historyItem.sourceName)}&_t=${timestamp}`;
|
| 532 |
+
|
| 533 |
+
// Add timeout to the fetch request
|
| 534 |
+
const controller = new AbortController();
|
| 535 |
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
| 536 |
+
|
| 537 |
+
const response = await fetch(apiUrl, {
|
| 538 |
+
signal: controller.signal
|
| 539 |
+
});
|
| 540 |
+
clearTimeout(timeoutId);
|
| 541 |
+
|
| 542 |
+
if (!response.ok) {
|
| 543 |
+
throw new Error(`API request failed with status ${response.status}`);
|
| 544 |
+
}
|
| 545 |
+
const videoDetails = await response.json();
|
| 546 |
+
|
| 547 |
+
if (videoDetails && videoDetails.episodes && videoDetails.episodes.length > 0) {
|
| 548 |
+
const oldEpisodeCount = episodesList.length;
|
| 549 |
+
episodesList = videoDetails.episodes;
|
| 550 |
+
syncSuccessful = true;
|
| 551 |
+
|
| 552 |
+
// Show success message with episode count info
|
| 553 |
+
const newEpisodeCount = episodesList.length;
|
| 554 |
+
if (newEpisodeCount > oldEpisodeCount) {
|
| 555 |
+
showToast(`已同步最新剧集列表 (${newEpisodeCount}集,新增${newEpisodeCount - oldEpisodeCount}集)`, 'success');
|
| 556 |
+
} else if (newEpisodeCount === oldEpisodeCount) {
|
| 557 |
+
showToast(`剧集列表已是最新 (${newEpisodeCount}集)`, 'success');
|
| 558 |
+
} else {
|
| 559 |
+
showToast(`已同步最新剧集列表 (${newEpisodeCount}集)`, 'success');
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
// console.log(`成功获取 "${title}" 最新剧集列表:`, episodesList.length, "集");
|
| 563 |
+
// Update the history item in localStorage with the fresh episodes
|
| 564 |
+
if (historyItem) {
|
| 565 |
+
historyItem.episodes = [...episodesList]; // Deep copy
|
| 566 |
+
historyItem.lastSyncTime = Date.now(); // Add sync timestamp
|
| 567 |
+
const history = JSON.parse(historyRaw); // Re-parse to ensure we have the latest version
|
| 568 |
+
const idx = history.findIndex(item => item.url === url);
|
| 569 |
+
if (idx !== -1) {
|
| 570 |
+
history[idx] = { ...history[idx], ...historyItem }; // Merge, ensuring other properties are kept
|
| 571 |
+
localStorage.setItem('viewingHistory', JSON.stringify(history));
|
| 572 |
+
// console.log("观看历史中的剧集列表已更新。");
|
| 573 |
+
}
|
| 574 |
+
}
|
| 575 |
+
} else {
|
| 576 |
+
// console.log(`未能获取 "${title}" 的最新剧集列表,或列表为空。将使用已存储的剧集。`);
|
| 577 |
+
showToast('未获取到最新剧集信息,使用缓存数据', 'warning');
|
| 578 |
+
}
|
| 579 |
+
} catch (fetchError) {
|
| 580 |
+
// console.error(`获取 "${title}" 最新剧集列表失败:`, fetchError, "将使用已存储的剧集。");
|
| 581 |
+
if (fetchError.name === 'AbortError') {
|
| 582 |
+
showToast('同步剧集列表超时,使用缓存数据', 'warning');
|
| 583 |
+
} else {
|
| 584 |
+
showToast('同步剧集列表失败,使用缓存数据', 'warning');
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
} else if (historyItem) {
|
| 588 |
+
// console.log(`历史记录项 "${title}" 缺少 vod_id 或 sourceName,无法刷新剧集列表。将使用已存储的剧集。`);
|
| 589 |
+
showToast('无法同步剧集列表,使用缓存数据', 'info');
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
// 如果在历史记录中没找到,尝试使用上一个会话的集数数据
|
| 594 |
+
if (episodesList.length === 0) {
|
| 595 |
+
try {
|
| 596 |
+
const storedEpisodes = JSON.parse(localStorage.getItem('currentEpisodes') || '[]');
|
| 597 |
+
if (storedEpisodes.length > 0) {
|
| 598 |
+
episodesList = storedEpisodes;
|
| 599 |
+
// console.log(`使用localStorage中的集数数据:`, episodesList.length);
|
| 600 |
+
}
|
| 601 |
+
} catch (e) {
|
| 602 |
+
// console.error('解析currentEpisodes失败:', e);
|
| 603 |
+
}
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
// 将剧集列表保存到localStorage,播放器页面会读取它
|
| 607 |
+
if (episodesList.length > 0) {
|
| 608 |
+
localStorage.setItem('currentEpisodes', JSON.stringify(episodesList));
|
| 609 |
+
// console.log(`已将剧集列表保存到localStorage,共 ${episodesList.length} 集`);
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
// 保存当前页面URL作为返回地址
|
| 613 |
+
let currentPath;
|
| 614 |
+
if (window.location.pathname.startsWith('/player.html') || window.location.pathname.startsWith('/watch.html')) {
|
| 615 |
+
currentPath = localStorage.getItem('lastPageUrl') || '/';
|
| 616 |
+
} else {
|
| 617 |
+
currentPath = window.location.origin + window.location.pathname + window.location.search;
|
| 618 |
+
}
|
| 619 |
+
localStorage.setItem('lastPageUrl', currentPath);
|
| 620 |
+
|
| 621 |
+
// 构造播放器URL
|
| 622 |
+
let playerUrl;
|
| 623 |
+
const sourceNameForUrl = historyItem ? historyItem.sourceName : (new URLSearchParams(new URL(url, window.location.origin).search)).get('source');
|
| 624 |
+
const sourceCodeForUrl = historyItem ? historyItem.sourceCode || historyItem.sourceName : (new URLSearchParams(new URL(url, window.location.origin).search)).get('source_code');
|
| 625 |
+
const idForUrl = historyItem ? historyItem.vod_id : '';
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
if (url.includes('player.html') || url.includes('watch.html')) {
|
| 629 |
+
// console.log('检测到嵌套播放链接,解析真实URL');
|
| 630 |
+
try {
|
| 631 |
+
const nestedUrl = new URL(url, window.location.origin);
|
| 632 |
+
const nestedParams = nestedUrl.searchParams;
|
| 633 |
+
const realVideoUrl = nestedParams.get('url') || url;
|
| 634 |
+
|
| 635 |
+
playerUrl = `player.html?url=${encodeURIComponent(realVideoUrl)}&title=${encodeURIComponent(title)}&index=${episodeIndex}&position=${Math.floor(playbackPosition || 0)}&returnUrl=${encodeURIComponent(currentPath)}`;
|
| 636 |
+
if (sourceNameForUrl) playerUrl += `&source=${encodeURIComponent(sourceNameForUrl)}`;
|
| 637 |
+
if (sourceCodeForUrl) playerUrl += `&source_code=${encodeURIComponent(sourceCodeForUrl)}`;
|
| 638 |
+
if (idForUrl) playerUrl += `&id=${encodeURIComponent(idForUrl)}`;
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
} catch (e) {
|
| 642 |
+
// console.error('解析嵌套URL出错:', e);
|
| 643 |
+
playerUrl = `player.html?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&index=${episodeIndex}&position=${Math.floor(playbackPosition || 0)}&returnUrl=${encodeURIComponent(currentPath)}`;
|
| 644 |
+
if (sourceNameForUrl) playerUrl += `&source=${encodeURIComponent(sourceNameForUrl)}`;
|
| 645 |
+
if (sourceCodeForUrl) playerUrl += `&source_code=${encodeURIComponent(sourceCodeForUrl)}`;
|
| 646 |
+
if (idForUrl) playerUrl += `&id=${encodeURIComponent(idForUrl)}`;
|
| 647 |
+
}
|
| 648 |
+
} else {
|
| 649 |
+
// This case should ideally not happen if 'url' is always a player.html link from history
|
| 650 |
+
// console.warn("Playing from history with a non-player.html URL structure. This might be an issue.");
|
| 651 |
+
const playUrl = new URL(url, window.location.origin);
|
| 652 |
+
if (!playUrl.searchParams.has('index') && episodeIndex > 0) {
|
| 653 |
+
playUrl.searchParams.set('index', episodeIndex);
|
| 654 |
+
}
|
| 655 |
+
playUrl.searchParams.set('position', Math.floor(playbackPosition || 0).toString());
|
| 656 |
+
playUrl.searchParams.set('returnUrl', encodeURIComponent(currentPath));
|
| 657 |
+
if (sourceNameForUrl) playUrl.searchParams.set('source', sourceNameForUrl);
|
| 658 |
+
if (sourceCodeForUrl) playUrl.searchParams.set('source_code', sourceCodeForUrl);
|
| 659 |
+
if (idForUrl) playUrl.searchParams.set('id', idForUrl);
|
| 660 |
+
playerUrl = playUrl.toString();
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
showVideoPlayer(playerUrl);
|
| 664 |
+
} catch (e) {
|
| 665 |
+
// console.error('从历史记录播放失败:', e);
|
| 666 |
+
const simpleUrl = `player.html?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&index=${episodeIndex}`;
|
| 667 |
+
showVideoPlayer(simpleUrl);
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
// 添加观看历史 - 确保每个视频标题只有一条记录
|
| 672 |
+
// IMPORTANT: videoInfo passed to this function should include a 'showIdentifier' property
|
| 673 |
+
// (ideally `${sourceName}_${vod_id}`), 'sourceName', and 'vod_id'.
|
| 674 |
+
function addToViewingHistory(videoInfo) {
|
| 675 |
+
// 密码保护校验
|
| 676 |
+
if (window.isPasswordProtected && window.isPasswordVerified) {
|
| 677 |
+
if (window.isPasswordProtected() && !window.isPasswordVerified()) {
|
| 678 |
+
showPasswordModal && showPasswordModal();
|
| 679 |
+
return;
|
| 680 |
+
}
|
| 681 |
+
}
|
| 682 |
+
try {
|
| 683 |
+
const history = getViewingHistory();
|
| 684 |
+
|
| 685 |
+
// Ensure videoInfo has a showIdentifier
|
| 686 |
+
if (!videoInfo.showIdentifier) {
|
| 687 |
+
if (videoInfo.sourceName && videoInfo.vod_id) {
|
| 688 |
+
videoInfo.showIdentifier = `${videoInfo.sourceName}_${videoInfo.vod_id}`;
|
| 689 |
+
} else {
|
| 690 |
+
// Fallback if critical IDs are missing for the preferred identifier
|
| 691 |
+
videoInfo.showIdentifier = (videoInfo.episodes && videoInfo.episodes.length > 0) ? videoInfo.episodes[0] : videoInfo.directVideoUrl;
|
| 692 |
+
// console.warn(`addToViewingHistory: videoInfo for "${videoInfo.title}" was missing sourceName or vod_id for preferred showIdentifier. Generated fallback: ${videoInfo.showIdentifier}`);
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
const existingIndex = history.findIndex(item =>
|
| 697 |
+
item.title === videoInfo.title &&
|
| 698 |
+
item.sourceName === videoInfo.sourceName &&
|
| 699 |
+
item.showIdentifier === videoInfo.showIdentifier // Strict check using the determined showIdentifier
|
| 700 |
+
);
|
| 701 |
+
|
| 702 |
+
if (existingIndex !== -1) {
|
| 703 |
+
// Exact match with showIdentifier: Update existing series entry
|
| 704 |
+
const existingItem = history[existingIndex];
|
| 705 |
+
existingItem.episodeIndex = videoInfo.episodeIndex;
|
| 706 |
+
existingItem.timestamp = Date.now();
|
| 707 |
+
existingItem.sourceName = videoInfo.sourceName || existingItem.sourceName;
|
| 708 |
+
existingItem.sourceCode = videoInfo.sourceCode || existingItem.sourceCode;
|
| 709 |
+
existingItem.vod_id = videoInfo.vod_id || existingItem.vod_id;
|
| 710 |
+
existingItem.directVideoUrl = videoInfo.directVideoUrl || existingItem.directVideoUrl;
|
| 711 |
+
existingItem.url = videoInfo.url || existingItem.url;
|
| 712 |
+
existingItem.playbackPosition = videoInfo.playbackPosition > 10 ? videoInfo.playbackPosition : (existingItem.playbackPosition || 0);
|
| 713 |
+
existingItem.duration = videoInfo.duration || existingItem.duration;
|
| 714 |
+
|
| 715 |
+
if (videoInfo.episodes && Array.isArray(videoInfo.episodes) && videoInfo.episodes.length > 0) {
|
| 716 |
+
if (!existingItem.episodes ||
|
| 717 |
+
!Array.isArray(existingItem.episodes) ||
|
| 718 |
+
existingItem.episodes.length !== videoInfo.episodes.length ||
|
| 719 |
+
!videoInfo.episodes.every((ep, i) => ep === existingItem.episodes[i])) {
|
| 720 |
+
existingItem.episodes = [...videoInfo.episodes];
|
| 721 |
+
// console.log(`更新 (addToViewingHistory) "${videoInfo.title}" 的剧集数据: ${videoInfo.episodes.length}集`);
|
| 722 |
+
}
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
history.splice(existingIndex, 1);
|
| 726 |
+
history.unshift(existingItem);
|
| 727 |
+
// console.log(`更新历史记录 (addToViewingHistory): "${videoInfo.title}", 第 ${videoInfo.episodeIndex !== undefined ? videoInfo.episodeIndex + 1 : 'N/A'} 集`);
|
| 728 |
+
} else {
|
| 729 |
+
// No exact match: Add as a new entry
|
| 730 |
+
const newItem = {
|
| 731 |
+
...videoInfo, // Includes the showIdentifier we ensured is present
|
| 732 |
+
timestamp: Date.now()
|
| 733 |
+
};
|
| 734 |
+
|
| 735 |
+
if (videoInfo.episodes && Array.isArray(videoInfo.episodes)) {
|
| 736 |
+
newItem.episodes = [...videoInfo.episodes];
|
| 737 |
+
} else {
|
| 738 |
+
newItem.episodes = [];
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
history.unshift(newItem);
|
| 742 |
+
// console.log(`创建新的历史记录 (addToViewingHistory): "${videoInfo.title}", Episode: ${videoInfo.episodeIndex !== undefined ? videoInfo.episodeIndex + 1 : 'N/A'}`);
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
// 限制历史记录数量为50条
|
| 746 |
+
const maxHistoryItems = 50;
|
| 747 |
+
if (history.length > maxHistoryItems) {
|
| 748 |
+
history.splice(maxHistoryItems);
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
// 保存到本地存储
|
| 752 |
+
localStorage.setItem('viewingHistory', JSON.stringify(history));
|
| 753 |
+
} catch (e) {
|
| 754 |
+
// console.error('保存观看历史失败:', e);
|
| 755 |
+
}
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
// 清空观看历史
|
| 759 |
+
function clearViewingHistory() {
|
| 760 |
+
try {
|
| 761 |
+
localStorage.removeItem('viewingHistory');
|
| 762 |
+
loadViewingHistory(); // 重新加载空的历史记录
|
| 763 |
+
showToast('观看历史已清空', 'success');
|
| 764 |
+
} catch (e) {
|
| 765 |
+
// console.error('清除观看历史失败:', e);
|
| 766 |
+
showToast('清除观看历史失败', 'error');
|
| 767 |
+
}
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
// 更新toggleSettings函数以处理历史面板互动
|
| 771 |
+
const originalToggleSettings = toggleSettings;
|
| 772 |
+
toggleSettings = function(e) {
|
| 773 |
+
if (e) e.stopPropagation();
|
| 774 |
+
|
| 775 |
+
// 原始设置面板切换逻辑
|
| 776 |
+
originalToggleSettings(e);
|
| 777 |
+
|
| 778 |
+
// 如果历史记录面板是打开的,则关闭它
|
| 779 |
+
const historyPanel = document.getElementById('historyPanel');
|
| 780 |
+
if (historyPanel && historyPanel.classList.contains('show')) {
|
| 781 |
+
historyPanel.classList.remove('show');
|
| 782 |
+
}
|
| 783 |
+
};
|
| 784 |
+
|
| 785 |
+
// 点击外部关闭历史面板
|
| 786 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 787 |
+
document.addEventListener('click', function(e) {
|
| 788 |
+
const historyPanel = document.getElementById('historyPanel');
|
| 789 |
+
const historyButton = document.querySelector('button[onclick="toggleHistory(event)"]');
|
| 790 |
+
|
| 791 |
+
if (historyPanel && historyButton &&
|
| 792 |
+
!historyPanel.contains(e.target) &&
|
| 793 |
+
!historyButton.contains(e.target) &&
|
| 794 |
+
historyPanel.classList.contains('show')) {
|
| 795 |
+
historyPanel.classList.remove('show');
|
| 796 |
+
}
|
| 797 |
+
});
|
| 798 |
+
});
|
| 799 |
+
|
| 800 |
+
// 清除本地存储缓存并刷新页面
|
| 801 |
+
function clearLocalStorage() {
|
| 802 |
+
// 确保模态框在页面上只有一个实例
|
| 803 |
+
let modal = document.getElementById('messageBoxModal');
|
| 804 |
+
if (modal) {
|
| 805 |
+
document.body.removeChild(modal);
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
// 创建模态框元素
|
| 809 |
+
modal = document.createElement('div');
|
| 810 |
+
modal.id = 'messageBoxModal';
|
| 811 |
+
modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40';
|
| 812 |
+
|
| 813 |
+
modal.innerHTML = `
|
| 814 |
+
<div class="bg-[#191919] rounded-lg p-6 max-w-md w-full max-h-[90vh] overflow-y-auto relative">
|
| 815 |
+
<button id="closeBoxModal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-xl">×</button>
|
| 816 |
+
|
| 817 |
+
<h3 class="text-xl font-bold text-red-500 mb-4">警告</h3>
|
| 818 |
+
|
| 819 |
+
<div class="mb-0">
|
| 820 |
+
<div class="text-sm font-medium text-gray-300">确定要清除页面缓存吗?</div>
|
| 821 |
+
<div class="text-sm font-medium text-gray-300 mb-4">此功能会删除你的观看记录、自定义 API 接口和 Cookie,<scan class="text-red-500 font-bold">此操作不可恢复!</scan></div>
|
| 822 |
+
<div class="flex justify-end space-x-2">
|
| 823 |
+
<button id="confirmBoxModal" class="ml-2 bg-gray-600 hover:bg-gray-700 text-white px-4 py-1 rounded">确定</button>
|
| 824 |
+
<button id="cancelBoxModal" class="ml-2 bg-pink-600 hover:bg-pink-700 text-white px-4 py-1 rounded">取消</button>
|
| 825 |
+
</div>
|
| 826 |
+
</div>
|
| 827 |
+
</div>`;
|
| 828 |
+
|
| 829 |
+
// 添加模态框到页面
|
| 830 |
+
document.body.appendChild(modal);
|
| 831 |
+
|
| 832 |
+
// 添加事件监听器 - 关闭按钮
|
| 833 |
+
document.getElementById('closeBoxModal').addEventListener('click', function () {
|
| 834 |
+
document.body.removeChild(modal);
|
| 835 |
+
});
|
| 836 |
+
|
| 837 |
+
// 添加事件监听器 - 确定按钮
|
| 838 |
+
document.getElementById('confirmBoxModal').addEventListener('click', function () {
|
| 839 |
+
// 清除所有localStorage数据
|
| 840 |
+
localStorage.clear();
|
| 841 |
+
|
| 842 |
+
// 清除所有cookie
|
| 843 |
+
const cookies = document.cookie.split(";");
|
| 844 |
+
for (let i = 0; i < cookies.length; i++) {
|
| 845 |
+
const cookie = cookies[i];
|
| 846 |
+
const eqPos = cookie.indexOf("=");
|
| 847 |
+
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
|
| 848 |
+
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
modal.innerHTML = `
|
| 852 |
+
<div class="bg-[#191919] rounded-lg p-6 max-w-md w-full max-h-[90vh] overflow-y-auto relative">
|
| 853 |
+
<button id="closeBoxModal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-xl">×</button>
|
| 854 |
+
|
| 855 |
+
<h3 class="text-xl font-bold text-white mb-4">提示</h3>
|
| 856 |
+
|
| 857 |
+
<div class="mb-4">
|
| 858 |
+
<div class="text-sm font-medium text-gray-300 mb-4">页面缓存和Cookie已清除,<span id="countdown">3</span> 秒后自动刷新本页面。</div>
|
| 859 |
+
</div>
|
| 860 |
+
</div>`;
|
| 861 |
+
|
| 862 |
+
let countdown = 3;
|
| 863 |
+
const countdownElement = document.getElementById('countdown');
|
| 864 |
+
|
| 865 |
+
const countdownInterval = setInterval(() => {
|
| 866 |
+
countdown--;
|
| 867 |
+
if (countdown >= 0) {
|
| 868 |
+
countdownElement.textContent = countdown;
|
| 869 |
+
} else {
|
| 870 |
+
clearInterval(countdownInterval);
|
| 871 |
+
window.location.reload();
|
| 872 |
+
}
|
| 873 |
+
}, 1000);
|
| 874 |
+
});
|
| 875 |
+
|
| 876 |
+
// 添加事件监听器 - 取消按钮
|
| 877 |
+
document.getElementById('cancelBoxModal').addEventListener('click', function () {
|
| 878 |
+
document.body.removeChild(modal);
|
| 879 |
+
});
|
| 880 |
+
|
| 881 |
+
// 添加事件监听器 - 点击模态框外部关闭
|
| 882 |
+
modal.addEventListener('click', function (e) {
|
| 883 |
+
if (e.target === modal) {
|
| 884 |
+
document.body.removeChild(modal);
|
| 885 |
+
}
|
| 886 |
+
});
|
| 887 |
+
}
|
| 888 |
+
|
| 889 |
+
// 显示配置文件导入页面
|
| 890 |
+
function showImportBox(fun) {
|
| 891 |
+
// 确保模态框在页面上只有一个实例
|
| 892 |
+
let modal = document.getElementById('showImportBoxModal');
|
| 893 |
+
if (modal) {
|
| 894 |
+
document.body.removeChild(modal);
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
// 创建模态框元素
|
| 898 |
+
modal = document.createElement('div');
|
| 899 |
+
modal.id = 'showImportBoxModal';
|
| 900 |
+
modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40';
|
| 901 |
+
|
| 902 |
+
modal.innerHTML = `
|
| 903 |
+
<div class="bg-[#191919] rounded-lg p-6 max-w-md w-full max-h-[90vh] overflow-y-auto relative">
|
| 904 |
+
<button id="closeBoxModal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-xl">×</button>
|
| 905 |
+
|
| 906 |
+
<div class="m-4">
|
| 907 |
+
<div id="dropZone" class="w-full py-9 bg-[#111] rounded-2xl border border-gray-300 gap-3 grid border-dashed">
|
| 908 |
+
<div class="grid gap-1">
|
| 909 |
+
<svg class="mx-auto" width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 910 |
+
<g id="File">
|
| 911 |
+
<path id="icon" d="M31.6497 10.6056L32.2476 10.0741L31.6497 10.6056ZM28.6559 7.23757L28.058 7.76907L28.058 7.76907L28.6559 7.23757ZM26.5356 5.29253L26.2079 6.02233L26.2079 6.02233L26.5356 5.29253ZM33.1161 12.5827L32.3683 12.867V12.867L33.1161 12.5827ZM31.8692 33.5355L32.4349 34.1012L31.8692 33.5355ZM24.231 11.4836L25.0157 11.3276L24.231 11.4836ZM26.85 14.1026L26.694 14.8872L26.85 14.1026ZM11.667 20.8667C11.2252 20.8667 10.867 21.2248 10.867 21.6667C10.867 22.1085 11.2252 22.4667 11.667 22.4667V20.8667ZM25.0003 22.4667C25.4422 22.4667 25.8003 22.1085 25.8003 21.6667C25.8003 21.2248 25.4422 20.8667 25.0003 20.8667V22.4667ZM11.667 25.8667C11.2252 25.8667 10.867 26.2248 10.867 26.6667C10.867 27.1085 11.2252 27.4667 11.667 27.4667V25.8667ZM20.0003 27.4667C20.4422 27.4667 20.8003 27.1085 20.8003 26.6667C20.8003 26.2248 20.4422 25.8667 20.0003 25.8667V27.4667ZM23.3337 34.2H16.667V35.8H23.3337V34.2ZM7.46699 25V15H5.86699V25H7.46699ZM32.5337 15.0347V25H34.1337V15.0347H32.5337ZM16.667 5.8H23.6732V4.2H16.667V5.8ZM23.6732 5.8C25.2185 5.8 25.7493 5.81639 26.2079 6.02233L26.8633 4.56274C26.0191 4.18361 25.0759 4.2 23.6732 4.2V5.8ZM29.2539 6.70608C28.322 5.65771 27.7076 4.94187 26.8633 4.56274L26.2079 6.02233C26.6665 6.22826 27.0314 6.6141 28.058 7.76907L29.2539 6.70608ZM34.1337 15.0347C34.1337 13.8411 34.1458 13.0399 33.8638 12.2984L32.3683 12.867C32.5216 13.2702 32.5337 13.7221 32.5337 15.0347H34.1337ZM31.0518 11.1371C31.9238 12.1181 32.215 12.4639 32.3683 12.867L33.8638 12.2984C33.5819 11.5569 33.0406 10.9662 32.2476 10.0741L31.0518 11.1371ZM16.667 34.2C14.2874 34.2 12.5831 34.1983 11.2872 34.0241C10.0144 33.8529 9.25596 33.5287 8.69714 32.9698L7.56577 34.1012C8.47142 35.0069 9.62375 35.4148 11.074 35.6098C12.5013 35.8017 14.3326 35.8 16.667 35.8V34.2ZM5.86699 25C5.86699 27.3344 5.86529 29.1657 6.05718 30.593C6.25217 32.0432 6.66012 33.1956 7.56577 34.1012L8.69714 32.9698C8.13833 32.411 7.81405 31.6526 7.64292 30.3798C7.46869 29.0839 7.46699 27.3796 7.46699 25H5.86699ZM23.3337 35.8C25.6681 35.8 27.4993 35.8017 28.9266 35.6098C30.3769 35.4148 31.5292 35.0069 32.4349 34.1012L31.3035 32.9698C30.7447 33.5287 29.9863 33.8529 28.7134 34.0241C27.4175 34.1983 25.7133 34.2 23.3337 34.2V35.8ZM32.5337 25C32.5337 27.3796 32.532 29.0839 32.3577 30.3798C32.1866 31.6526 31.8623 32.411 31.3035 32.9698L32.4349 34.1012C33.3405 33.1956 33.7485 32.0432 33.9435 30.593C34.1354 29.1657 34.1337 27.3344 34.1337 25H32.5337ZM7.46699 15C7.46699 12.6204 7.46869 10.9161 7.64292 9.62024C7.81405 8.34738 8.13833 7.58897 8.69714 7.03015L7.56577 5.89878C6.66012 6.80443 6.25217 7.95676 6.05718 9.40704C5.86529 10.8343 5.86699 12.6656 5.86699 15H7.46699ZM16.667 4.2C14.3326 4.2 12.5013 4.1983 11.074 4.39019C9.62375 4.58518 8.47142 4.99313 7.56577 5.89878L8.69714 7.03015C9.25596 6.47133 10.0144 6.14706 11.2872 5.97592C12.5831 5.8017 14.2874 5.8 16.667 5.8V4.2ZM23.367 5V10H24.967V5H23.367ZM28.3337 14.9667H33.3337V13.3667H28.3337V14.9667ZM23.367 10C23.367 10.7361 23.3631 11.221 23.4464 11.6397L25.0157 11.3276C24.9709 11.1023 24.967 10.8128 24.967 10H23.367ZM28.3337 13.3667C27.5209 13.3667 27.2313 13.3628 27.0061 13.318L26.694 14.8872C27.1127 14.9705 27.5976 14.9667 28.3337 14.9667V13.3667ZM23.4464 11.6397C23.7726 13.2794 25.0543 14.5611 26.694 14.8872L27.0061 13.318C26.0011 13.1181 25.2156 12.3325 25.0157 11.3276L23.4464 11.6397ZM11.667 22.4667H25.0003V20.8667H11.667V22.4667ZM11.667 27.4667H20.0003V25.8667H11.667V27.4667ZM32.2476 10.0741L29.2539 6.70608L28.058 7.76907L31.0518 11.1371L32.2476 10.0741Z" fill="#DB2777" />
|
| 912 |
+
</g>
|
| 913 |
+
</svg>
|
| 914 |
+
</div>
|
| 915 |
+
<div class="grid gap-2">
|
| 916 |
+
<h4 class="text-center text-white-900 text-sm font-medium leading-snug">将配置文件拖到此处,或手动选择文件</h4>
|
| 917 |
+
<div class="flex items-center justify-center gap-2">
|
| 918 |
+
<label>
|
| 919 |
+
<input type="file" id="ChooseFile" hidden />
|
| 920 |
+
<div class="flex w-28 h-9 px-2 flex-col bg-pink-600 rounded-full shadow text-white text-xs font-semibold leading-4 items-center justify-center cursor-pointer focus:outline-none">选择文件</div>
|
| 921 |
+
</label>
|
| 922 |
+
<button onclick="importConfigFromUrl()" class="flex w-28 h-9 px-2 flex-col bg-blue-600 rounded-full shadow text-white text-xs font-semibold leading-4 items-center justify-center cursor-pointer focus:outline-none">从URL导入</button>
|
| 923 |
+
</div>
|
| 924 |
+
</div>
|
| 925 |
+
</div>
|
| 926 |
+
</div>
|
| 927 |
+
</div>`;
|
| 928 |
+
|
| 929 |
+
// 添加模态框到页面
|
| 930 |
+
document.body.appendChild(modal);
|
| 931 |
+
|
| 932 |
+
// 添加事件监听器 - 关闭按钮
|
| 933 |
+
document.getElementById('closeBoxModal').addEventListener('click', function () {
|
| 934 |
+
document.body.removeChild(modal);
|
| 935 |
+
});
|
| 936 |
+
|
| 937 |
+
// 添加事件监听器 - 点击模态框外部关闭
|
| 938 |
+
modal.addEventListener('click', function (e) {
|
| 939 |
+
if (e.target === modal) {
|
| 940 |
+
document.body.removeChild(modal);
|
| 941 |
+
}
|
| 942 |
+
});
|
| 943 |
+
|
| 944 |
+
// 添加事件监听器 - 拖拽文件
|
| 945 |
+
const dropZone = document.getElementById('dropZone');
|
| 946 |
+
const fileInput = document.getElementById('ChooseFile');
|
| 947 |
+
|
| 948 |
+
dropZone.addEventListener('dragover', (e) => {
|
| 949 |
+
e.preventDefault();
|
| 950 |
+
dropZone.classList.add('border-blue-500');
|
| 951 |
+
});
|
| 952 |
+
|
| 953 |
+
dropZone.addEventListener('dragleave', () => {
|
| 954 |
+
dropZone.classList.remove('border-blue-500');
|
| 955 |
+
});
|
| 956 |
+
|
| 957 |
+
dropZone.addEventListener('drop', (e) => {
|
| 958 |
+
e.preventDefault();
|
| 959 |
+
fun(e.dataTransfer.files[0]);
|
| 960 |
+
});
|
| 961 |
+
|
| 962 |
+
fileInput.addEventListener('change', (e) => {
|
| 963 |
+
fun(fileInput.files[0]);
|
| 964 |
+
});
|
| 965 |
+
}
|
js/version-check.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 添加动画样式
|
| 2 |
+
(function() {
|
| 3 |
+
const style = document.createElement('style');
|
| 4 |
+
style.textContent = `
|
| 5 |
+
@keyframes pulse {
|
| 6 |
+
0%, 100% {
|
| 7 |
+
opacity: 1;
|
| 8 |
+
}
|
| 9 |
+
50% {
|
| 10 |
+
opacity: 0.6;
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
.animate-pulse {
|
| 14 |
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 15 |
+
}
|
| 16 |
+
`;
|
| 17 |
+
document.head.appendChild(style);
|
| 18 |
+
})();
|
| 19 |
+
|
| 20 |
+
// 获取版本信息
|
| 21 |
+
async function fetchVersion(url, errorMessage, options = {}) {
|
| 22 |
+
const response = await fetch(url, options);
|
| 23 |
+
if (!response.ok) {
|
| 24 |
+
throw new Error(errorMessage);
|
| 25 |
+
}
|
| 26 |
+
return await response.text();
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// 版本检查函数
|
| 30 |
+
async function checkForUpdates() {
|
| 31 |
+
try {
|
| 32 |
+
// 获取当前版本
|
| 33 |
+
const currentVersion = await fetchVersion('/VERSION.txt', '获取当前版本失败', {
|
| 34 |
+
cache: 'no-store'
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
// 获取最新版本
|
| 38 |
+
let latestVersion;
|
| 39 |
+
const VERSION_URL = {
|
| 40 |
+
PROXY: 'https://ghfast.top/raw.githubusercontent.com/LibreSpark/LibreTV/main/VERSION.txt',
|
| 41 |
+
DIRECT: 'https://raw.githubusercontent.com/LibreSpark/LibreTV/main/VERSION.txt'
|
| 42 |
+
};
|
| 43 |
+
const FETCH_TIMEOUT = 1500;
|
| 44 |
+
|
| 45 |
+
try {
|
| 46 |
+
// 尝试使用代理URL获取最新版本
|
| 47 |
+
const proxyPromise = fetchVersion(VERSION_URL.PROXY, '代理请求失败');
|
| 48 |
+
const timeoutPromise = new Promise((_, reject) =>
|
| 49 |
+
setTimeout(() => reject(new Error('代理请求超时')), FETCH_TIMEOUT)
|
| 50 |
+
);
|
| 51 |
+
|
| 52 |
+
latestVersion = await Promise.race([proxyPromise, timeoutPromise]);
|
| 53 |
+
console.log('通过代理服务器获取版本成功');
|
| 54 |
+
} catch (error) {
|
| 55 |
+
console.log('代理请求失败,尝试直接请求:', error.message);
|
| 56 |
+
try {
|
| 57 |
+
// 代理失败后尝试直接获取
|
| 58 |
+
latestVersion = await fetchVersion(VERSION_URL.DIRECT, '获取最新版本失败');
|
| 59 |
+
console.log('直接请求获取版本成功');
|
| 60 |
+
} catch (directError) {
|
| 61 |
+
console.error('所有版本检查请求均失败:', directError);
|
| 62 |
+
throw new Error('无法获取最新版本信息');
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
console.log('当前版本:', currentVersion);
|
| 67 |
+
console.log('最新版本:', latestVersion);
|
| 68 |
+
|
| 69 |
+
// 清理版本字符串(移除可能的空格或换行符)
|
| 70 |
+
const cleanCurrentVersion = currentVersion.trim();
|
| 71 |
+
const cleanLatestVersion = latestVersion.trim();
|
| 72 |
+
|
| 73 |
+
// 返回版本信息
|
| 74 |
+
return {
|
| 75 |
+
current: cleanCurrentVersion,
|
| 76 |
+
latest: cleanLatestVersion,
|
| 77 |
+
hasUpdate: parseInt(cleanLatestVersion) > parseInt(cleanCurrentVersion),
|
| 78 |
+
currentFormatted: formatVersion(cleanCurrentVersion),
|
| 79 |
+
latestFormatted: formatVersion(cleanLatestVersion)
|
| 80 |
+
};
|
| 81 |
+
} catch (error) {
|
| 82 |
+
console.error('版本检测出错:', error);
|
| 83 |
+
throw error;
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// 格式化版本号为可读形式 (yyyyMMddhhmm -> yyyy-MM-dd hh:mm)
|
| 88 |
+
function formatVersion(versionString) {
|
| 89 |
+
// 检测版本字符串是否有效
|
| 90 |
+
if (!versionString) {
|
| 91 |
+
return '未知版本';
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// 清理版本字符串(移除可能的空格或换行符)
|
| 95 |
+
const cleanedString = versionString.trim();
|
| 96 |
+
|
| 97 |
+
// 格式化标准12位版本号
|
| 98 |
+
if (cleanedString.length === 12) {
|
| 99 |
+
const year = cleanedString.substring(0, 4);
|
| 100 |
+
const month = cleanedString.substring(4, 6);
|
| 101 |
+
const day = cleanedString.substring(6, 8);
|
| 102 |
+
const hour = cleanedString.substring(8, 10);
|
| 103 |
+
const minute = cleanedString.substring(10, 12);
|
| 104 |
+
|
| 105 |
+
return `${year}-${month}-${day} ${hour}:${minute}`;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return cleanedString;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// 创建错误版本信息元素
|
| 112 |
+
function createErrorVersionElement(errorMessage) {
|
| 113 |
+
const errorElement = document.createElement('p');
|
| 114 |
+
errorElement.className = 'text-gray-500 text-sm mt-1 text-center md:text-left';
|
| 115 |
+
errorElement.innerHTML = `版本: <span class="text-amber-500">检测失败</span>`;
|
| 116 |
+
errorElement.title = errorMessage;
|
| 117 |
+
return errorElement;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// 添加版本信息到页脚
|
| 121 |
+
function addVersionInfoToFooter() {
|
| 122 |
+
checkForUpdates().then(result => {
|
| 123 |
+
if (!result) {
|
| 124 |
+
// 如果版本检测失败,显示错误信息
|
| 125 |
+
const versionElement = createErrorVersionElement();
|
| 126 |
+
// 在页脚显示错误元素
|
| 127 |
+
displayVersionElement(versionElement);
|
| 128 |
+
return;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// 创建版本信息元素
|
| 132 |
+
const versionElement = document.createElement('p');
|
| 133 |
+
versionElement.className = 'text-gray-500 text-sm mt-1 text-center md:text-left';
|
| 134 |
+
|
| 135 |
+
// 添加当前版本信息
|
| 136 |
+
versionElement.innerHTML = `版本: ${result.currentFormatted}`;
|
| 137 |
+
|
| 138 |
+
// 如果���更新,添加更新提示
|
| 139 |
+
if (result.hasUpdate) {
|
| 140 |
+
versionElement.innerHTML += ` <span class="inline-flex items-center bg-red-600 text-white text-xs px-2 py-0.5 rounded-md ml-1 cursor-pointer animate-pulse font-medium">
|
| 141 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 142 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
| 143 |
+
</svg>
|
| 144 |
+
发现新版
|
| 145 |
+
</span>`;
|
| 146 |
+
|
| 147 |
+
setTimeout(() => {
|
| 148 |
+
const updateBtn = versionElement.querySelector('span');
|
| 149 |
+
if (updateBtn) {
|
| 150 |
+
updateBtn.addEventListener('click', () => {
|
| 151 |
+
window.open('https://github.com/LibreSpark/LibreTV', '_blank');
|
| 152 |
+
});
|
| 153 |
+
}
|
| 154 |
+
}, 100);
|
| 155 |
+
} else {
|
| 156 |
+
// 如果没有更新,显示当前版本为最新版本
|
| 157 |
+
versionElement.innerHTML = `版本: ${result.currentFormatted} <span class="text-green-500">(最新版本)</span>`;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// 显示版本元素
|
| 161 |
+
displayVersionElement(versionElement);
|
| 162 |
+
}).catch(error => {
|
| 163 |
+
console.error('版本检测出错:', error);
|
| 164 |
+
// 创建错误版本信息元素并显示
|
| 165 |
+
const errorElement = createErrorVersionElement(`错误信息: ${error.message}`);
|
| 166 |
+
displayVersionElement(errorElement);
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// 在页脚显示版本元素的辅助函数
|
| 171 |
+
function displayVersionElement(element) {
|
| 172 |
+
// 获取页脚元素
|
| 173 |
+
const footerElement = document.querySelector('.footer p.text-gray-500.text-sm');
|
| 174 |
+
if (footerElement) {
|
| 175 |
+
// 在原版权信息后插入版本信息
|
| 176 |
+
footerElement.insertAdjacentElement('afterend', element);
|
| 177 |
+
} else {
|
| 178 |
+
// 如果找不到页脚元素,尝试在页脚区域最后添加
|
| 179 |
+
const footer = document.querySelector('.footer .container');
|
| 180 |
+
if (footer) {
|
| 181 |
+
footer.querySelector('div').appendChild(element);
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// 页面加载完成后添加版本信息
|
| 187 |
+
document.addEventListener('DOMContentLoaded', addVersionInfoToFooter);
|
js/watch.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 获取当前URL的参数,并将它们传递给player.html
|
| 2 |
+
window.onload = function() {
|
| 3 |
+
// 获取当前URL的查询参数
|
| 4 |
+
const currentParams = new URLSearchParams(window.location.search);
|
| 5 |
+
|
| 6 |
+
// 创建player.html的URL对象
|
| 7 |
+
const playerUrlObj = new URL("player.html", window.location.origin);
|
| 8 |
+
|
| 9 |
+
// 更新状态文本
|
| 10 |
+
const statusElement = document.getElementById('redirect-status');
|
| 11 |
+
const manualRedirect = document.getElementById('manual-redirect');
|
| 12 |
+
let statusMessages = [
|
| 13 |
+
"准备视频数据中...",
|
| 14 |
+
"正在加载视频信息...",
|
| 15 |
+
"即将开始播放...",
|
| 16 |
+
];
|
| 17 |
+
let currentStatus = 0;
|
| 18 |
+
|
| 19 |
+
// 状态文本动画
|
| 20 |
+
let statusInterval = setInterval(() => {
|
| 21 |
+
if (currentStatus >= statusMessages.length) {
|
| 22 |
+
currentStatus = 0;
|
| 23 |
+
}
|
| 24 |
+
if (statusElement) {
|
| 25 |
+
statusElement.textContent = statusMessages[currentStatus];
|
| 26 |
+
statusElement.style.opacity = 0.7;
|
| 27 |
+
setTimeout(() => {
|
| 28 |
+
if (statusElement) statusElement.style.opacity = 1;
|
| 29 |
+
}, 300);
|
| 30 |
+
}
|
| 31 |
+
currentStatus++;
|
| 32 |
+
}, 1000);
|
| 33 |
+
|
| 34 |
+
// 确保保留所有原始参数
|
| 35 |
+
currentParams.forEach((value, key) => {
|
| 36 |
+
playerUrlObj.searchParams.set(key, value);
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// 获取来源URL (如果存在)
|
| 40 |
+
const referrer = document.referrer;
|
| 41 |
+
|
| 42 |
+
// 获取当前URL中的返回URL参数(如果有)
|
| 43 |
+
const backUrl = currentParams.get('back');
|
| 44 |
+
|
| 45 |
+
// 确定返回URL的优先级:1. 指定的back参数 2. referrer 3. 搜索页面
|
| 46 |
+
let returnUrl = '';
|
| 47 |
+
if (backUrl) {
|
| 48 |
+
// 有显式指定的返回URL
|
| 49 |
+
returnUrl = decodeURIComponent(backUrl);
|
| 50 |
+
} else if (referrer && (referrer.includes('/s=') || referrer.includes('?s='))) {
|
| 51 |
+
// 来源是搜索页面
|
| 52 |
+
returnUrl = referrer;
|
| 53 |
+
} else if (referrer && referrer.trim() !== '') {
|
| 54 |
+
// 如果有referrer但不是搜索页,也使用它
|
| 55 |
+
returnUrl = referrer;
|
| 56 |
+
} else {
|
| 57 |
+
// 默认回到首页
|
| 58 |
+
returnUrl = '/';
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// 将返回URL添加到player.html的参数中
|
| 62 |
+
if (!playerUrlObj.searchParams.has('returnUrl')) {
|
| 63 |
+
playerUrlObj.searchParams.set('returnUrl', encodeURIComponent(returnUrl));
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// 同时保存在localStorage中,作为备用
|
| 67 |
+
localStorage.setItem('lastPageUrl', returnUrl);
|
| 68 |
+
|
| 69 |
+
// 标记来自搜索页面
|
| 70 |
+
if (returnUrl.includes('/s=') || returnUrl.includes('?s=')) {
|
| 71 |
+
localStorage.setItem('cameFromSearch', 'true');
|
| 72 |
+
localStorage.setItem('searchPageUrl', returnUrl);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// 获取最终的URL字符串
|
| 76 |
+
const finalPlayerUrl = playerUrlObj.toString();
|
| 77 |
+
|
| 78 |
+
// 更新手动重定向链接
|
| 79 |
+
if (manualRedirect) {
|
| 80 |
+
manualRedirect.href = finalPlayerUrl;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// 更新meta refresh标签
|
| 84 |
+
const metaRefresh = document.querySelector('meta[http-equiv="refresh"]');
|
| 85 |
+
if (metaRefresh) {
|
| 86 |
+
metaRefresh.content = `3; url=${finalPlayerUrl}`;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// 重定向到播放器页面
|
| 90 |
+
setTimeout(() => {
|
| 91 |
+
clearInterval(statusInterval);
|
| 92 |
+
window.location.href = finalPlayerUrl;
|
| 93 |
+
}, 2800); // 稍微早于meta refresh的时间,确保我们的JS控制重定向
|
| 94 |
+
};
|