diff --git a/.env.example b/.env.example index f6bb4b8..5c9e08c 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # 基本配置 PORT=8080 PASSWORD= +ADMINPASSWORD= DEBUG=false # CORS 配置 diff --git a/.gitignore b/.gitignore index fb27832..1a1ffac 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ coverage *.njsproj *.sln *.sw? +.env diff --git a/README.md b/README.md index 2feadb0..7110b0d 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,8 @@ Pull Bot 会反复触发无效的 PR 和垃圾邮件,严重干扰项目维护 - 构建命令:留空(无需构建) - 输出目录:留空(默认为根目录) 5. **⚠️ 重要:在"设置" > "环境变量"中添加 `PASSWORD` 变量** -6. 点击"保存并部署" +6. **可选:在"Settings" > "Environment Variables"中添加 `ADMINPASSWORD` 变量** +7. 点击"保存并部署" ### Vercel @@ -83,8 +84,9 @@ Pull Bot 会反复触发无效的 PR 和垃圾邮件,严重干扰项目维护 2. 登录 [Vercel](https://vercel.com/),点击"New Project" 3. 导入您的仓库,使用默认设置 4. **⚠️ 重要:在"Settings" > "Environment Variables"中添加 `PASSWORD` 变量** -5. 点击"Deploy" -6. 可选:在"Settings" > "Environment Variables"中配置密码保护 +5. **可选:在"Settings" > "Environment Variables"中添加 `ADMINPASSWORD` 变量** +6. 点击"Deploy" +7. 可选:在"Settings" > "Environment Variables"中配置密码保护和设置按钮密码保护 ### Docker ``` @@ -93,6 +95,7 @@ docker run -d \ --restart unless-stopped \ -p 8899:8080 \ -e PASSWORD=your_password \ + -e ADMINPASSWORD=your_adminpassword \ bestzwei/libretv:latest ``` @@ -109,6 +112,7 @@ services: - "8899:8080" # 将内部 8080 端口映射到主机的 8899 端口 environment: - PASSWORD=${PASSWORD:-your_password} # 可将 your_password 修改为你想要的密码,默认为 your_password + - ADMINPASSWORD=${PASSWORD:-your_adminpassword} # 可将 your_adminpassword 修改为你想要的密码,默认为 your_adminpassword restart: unless-stopped ``` 启动 LibreTV: @@ -146,6 +150,9 @@ npm run dev **环境变量名**: `PASSWORD` **值**: 您想设置的密码 +**环境变量名**: `ADMINPASSWORD` +**值**: 您想设置的密码 + 各平台设置方法: - **Cloudflare Pages**: Dashboard > 您的项目 > 设置 > 环境变量 diff --git a/functions/_middleware.js b/functions/_middleware.js index 71604f9..ec21453 100644 --- a/functions/_middleware.js +++ b/functions/_middleware.js @@ -1,30 +1,31 @@ -import { sha256 } from '../js/sha256.js'; // 需新建或引入SHA-256实现 +import { sha256 } from '../js/sha256.js'; -// Cloudflare Pages Middleware to inject environment variables export async function onRequest(context) { const { request, env, next } = context; - - // Proceed to the next middleware or route handler const response = await next(); - - // Check if the response is HTML const contentType = response.headers.get("content-type") || ""; if (contentType.includes("text/html")) { - // Get the original HTML content let html = await response.text(); - // Replace the placeholder with actual environment variable value - // If PASSWORD is not set, replace with empty string + // 处理普通密码 const password = env.PASSWORD || ""; let passwordHash = ""; if (password) { passwordHash = await sha256(password); } html = html.replace('window.__ENV__.PASSWORD = "{{PASSWORD}}";', - `window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash`); + `window.__ENV__.PASSWORD = "${passwordHash}";`); + + // 处理管理员密码 - 确保这部分代码被执行 + const adminPassword = env.ADMINPASSWORD || ""; + let adminPasswordHash = ""; + if (adminPassword) { + adminPasswordHash = await sha256(adminPassword); + } + html = html.replace('window.__ENV__.ADMINPASSWORD = "{{ADMINPASSWORD}}";', + `window.__ENV__.ADMINPASSWORD = "${adminPasswordHash}";`); - // Create a new response with the modified HTML return new Response(html, { headers: response.headers, status: response.status, @@ -32,6 +33,5 @@ export async function onRequest(context) { }); } - // Return the original response for non-HTML content return response; } \ No newline at end of file diff --git a/index.html b/index.html index c8f413a..1f6207a 100644 --- a/index.html +++ b/index.html @@ -428,6 +428,7 @@ // 注入服务器端环境变量 (将由服务器端替换) // PASSWORD 变量将在这里被服务器端注入 window.__ENV__.PASSWORD = "{{PASSWORD}}"; + window.__ENV__.ADMINPASSWORD = "{{ADMINPASSWORD}}"; diff --git a/js/app.js b/js/app.js index 868c2af..c49cebe 100644 --- a/js/app.js +++ b/js/app.js @@ -12,51 +12,51 @@ let currentVideoTitle = ''; let episodesReversed = false; // 页面初始化 -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener('DOMContentLoaded', function () { // 初始化API复选框 initAPICheckboxes(); - + // 初始化自定义API列表 renderCustomAPIsList(); - + // 初始化显示选中的API数量 updateSelectedApiCount(); - + // 渲染搜索历史 renderSearchHistory(); - + // 设置默认API选择(如果是第一次加载) if (!localStorage.getItem('hasInitializedDefaults')) { // 默认选中资源 - selectedAPIs = ["tyyszy","bfzy","dyttzy", "ruyi"]; + selectedAPIs = ["tyyszy", "bfzy", "dyttzy", "ruyi"]; localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs)); - + // 默认选中过滤开关 localStorage.setItem('yellowFilterEnabled', 'true'); localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, 'true'); - + // 默认启用豆瓣功能 localStorage.setItem('doubanEnabled', 'true'); // 标记已初始化默认值 localStorage.setItem('hasInitializedDefaults', 'true'); } - + // 设置黄色内容过滤器开关初始状态 const yellowFilterToggle = document.getElementById('yellowFilterToggle'); if (yellowFilterToggle) { yellowFilterToggle.checked = localStorage.getItem('yellowFilterEnabled') === 'true'; } - + // 设置广告过滤开关初始状态 const adFilterToggle = document.getElementById('adFilterToggle'); if (adFilterToggle) { adFilterToggle.checked = localStorage.getItem(PLAYER_CONFIG.adFilteringStorage) !== 'false'; // 默认为true } - + // 设置事件监听器 setupEventListeners(); - + // 初始检查成人API选中状态 setTimeout(checkAdultAPIsSelected, 100); }); @@ -74,14 +74,14 @@ function initAPICheckboxes() { normalTitle.className = 'api-group-title'; normalTitle.textContent = '普通资源'; normaldiv.appendChild(normalTitle); - + // 创建普通API源的复选框 Object.keys(API_SITES).forEach(apiKey => { const api = API_SITES[apiKey]; if (api.adult) return; // 跳过成人内容API,稍后添加 - + const checked = selectedAPIs.includes(apiKey); - + const checkbox = document.createElement('div'); checkbox.className = 'flex items-center'; checkbox.innerHTML = ` @@ -92,9 +92,9 @@ function initAPICheckboxes() { `; normaldiv.appendChild(checkbox); - + // 添加事件监听器 - checkbox.querySelector('input').addEventListener('change', function() { + checkbox.querySelector('input').addEventListener('change', function () { updateSelectedAPIs(); checkAdultAPIsSelected(); }); @@ -126,14 +126,14 @@ function addAdultAPI() { `; adultdiv.appendChild(adultTitle); - + // 创建成人API源的复选框 Object.keys(API_SITES).forEach(apiKey => { const api = API_SITES[apiKey]; if (!api.adult) return; // 仅添加成人内容API - + const checked = selectedAPIs.includes(apiKey); - + const checkbox = document.createElement('div'); checkbox.className = 'flex items-center'; checkbox.innerHTML = ` @@ -144,9 +144,9 @@ function addAdultAPI() { `; adultdiv.appendChild(checkbox); - + // 添加事件监听器 - checkbox.querySelector('input').addEventListener('change', function() { + checkbox.querySelector('input').addEventListener('change', function () { updateSelectedAPIs(); checkAdultAPIsSelected(); }); @@ -159,30 +159,30 @@ function addAdultAPI() { function checkAdultAPIsSelected() { // 查找所有内置成人API复选框 const adultBuiltinCheckboxes = document.querySelectorAll('#apiCheckboxes .api-adult:checked'); - + // 查找所有自定义成人API复选框 const customApiCheckboxes = document.querySelectorAll('#customApisList .api-adult:checked'); - + const hasAdultSelected = adultBuiltinCheckboxes.length > 0 || customApiCheckboxes.length > 0; - + const yellowFilterToggle = document.getElementById('yellowFilterToggle'); const yellowFilterContainer = yellowFilterToggle.closest('div').parentNode; const filterDescription = yellowFilterContainer.querySelector('p.filter-description'); - + // 如果选择了成人API,禁用黄色内容过滤器 if (hasAdultSelected) { yellowFilterToggle.checked = false; yellowFilterToggle.disabled = true; localStorage.setItem('yellowFilterEnabled', 'false'); - + // 添加禁用样式 yellowFilterContainer.classList.add('filter-disabled'); - + // 修改描述文字 if (filterDescription) { filterDescription.innerHTML = '选中黄色资源站时无法启用此过滤'; } - + // 移除提示信息(如果存在) const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip'); if (existingTooltip) { @@ -192,12 +192,12 @@ function checkAdultAPIsSelected() { // 启用黄色内容过滤器 yellowFilterToggle.disabled = false; yellowFilterContainer.classList.remove('filter-disabled'); - + // 恢复原来的描述文字 if (filterDescription) { filterDescription.innerHTML = '过滤"伦理片"等黄色内容'; } - + // 移除提示信息 const existingTooltip = yellowFilterContainer.querySelector('.filter-tooltip'); if (existingTooltip) { @@ -210,12 +210,12 @@ function checkAdultAPIsSelected() { function renderCustomAPIsList() { const container = document.getElementById('customApisList'); if (!container) return; - + if (customAPIs.length === 0) { container.innerHTML = '

