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 `