未添加自定义API

'; return; } - + container.innerHTML = ''; customAPIs.forEach((api, index) => { const apiItem = document.createElement('div'); @@ -244,7 +244,7 @@ function renderCustomAPIsList() { `; container.appendChild(apiItem); - apiItem.querySelector('input').addEventListener('change', function() { + apiItem.querySelector('input').addEventListener('change', function () { updateSelectedAPIs(); checkAdultAPIsSelected(); }); @@ -313,10 +313,10 @@ function cancelEditCustomApi() { document.getElementById('customApiDetail').value = ''; const isAdultInput = document.getElementById('customApiIsAdult'); if (isAdultInput) isAdultInput.checked = false; - + // 隐藏表单 document.getElementById('addCustomApiForm').classList.add('hidden'); - + // 恢复添加按钮 restoreAddCustomApiButtons(); } @@ -335,20 +335,20 @@ function restoreAddCustomApiButtons() { function updateSelectedAPIs() { // 获取所有内置API复选框 const builtInApiCheckboxes = document.querySelectorAll('#apiCheckboxes input:checked'); - + // 获取选中的内置API const builtInApis = Array.from(builtInApiCheckboxes).map(input => input.dataset.api); - + // 获取选中的自定义API const customApiCheckboxes = document.querySelectorAll('#customApisList input:checked'); const customApiIndices = Array.from(customApiCheckboxes).map(input => 'custom_' + input.dataset.customIndex); - + // 合并内置和自定义API selectedAPIs = [...builtInApis, ...customApiIndices]; - + // 保存到localStorage localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs)); - + // 更新显示选中的API数量 updateSelectedApiCount(); } @@ -364,7 +364,7 @@ function updateSelectedApiCount() { // 全选或取消全选API function selectAllAPIs(selectAll = true, excludeAdult = false) { const checkboxes = document.querySelectorAll('#apiCheckboxes input[type="checkbox"]'); - + checkboxes.forEach(checkbox => { if (excludeAdult && checkbox.classList.contains('api-adult')) { checkbox.checked = false; @@ -372,7 +372,7 @@ function selectAllAPIs(selectAll = true, excludeAdult = false) { checkbox.checked = selectAll; } }); - + updateSelectedAPIs(); checkAdultAPIsSelected(); } @@ -395,7 +395,7 @@ function cancelAddCustomApi() { document.getElementById('customApiDetail').value = ''; const isAdultInput = document.getElementById('customApiIsAdult'); if (isAdultInput) isAdultInput.checked = false; - + // 确保按钮是添加按钮 restoreAddCustomApiButtons(); } @@ -428,7 +428,7 @@ function addCustomApi() { const newApiIndex = customAPIs.length - 1; selectedAPIs.push('custom_' + newApiIndex); localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs)); - + // 重新渲染自定义API列表 renderCustomAPIsList(); updateSelectedApiCount(); @@ -444,17 +444,17 @@ function addCustomApi() { // 移除自定义API function removeCustomApi(index) { if (index < 0 || index >= customAPIs.length) return; - + const apiName = customAPIs[index].name; - + // 从列表中移除API customAPIs.splice(index, 1); localStorage.setItem('customAPIs', JSON.stringify(customAPIs)); - + // 从选中列表中移除此API const customApiId = 'custom_' + index; selectedAPIs = selectedAPIs.filter(id => id !== customApiId); - + // 更新大于此索引的自定义API索引 selectedAPIs = selectedAPIs.map(id => { if (id.startsWith('custom_')) { @@ -465,38 +465,66 @@ function removeCustomApi(index) { } return id; }); - + localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs)); - + // 重新渲染自定义API列表 renderCustomAPIsList(); - + // 更新选中的API数量 updateSelectedApiCount(); - + // 重新检查成人API选中状态 checkAdultAPIsSelected(); - + showToast('已移除自定义API: ' + apiName, 'info'); } +function toggleSettings(e) { + const settingsPanel = document.getElementById('settingsPanel'); + if (!settingsPanel) return; + + // 检查是否有管理员密码 + const hasAdminPassword = window.__ENV__?.ADMINPASSWORD && + window.__ENV__.ADMINPASSWORD.length === 64 && + !/^0+$/.test(window.__ENV__.ADMINPASSWORD); + + if (settingsPanel.classList.contains('show')) { + settingsPanel.classList.remove('show'); + } else { + // 只有设置了管理员密码且未验证时才拦截 + if (hasAdminPassword && !isAdminVerified()) { + e.preventDefault(); + e.stopPropagation(); + showAdminPasswordModal(); + return; + } + settingsPanel.classList.add('show'); + } + + if (e) { + e.preventDefault(); + e.stopPropagation(); + } +} + // 设置事件监听器 function setupEventListeners() { // 回车搜索 - document.getElementById('searchInput').addEventListener('keypress', function(e) { + document.getElementById('searchInput').addEventListener('keypress', function (e) { if (e.key === 'Enter') { search(); } }); // 点击外部关闭设置面板和历史记录面板 - document.addEventListener('click', function(e) { + document.addEventListener('click', function (e) { // 关闭设置面板 const settingsPanel = document.querySelector('#settingsPanel.show'); const settingsButton = document.querySelector('#settingsPanel .close-btn'); - - if (settingsPanel && settingsButton && - !settingsPanel.contains(e.target) && + + if (settingsPanel && settingsButton && + !settingsPanel.contains(e.target) && !settingsButton.contains(e.target)) { settingsPanel.classList.remove('show'); } @@ -504,18 +532,18 @@ function setupEventListeners() { // 关闭历史记录面板 const historyPanel = document.querySelector('#historyPanel.show'); const historyButton = document.querySelector('#historyPanel .close-btn'); - - if (historyPanel && historyButton && - !historyPanel.contains(e.target) && + + if (historyPanel && historyButton && + !historyPanel.contains(e.target) && !historyButton.contains(e.target)) { historyPanel.classList.remove('show'); } }); - + // 黄色内容过滤开关事件绑定 const yellowFilterToggle = document.getElementById('yellowFilterToggle'); if (yellowFilterToggle) { - yellowFilterToggle.addEventListener('change', function(e) { + yellowFilterToggle.addEventListener('change', function (e) { localStorage.setItem('yellowFilterEnabled', e.target.checked); // 控制黄色内容接口的显示状态 @@ -532,11 +560,11 @@ function setupEventListeners() { } }); } - + // 广告过滤开关事件绑定 const adFilterToggle = document.getElementById('adFilterToggle'); if (adFilterToggle) { - adFilterToggle.addEventListener('change', function(e) { + adFilterToggle.addEventListener('change', function (e) { localStorage.setItem(PLAYER_CONFIG.adFilteringStorage, e.target.checked); }); } @@ -547,28 +575,28 @@ function resetSearchArea() { // 清理搜索结果 document.getElementById('results').innerHTML = ''; document.getElementById('searchInput').value = ''; - + // 恢复搜索区域的样式 document.getElementById('searchArea').classList.add('flex-1'); document.getElementById('searchArea').classList.remove('mb-8'); document.getElementById('resultsArea').classList.add('hidden'); - + // 确保页脚正确显示,移除相对定位 const footer = document.querySelector('.footer'); if (footer) { footer.style.position = ''; } - + // 如果有豆瓣功能,检查是否需要显示豆瓣推荐区域 if (typeof updateDoubanVisibility === 'function') { updateDoubanVisibility(); } - + // 重置URL为主页 try { window.history.pushState( - {}, - `LibreTV - 免费在线视频搜索与观看平台`, + {}, + `LibreTV - 免费在线视频搜索与观看平台`, `/` ); // 更新页面标题 @@ -597,35 +625,35 @@ async function search() { } } const query = document.getElementById('searchInput').value.trim(); - + if (!query) { showToast('请输入搜索内容', 'info'); return; } - + if (selectedAPIs.length === 0) { showToast('请至少选择一个API源', 'warning'); return; } - + showLoading(); - + try { // 保存搜索历史 saveSearchHistory(query); - + // 从所有选中的API源搜索 let allResults = []; const searchPromises = selectedAPIs.map(async (apiId) => { try { let apiUrl, apiName, apiBaseUrl; - + // 处理自定义API if (apiId.startsWith('custom_')) { const customIndex = apiId.replace('custom_', ''); const customApi = getCustomApiInfo(customIndex); if (!customApi) return []; - + apiBaseUrl = customApi.url; apiUrl = apiBaseUrl + API_CONFIG.search.path + encodeURIComponent(query); apiName = customApi.name; @@ -636,28 +664,28 @@ async function search() { apiUrl = apiBaseUrl + API_CONFIG.search.path + encodeURIComponent(query); apiName = API_SITES[apiId].name; } - + // 添加超时处理 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 8000); - + const response = await fetch(PROXY_URL + encodeURIComponent(apiUrl), { headers: API_CONFIG.search.headers, signal: controller.signal }); - + clearTimeout(timeoutId); - + if (!response.ok) { return []; } - + const data = await response.json(); - + if (!data || !data.list || !Array.isArray(data.list) || data.list.length === 0) { return []; } - + // 处理第一页结果 const results = data.list.map(item => ({ ...item, @@ -665,41 +693,41 @@ async function search() { source_code: apiId, api_url: apiId.startsWith('custom_') ? getCustomApiInfo(apiId.replace('custom_', ''))?.url : undefined })); - + // 获取总页数 const pageCount = data.pagecount || 1; // 确定需要获取的额外页数 (最多获取maxPages页) const pagesToFetch = Math.min(pageCount - 1, API_CONFIG.search.maxPages - 1); - + // 如果有额外页数,获取更多页的结果 if (pagesToFetch > 0) { const additionalPagePromises = []; - + for (let page = 2; page <= pagesToFetch + 1; page++) { // 构建分页URL const pageUrl = apiBaseUrl + API_CONFIG.search.pagePath .replace('{query}', encodeURIComponent(query)) .replace('{page}', page); - + // 创建获取额外页的Promise const pagePromise = (async () => { try { const pageController = new AbortController(); const pageTimeoutId = setTimeout(() => pageController.abort(), 8000); - + const pageResponse = await fetch(PROXY_URL + encodeURIComponent(pageUrl), { headers: API_CONFIG.search.headers, signal: pageController.signal }); - + clearTimeout(pageTimeoutId); - + if (!pageResponse.ok) return []; - + const pageData = await pageResponse.json(); - + if (!pageData || !pageData.list || !Array.isArray(pageData.list)) return []; - + // 处理当前页结果 return pageData.list.map(item => ({ ...item, @@ -712,13 +740,13 @@ async function search() { return []; } })(); - + additionalPagePromises.push(pagePromise); } - + // 等待所有额外页的结果 const additionalResults = await Promise.all(additionalPagePromises); - + // 合并所有页的结果 additionalResults.forEach(pageResults => { if (pageResults.length > 0) { @@ -726,43 +754,43 @@ async function search() { } }); } - + return results; } catch (error) { console.warn(`API ${apiId} 搜索失败:`, error); return []; } }); - + // 等待所有搜索请求完成 const resultsArray = await Promise.all(searchPromises); - + // 合并所有结果 resultsArray.forEach(results => { if (Array.isArray(results) && results.length > 0) { allResults = allResults.concat(results); } }); - + // 更新搜索结果计数 const searchResultsCount = document.getElementById('searchResultsCount'); if (searchResultsCount) { searchResultsCount.textContent = allResults.length; } - + // 显示结果区域,调整搜索区域 document.getElementById('searchArea').classList.remove('flex-1'); document.getElementById('searchArea').classList.add('mb-8'); document.getElementById('resultsArea').classList.remove('hidden'); - + // 隐藏豆瓣推荐区域(如果存在) const doubanArea = document.getElementById('doubanArea'); if (doubanArea) { doubanArea.classList.add('hidden'); } - + const resultsDiv = document.getElementById('results'); - + // 如果没有结果 if (!allResults || allResults.length === 0) { resultsDiv.innerHTML = ` @@ -785,8 +813,8 @@ async function search() { const encodedQuery = encodeURIComponent(query); // 使用HTML5 History API更新URL,不刷新页面 window.history.pushState( - { search: query }, - `搜索: ${query} - LibreTV`, + { search: query }, + `搜索: ${query} - LibreTV`, `/s=${encodedQuery}` ); // 更新页面标题 @@ -799,7 +827,7 @@ async function search() { // 处理搜索结果过滤:如果启用了黄色内容过滤,则过滤掉分类含有敏感内容的项目 const yellowFilterEnabled = localStorage.getItem('yellowFilterEnabled') === 'true'; if (yellowFilterEnabled) { - const banned = ['伦理片','福利','里番动漫','门事件','萝莉少女','制服诱惑','国产传媒','cosplay','黑丝诱惑','无码','日本无码','有码','日本有码','SWAG','网红主播', '色情片','同性片','福利视频','福利片']; + const banned = ['伦理片', '福利', '里番动漫', '门事件', '萝莉少女', '制服诱惑', '国产传媒', 'cosplay', '黑丝诱惑', '无码', '日本无码', '有码', '日本有码', 'SWAG', '网红主播', '色情片', '同性片', '福利视频', '福利片']; allResults = allResults.filter(item => { const typeName = item.type_name || ''; return !banned.some(keyword => typeName.includes(keyword)); @@ -813,17 +841,17 @@ async function search() { .replace(//g, '>') .replace(/"/g, '"'); - const sourceInfo = item.source_name ? + const sourceInfo = item.source_name ? `${item.source_name}` : ''; const sourceCode = item.source_code || ''; - + // 添加API URL属性,用于详情获取 - const apiUrlAttr = item.api_url ? + const apiUrlAttr = item.api_url ? `data-api-url="${item.api_url.replace(/"/g, '"')}"` : ''; - + // 修改为水平卡片布局,图片在左侧,文本在右侧,并优化样式 const hasCover = item.vod_pic && item.vod_pic.startsWith('http'); - + return `
@@ -842,12 +870,12 @@ async function search() {

${safeName}

- ${(item.type_name || '').toString().replace(/ + ${(item.type_name || '').toString().replace(/ ${(item.type_name || '').toString().replace(/` : ''} - ${(item.vod_year || '') ? - ` + ${(item.vod_year || '') ? + ` ${item.vod_year} ` : ''}
@@ -874,7 +902,7 @@ async function search() {
`; }).join(''); - + resultsDiv.innerHTML = safeResults; } catch (error) { console.error('搜索错误:', error); @@ -914,12 +942,12 @@ function hookInput() { // 重写 value 属性的 getter 和 setter Object.defineProperty(input, 'value', { - get: function() { + get: function () { // 确保读取时返回字符串(即使原始值为 undefined/null) const originalValue = descriptor.get.call(this); return originalValue != null ? String(originalValue) : ''; }, - set: function(value) { + set: function (value) { // 显式将值转换为字符串后写入 const strValue = String(value); descriptor.set.call(this, strValue); @@ -945,12 +973,12 @@ async function showDetails(id, vod_name, sourceCode) { showToast('视频ID无效', 'error'); return; } - + showLoading(); try { // 构建API参数 let apiParams = ''; - + // 处理自定义API源 if (sourceCode.startsWith('custom_')) { const customIndex = sourceCode.replace('custom_', ''); @@ -970,26 +998,26 @@ async function showDetails(id, vod_name, sourceCode) { // 内置API apiParams = '&source=' + sourceCode; } - + // Add a timestamp to prevent caching const timestamp = new Date().getTime(); const cacheBuster = `&_t=${timestamp}`; const response = await fetch(`/api/detail?id=${encodeURIComponent(id)}${apiParams}${cacheBuster}`); - + const data = await response.json(); - + const modal = document.getElementById('modal'); const modalTitle = document.getElementById('modalTitle'); const modalContent = document.getElementById('modalContent'); - + // 显示来源信息 - const sourceName = data.videoInfo && data.videoInfo.source_name ? + const sourceName = data.videoInfo && data.videoInfo.source_name ? ` (${data.videoInfo.source_name})` : ''; - + // 不对标题进行截断处理,允许完整显示 modalTitle.innerHTML = `${vod_name || '未知视频'}${sourceName}`; currentVideoTitle = vod_name || '未知视频'; - + if (data.episodes && data.episodes.length > 0) { // 构建详情信息HTML let detailInfoHtml = ''; @@ -1021,10 +1049,10 @@ async function showDetails(id, vod_name, sourceCode) { `; } } - + currentEpisodes = data.episodes; currentEpisodeIndex = 0; - + modalContent.innerHTML = ` ${detailInfoHtml}
@@ -1054,7 +1082,7 @@ async function showDetails(id, vod_name, sourceCode) {
`; } - + modal.classList.remove('hidden'); } catch (error) { console.error('获取详情错误:', error); @@ -1073,18 +1101,18 @@ function playVideo(url, vod_name, sourceCode, episodeIndex = 0, vodId = '') { return; } } - + // 获取当前路径作为返回页面 let currentPath = window.location.href; - + // 构建播放页面URL,使用watch.html作为中间跳转页 let watchUrl = `watch.html?id=${vodId || ''}&source=${sourceCode || ''}&url=${encodeURIComponent(url)}&index=${episodeIndex}&title=${encodeURIComponent(vod_name || '')}`; - + // 添加返回URL参数 if (currentPath.includes('index.html') || currentPath.endsWith('/')) { watchUrl += `&back=${encodeURIComponent(currentPath)}`; } - + // 保存当前状态到localStorage try { localStorage.setItem('currentVideoTitle', vod_name || '未知视频'); @@ -1097,7 +1125,7 @@ function playVideo(url, vod_name, sourceCode, episodeIndex = 0, vodId = '') { } catch (e) { console.error('保存播放状态失败:', e); } - + // 在当前标签页中打开播放页面 window.location.href = watchUrl; } @@ -1203,7 +1231,7 @@ function toggleEpisodeOrder(sourceCode, vodId) { if (episodesGrid) { episodesGrid.innerHTML = renderEpisodes(currentVideoTitle, sourceCode, vodId); } - + // 更新按钮文本和箭头方向 const toggleBtn = document.querySelector(`button[onclick="toggleEpisodeOrder('${sourceCode}', '${vodId}')"]`); if (toggleBtn) { @@ -1277,7 +1305,7 @@ async function importConfigFromUrl() { } showLoading('正在从URL导入配置...'); - + try { // 获取配置文件 - 直接请求URL const response = await fetch(url, { @@ -1305,7 +1333,7 @@ async function importConfigFromUrl() { for (let item in config.data) { localStorage.setItem(item, config.data[item]); } - + showToast('配置文件导入成功,3 秒后自动刷新本页面。', 'success'); setTimeout(() => { window.location.reload(); @@ -1335,7 +1363,7 @@ async function importConfig() { if (!(file.type === 'application/json' || file.name.endsWith('.json'))) throw '文件类型不正确'; // 检查文件大小 - if(file.size > 1024 * 1024 * 10) throw new Error('文件大小超过 10MB'); + if (file.size > 1024 * 1024 * 10) throw new Error('文件大小超过 10MB'); // 读取文件内容 const content = await new Promise((resolve, reject) => { @@ -1357,7 +1385,7 @@ async function importConfig() { for (let item in config.data) { localStorage.setItem(item, config.data[item]); } - + showToast('配置文件导入成功,3 秒后自动刷新本页面。', 'success'); setTimeout(() => { window.location.reload(); diff --git a/js/config.js b/js/config.js index 81b1691..cd1df8e 100644 --- a/js/config.js +++ b/js/config.js @@ -8,6 +8,7 @@ const MAX_HISTORY_ITEMS = 5; const PASSWORD_CONFIG = { localStorageKey: 'passwordVerified', // 存储验证状态的键名 verificationTTL: 90 * 24 * 60 * 60 * 1000, // 验证有效期(90天,约3个月) + adminLocalStorageKey: 'adminPasswordVerified' // 新增的管理员验证状态的键名 }; // 网站信息配置 diff --git a/js/password.js b/js/password.js index 8d40cd0..554dccc 100644 --- a/js/password.js +++ b/js/password.js @@ -7,63 +7,77 @@ function isPasswordProtected() { // 检查页面上嵌入的环境变量 const pwd = window.__ENV__ && window.__ENV__.PASSWORD; - // 只有当密码 hash 存在且为64位(SHA-256十六进制长度)才认为启用密码保护 - return typeof pwd === 'string' && pwd.length === 64 && !/^0+$/.test(pwd); -} + const adminPwd = window.__ENV__ && window.__ENV__.ADMINPASSWORD; -/** - * 检查用户是否已通过密码验证 - * 检查localStorage中的验证状态和时间戳是否有效,并确认密码哈希未更改 - */ -function isPasswordVerified() { - try { - // 如果没有设置密码保护,则视为已验证 - if (!isPasswordProtected()) { - return true; - } + // 检查普通密码或管理员密码是否有效 + const isPwdValid = typeof pwd === 'string' && pwd.length === 64 && !/^0+$/.test(pwd); + const isAdminPwdValid = typeof adminPwd === 'string' && adminPwd.length === 64 && !/^0+$/.test(adminPwd); - const verificationData = JSON.parse(localStorage.getItem(PASSWORD_CONFIG.localStorageKey) || '{}'); - const { verified, timestamp, passwordHash } = verificationData; - - // 获取当前环境中的密码哈希 - const currentHash = window.__ENV__ && window.__ENV__.PASSWORD; - - // 验证是否已验证、未过期,且密码哈希未更改 - if (verified && timestamp && passwordHash === currentHash) { - const now = Date.now(); - const expiry = timestamp + PASSWORD_CONFIG.verificationTTL; - return now < expiry; - } - - return false; - } catch (error) { - console.error('验证密码状态时出错:', error); - return false; - } + // 任意一个密码有效即认为启用了密码保护 + return isPwdValid || isAdminPwdValid; } window.isPasswordProtected = isPasswordProtected; -window.isPasswordVerified = isPasswordVerified; /** * 验证用户输入的密码是否正确(异步,使用SHA-256哈希) */ -async function verifyPassword(password) { - const correctHash = window.__ENV__ && window.__ENV__.PASSWORD; - if (!correctHash) return false; - const inputHash = await sha256(password); - const isValid = inputHash === correctHash; - if (isValid) { - const verificationData = { - verified: true, - timestamp: Date.now(), - passwordHash: correctHash // 保存当前密码的哈希值 - }; - localStorage.setItem(PASSWORD_CONFIG.localStorageKey, JSON.stringify(verificationData)); +// 统一验证函数 +async function verifyPassword(password, passwordType = 'PASSWORD') { + try { + const correctHash = window.__ENV__?.[passwordType]; + if (!correctHash) return false; + + const inputHash = await sha256(password); + const isValid = inputHash === correctHash; + + if (isValid) { + const storageKey = passwordType === 'PASSWORD' + ? PASSWORD_CONFIG.localStorageKey + : PASSWORD_CONFIG.adminLocalStorageKey; + + localStorage.setItem(storageKey, JSON.stringify({ + verified: true, + timestamp: Date.now(), + passwordHash: correctHash + })); + } + return isValid; + } catch (error) { + console.error(`验证${passwordType}密码时出错:`, error); + return false; } - return isValid; } +// 统一验证状态检查 +function isVerified(passwordType = 'PASSWORD') { + try { + if (!isPasswordProtected()) return true; + + const storageKey = passwordType === 'PASSWORD' + ? PASSWORD_CONFIG.localStorageKey + : PASSWORD_CONFIG.adminLocalStorageKey; + + const stored = localStorage.getItem(storageKey); + if (!stored) return false; + + const { timestamp, passwordHash } = JSON.parse(stored); + const currentHash = window.__ENV__?.[passwordType]; + + return timestamp && passwordHash === currentHash && + Date.now() - timestamp < PASSWORD_CONFIG.verificationTTL; + } catch (error) { + console.error(`检查${passwordType}验证状态时出错:`, error); + return false; + } +} + +// 更新全局导出 +window.isPasswordProtected = isPasswordProtected; +window.isPasswordVerified = () => isVerified('PASSWORD'); +window.isAdminVerified = () => isVerified('ADMINPASSWORD'); +window.verifyPassword = verifyPassword; + // SHA-256实现,可用Web Crypto API async function sha256(message) { if (window.crypto && crypto.subtle && crypto.subtle.digest) { @@ -89,7 +103,7 @@ function showPasswordModal() { document.getElementById('doubanArea').classList.add('hidden'); passwordModal.style.display = 'flex'; - + // 确保输入框获取焦点 setTimeout(() => { const passwordInput = document.getElementById('passwordInput'); @@ -160,32 +174,71 @@ async function handlePasswordSubmit() { /** * 初始化密码验证系统(需适配异步事件) */ +// 修改initPasswordProtection函数 function initPasswordProtection() { if (!isPasswordProtected()) { - return; // 如果未设置密码保护,则不进行任何操作 + return; } - // 如果未验证密码,则显示密码验证弹窗 - if (!isPasswordVerified()) { + // 检查是否有普通密码 + const hasNormalPassword = window.__ENV__?.PASSWORD && + window.__ENV__.PASSWORD.length === 64 && + !/^0+$/.test(window.__ENV__.PASSWORD); + + // 只有当设置了普通密码且未验证时才显示密码框 + if (hasNormalPassword && !isPasswordVerified()) { showPasswordModal(); - - // 设置密码提交按钮事件监听 - const submitButton = document.getElementById('passwordSubmitBtn'); - if (submitButton) { - submitButton.addEventListener('click', handlePasswordSubmit); - } - - // 设置密码输入框回车键监听 - const passwordInput = document.getElementById('passwordInput'); - if (passwordInput) { - passwordInput.addEventListener('keypress', function(e) { - if (e.key === 'Enter') { - handlePasswordSubmit(); - } - }); - } + } + + // 设置按钮事件监听 + const settingsBtn = document.querySelector('[onclick="toggleSettings(event)"]'); + if (settingsBtn) { + settingsBtn.addEventListener('click', function(e) { + // 只有当设置了普通密码且未验证时才拦截点击 + if (hasNormalPassword && !isPasswordVerified()) { + e.preventDefault(); + e.stopPropagation(); + showPasswordModal(); + return; + } + + }); + } +} + +// 设置按钮密码框验证 +function showAdminPasswordModal() { + const passwordModal = document.getElementById('passwordModal'); + if (!passwordModal) return; + + // 清空密码输入框 + const passwordInput = document.getElementById('passwordInput'); + if (passwordInput) passwordInput.value = ''; + + // 修改标题为管理员验证 + const title = passwordModal.querySelector('h2'); + if (title) title.textContent = '管理员验证'; + passwordModal.style.display = 'flex'; + + // 设置表单提交处理 + const form = document.getElementById('passwordForm'); + if (form) { + form.onsubmit = async function (e) { + e.preventDefault(); + const password = document.getElementById('passwordInput').value.trim(); + if (await verifyPassword(password, 'ADMINPASSWORD')) { + passwordModal.style.display = 'none'; + document.getElementById('settingsPanel').classList.add('show'); + } else { + showPasswordError(); + } + }; } } // 在页面加载完成后初始化密码保护 -document.addEventListener('DOMContentLoaded', initPasswordProtection); +document.addEventListener('DOMContentLoaded', function () { + initPasswordProtection(); +}); + + diff --git a/middleware.js b/middleware.js index ddec704..f490ea0 100644 --- a/middleware.js +++ b/middleware.js @@ -30,12 +30,25 @@ export default async function middleware(request) { if (password) { passwordHash = await sha256(password); } - const modifiedHtml = originalHtml.replace( - 'window.__ENV__.PASSWORD = "{{PASSWORD}}";', - `window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash` - ); + + const adminpassword = process.env.ADMINPASSWORD || ''; + let adminpasswordHash = ''; + if (adminpassword) { + adminpasswordHash = await sha256(adminpassword); // 修复变量名 + } - // Create a new response with the modified HTML + // 合并两次替换为一次操作 + let modifiedHtml = originalHtml + .replace( + 'window.__ENV__.PASSWORD = "{{PASSWORD}}";', + `window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash` + ) + .replace( + 'window.__ENV__.ADMINPASSWORD = "{{ADMINPASSWORD}}";', + `window.__ENV__.ADMINPASSWORD = "${adminpasswordHash}"; // SHA-256 hash` + ); + + // 修复Response构造 return new Response(modifiedHtml, { status: response.status, statusText: response.statusText, diff --git a/player.html b/player.html index bc75456..6a97257 100644 --- a/player.html +++ b/player.html @@ -339,6 +339,7 @@ // 注入服务器端环境变量 (将由服务器端替换) // PASSWORD 变量将在这里被服务器端注入 window.__ENV__.PASSWORD = "{{PASSWORD}}"; + window.__ENV__.ADMINPASSWORD = "{{ADMINPASSWORD}}"; // 修复 home 跳转 document.addEventListener('DOMContentLoaded', function() { diff --git a/server.mjs b/server.mjs index 46c990f..6d3a9ea 100644 --- a/server.mjs +++ b/server.mjs @@ -15,6 +15,7 @@ const __dirname = path.dirname(__filename); const config = { port: process.env.PORT || 8080, password: process.env.PASSWORD || '', + adminpassword: process.env.ADMINPASSWORD || '', corsOrigin: process.env.CORS_ORIGIN || '*', timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'), maxRetries: parseInt(process.env.MAX_RETRIES || '2'), @@ -58,6 +59,11 @@ async function renderPage(filePath, password) { const sha256 = await sha256Hash(password); content = content.replace('{{PASSWORD}}', sha256); } + // 添加ADMINPASSWORD注入 + if (config.adminpassword !== '') { + const adminSha256 = await sha256Hash(config.adminpassword); + content = content.replace('{{ADMINPASSWORD}}', adminSha256); + } return content; } @@ -196,10 +202,13 @@ app.use((req, res) => { app.listen(config.port, () => { console.log(`服务器运行在 http://localhost:${config.port}`); if (config.password !== '') { - console.log('登录密码已设置'); + console.log('用户登录密码已设置'); + } + if (config.adminpassword !== '') { + console.log('管理员登录密码已设置'); } if (config.debug) { console.log('调试模式已启用'); - console.log('配置:', { ...config, password: config.password ? '******' : '' }); + console.log('配置:', { ...config, password: config.password ? '******' : '', adminpassword: config.adminpassword? '******' : '' }); } });