HOHOJ
https://hohoj.tv/
分享者: anph (14023)发布时间: 2天前
该用户很懒,什么介绍也没有写!
{
"articleStyle": 1,
"customOrder": 0,
"enableJs": true,
"enabled": true,
"enabledCookieJar": true,
"lastUpdateTime": 0,
"loadWithBaseUrl": true,
"ruleArticles": "div.video-item",
"ruleContent": "<!DOCTYPE html>\n<html lang=\"zh-Hans\">\n<head>\n<title>{{@@h5.mt-3@text}}<\/title>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no\">\n<meta name=\"referrer\" content=\"no-referrer\">\n<link href=\"https:\/\/cdn.bootcdn.net\/ajax\/libs\/plyr\/3.7.8\/plyr.css\" rel=\"stylesheet\">\n<\/head>\n<body>\n<p><\/p>\n<div class=\"video-container\">\n <video id=\"player\" playsinline controls preload=\"auto\" poster=\"https:\/\/qyyuapi.com\/img\/noposter.png\">\n <\/video>\n<\/div>\n<details>\n <summary>\n <h3><\/h3>\n <\/summary>\n <img>\n<\/details>\n<div class=\"all-info\">\n<div>\n <p>🕵 片名:{{@@h5.mt-3@text}}<\/p>\n <p>👨🎤 主演:{{@@div.model-name.mt-1@text}}<\/p>\n<\/div>\n<div class=\"jiekou\" style=\"\">\n<p>🎬 集数: <\/p>\n<div id=\"selected-jiekou\" onclick=\"jiekouList()\"><button data-id=\"0\"><b>无尽视频<sup>1<\/sup><\/b><\/button><\/div><div id=\"jiekou-list\" style=\"display: none;\"><\/div><\/div>\n\n{{\nlet video_url= java.getString('iframe.player@src')\nlet content = java.get(source.sourceUrl + video_url,{}).body();\nlet ss = java.getString('#my-video@src', content);\n\nlet dom = `<div class=\"jishu\" style=\"display:block;\"><p>\n<button onclick=\"jishu(this)\" data-src=\"${ss}\"><b>01<\/b><\/button>\n<\/p><\/div>`;\ndom;\n}}\n\n\n<\/div>\n\n<script src=\"https:\/\/gcore.jsdelivr.net\/npm\/hls.js@canary\"><\/script>\n<script src=\"https:\/\/cdn.bootcdn.net\/ajax\/libs\/plyr\/3.7.8\/plyr.min.js\"><\/script>\n\n<script>\n\nconst JKkey = \"{{java.md5Encode16(baseUrl.replace(\/.*\\}([^\\}\\`]+)\\`\/,'$1'))}}\";\nconst JDkey = \"{{java.md5Encode16(baseUrl.replace(\/.*\\}([^\\}\\`]+)\\`\/,'$1') + 'time')}}\";\nconst PTtime = {{\/^\\d+$\/.test('跳过片头:') ?'跳过片头:' : 0}};\nconst PWtime = {{\/^\\d+$\/.test('跳过片尾:') ? '跳过片尾:' : 0}};\nconst BSspeed = {{\/^\\d+$\/.test('长按倍速:') ? '长按倍速:' : 2}};\nconst ImageUrl = \"{{\/^http\/.test('背景图片:') ? '背景图片:' : ''}}\";\nconst Opacity1 = \"{{\/0|1|^0\\.\\d+$\/.test('图片透明度:') ? '图片透明度:' : ''}}\";\nconst Opacity2 = \"{{\/0|1|^0\\.\\d+$\/.test('按钮透明度:') ? '按钮透明度:' : ''}}\";\n\n\/\/ 获取视频URL并更新视频源\nasync function geturl() {\n try {\n let src = String($(\".jishu button.active\")[0].dataset.src);\n\n \/\/ 获取页面信息\n let fm = \"\";\n\n \/\/ 获取视频源\n let zyurl = [];\n if (\/mxcontent\/.test(src)) {\n zyurl.push({src:src,size:\"1\"});\n } else {\n zyurl.push({src:src,size:\"1\"});\n }\n\n \/\/ 更新详情封面\n $(\"img\")[0].src = fm;\n\n \/\/ 更新视频封面\n $(\".video-container\")[0].style.background = `#000 url('${fm}') no-repeat center center \/ cover`;\n\n \/\/ 返回视频源\n let sources = zyurl;\n return { sources: sources };\n } catch (error) {\n weblog(error, '错误:', true);\n console.error(\"错误:\", error);\n throw error;\n }\n}\n\n\/\/ 点击集数按钮时调用的函数\nasync function jishu(item) {\n var video = $('video')[0];\n var wasPlaying = (video && !video.paused) || localStorage.getItem('fromEnded') === 'true';\n if (localStorage.getItem('fromEnded') === 'true') {\n localStorage.removeItem('fromEnded');\n }\n omit($('.jishu button.active'));\n item.className = \"active\";\n const { sources } = await geturl();\n setTimeout(updatePadding, 100);\n var index1 = $('#selected-jiekou button')[0].dataset.id;\n var index2 = Array.from(item.parentNode.children).indexOf(item);\n var Progress = {\n index1: index1,\n index2: index2\n };\n localStorage.setItem(JKkey, JSON.stringify(Progress));\n localStorage.removeItem(JDkey);\n initializePlayer(sources, JDkey, PTtime, PWtime, BSspeed, 1);\n if (wasPlaying && video) {\n const tryAutoPlay = () => {\n if (video.readyState >= 3) {\n video.play().catch(e => {\n console.log(\"自动播放被阻止:\", e);\n $('.plyr__control--overlaid').show();\n });\n video.removeEventListener('canplay', tryAutoPlay);\n }\n };\n if (video.readyState >= 3) {\n video.play().catch(e => console.log(\"立即播放失败:\", e));\n } else {\n video.addEventListener('canplay', tryAutoPlay);\n }\n }\n}\n\n\/\/ 页面加载时初始化播放器\n(async () => {\n var m = 0,n = 0;\n var Progress = localStorage.getItem(JKkey);\n if (Progress) {\n var history = JSON.parse(Progress);\n m = history.index1;\n n = history.index2;\n }\n if (m > 0) {\n const buttonList = $('#jiekou-list')[0].querySelectorAll('button');\n const targetButton = Array.from(buttonList).find(btn => btn.getAttribute('data-id') == m);\n jiekou(targetButton);\n const allButtons = $('.jishu')[m].querySelectorAll('button');\n active(allButtons, n);\n } else {\n active($('.jishu button'), n);\n }\n const { sources } = await geturl();\n setTimeout(updatePadding, 100);\n localStorage.setItem('HistoryTAG', 1);\n initializePlayer(sources, JDkey, PTtime, PWtime, BSspeed, 1);\n})();\n\n\/*************jsku************\/\n\/\/ 弹窗提示\nfunction weblog(message, title = '提示:', copy = false) {\n \/\/ 0. 全屏状态处理(仅退出不恢复)\n if (document.fullscreenElement) {\n document.exitFullscreen().catch(err => {\n console.warn('退出全屏失败:', err);\n });\n setTimeout(updatePadding, 100);\n }\n\n \/\/ 1. 保存页面原始状态\n const previousOverflow = document.body.style.overflow;\n const previousActiveElement = document.activeElement;\n document.body.style.overflow = 'hidden';\n\n \/\/ 2. 创建遮罩层\n const overlay = document.createElement('div');\n overlay.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.7);\n z-index: 2147483647;\n backdrop-filter: blur(3px);\n opacity: 0;\n transition: opacity 0.3s ease;\n pointer-events: auto;\n `;\n\n \/\/ 3. 创建弹窗容器\n const popup = document.createElement('div');\n popup.className = 'popup';\n popup.tabIndex = -1;\n\n \/\/ 4. 创建标题\n const titleElement = document.createElement('h3');\n titleElement.textContent = title;\n titleElement.className = 'titleElement';\n popup.appendChild(titleElement);\n\n \/\/ 5. 创建内容区域\n const contentElement = document.createElement('div');\n contentElement.textContent = message;\n contentElement.className = 'contentElement';\n popup.appendChild(contentElement);\n\n \/\/ 6. 创建按钮容器\n const buttonContainer = document.createElement('div');\n buttonContainer.style.cssText = `\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n `;\n\n \/\/ 7. 根据copy参数决定是否显示复制按钮\n if (copy) {\n const copyButton = document.createElement('button');\n copyButton.textContent = '复制文本';\n copyButton.style.cssText = `\n padding: 8px 18px;\n cursor: pointer;\n background-color: #4CAF50;\n color: #eee;\n border: none;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n transition: all 0.2s;\n height: 36px;\n box-sizing: border-box;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n `;\n\n \/\/ 复制功能实现\n copyButton.onclick = async () => {\n const originalText = copyButton.textContent;\n\n copyButton.disabled = true;\n copyButton.style.opacity = '0.7';\n copyButton.textContent = '复制中...';\n\n try {\n if (navigator.clipboard) {\n await navigator.clipboard.writeText(message);\n } else {\n const textarea = document.createElement('textarea');\n textarea.value = message;\n textarea.style.position = 'fixed';\n document.body.appendChild(textarea);\n textarea.select();\n document.execCommand('copy');\n document.body.removeChild(textarea);\n }\n\n copyButton.textContent = '✓ 已复制';\n copyButton.style.backgroundColor = '#2196F3';\n\n setTimeout(() => {\n copyButton.textContent = originalText;\n copyButton.style.backgroundColor = '#4CAF50';\n copyButton.disabled = false;\n copyButton.style.opacity = '1';\n }, 2000);\n } catch (err) {\n console.error('复制失败:', err);\n copyButton.textContent = '复制失败';\n setTimeout(() => {\n copyButton.textContent = originalText;\n copyButton.disabled = false;\n copyButton.style.opacity = '1';\n }, 2000);\n }\n };\n\n buttonContainer.appendChild(copyButton);\n }\n\n \/\/ 8. 创建关闭按钮\n const closeButton = document.createElement('button');\n closeButton.textContent = '关闭';\n closeButton.style.cssText = `\n padding: 8px 18px;\n cursor: pointer;\n background-color: #3493b6;\n color: #eee;\n border: none;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n transition: all 0.2s;\n height: 36px;\n box-sizing: border-box;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n `;\n buttonContainer.appendChild(closeButton);\n popup.appendChild(buttonContainer);\n\n \/\/ 9. 关闭功能\n const removePopup = () => {\n popup.style.opacity = '0';\n popup.style.transform = 'translate(-50%, -50%) scale(0.95)';\n overlay.style.opacity = '0';\n \n setTimeout(() => {\n \/\/ 移除元素\n if (overlay.parentNode) overlay.parentNode.removeChild(overlay);\n if (popup.parentNode) popup.parentNode.removeChild(popup);\n \n \/\/ 恢复页面状态\n document.body.style.overflow = previousOverflow;\n previousActiveElement?.focus();\n document.removeEventListener('keydown', escHandler);\n }, 300);\n };\n\n closeButton.onclick = removePopup;\n overlay.onclick = removePopup;\n\n \/\/ 10. 添加到页面\n document.body.appendChild(overlay);\n document.body.appendChild(popup);\n\n \/\/ 触发动画\n setTimeout(() => {\n overlay.style.opacity = '1';\n popup.style.opacity = '1';\n popup.style.transform = 'translate(-50%, -50%) scale(1)';\n }, 10);\n\n \/\/ 11. ESC键关闭\n const escHandler = (e) => {\n if (e.key === 'Escape') {\n removePopup();\n }\n };\n document.addEventListener('keydown', escHandler);\n}\n\n\/\/ 选中标签\nfunction $(rule) {\n return document.querySelectorAll(rule);\n}\n\n\/\/ 删除选中标签的class\nfunction omit(items) {\n return Array.from(items, (item) => {\n item.className = \"\";\n });\n}\n\n\/\/ 选中标签的class增加active\nfunction active(items, index) {\n items[index].className = \"active\";\n}\n\nlet isButtonListVisible = false;\n\n\/\/切换候选列表显示\nfunction jiekouList() {\n const selectedButton = $('#selected-jiekou > button')[0];\n const buttonList = $('#jiekou-list')[0];\n isButtonListVisible = !isButtonListVisible;\n if (isButtonListVisible) {\n if (buttonList.querySelectorAll('button').length > 0) {\n selectedButton.style.borderRadius = '0';\n buttonList.style.display = 'block';\n }\n selectedButton.style.opacity = '1';\n \/\/ 添加全局点击事件监听\n document.addEventListener('click', function globalClickListener(e) {\n const clickedInsideButtonList = buttonList.contains(e.target);\n const clickedSelectedButton = $('#selected-jiekou > button')[0].contains(e.target);\n if (!clickedInsideButtonList && !clickedSelectedButton) {\n selectedButton.style.borderRadius = '';\n selectedButton.style.opacity = '';\n buttonList.style.display = 'none';\n isButtonListVisible = false;\n document.removeEventListener('click', globalClickListener);\n }\n });\n } else {\n selectedButton.style.borderRadius = '';\n selectedButton.style.opacity = '';\n buttonList.style.display = 'none';\n }\n}\n\n\/\/切换列表接口\nfunction jiekou(clickedButton) {\n const selectedContainer = $('#selected-jiekou')[0];\n const selectedButton = selectedContainer.querySelector('button');\n if (selectedButton) {\n selectedButton.style.borderRadius = '';\n selectedButton.style.opacity = '';\n }\n const buttonList = $('#jiekou-list')[0];\n buttonList.appendChild(selectedContainer.querySelector('button'));\n selectedContainer.appendChild(clickedButton);\n isButtonListVisible = false;\n buttonList.style.display = 'none';\n const allButtons = buttonList.querySelectorAll('button');\n allButtons.forEach(button => {\n button.setAttribute('onclick', 'jiekou(this)');\n });\n const buttonsArray = Array.from(buttonList.querySelectorAll('button'));\n buttonsArray.sort((a, b) => {\n return parseInt(a.getAttribute('data-id')) - parseInt(b.getAttribute('data-id'));\n });\n buttonList.innerHTML = '';\n buttonsArray.forEach(button => {\n buttonList.appendChild(button);\n });\n JishuList(clickedButton);\n}\n\n\/\/ 切换集数列表显示\nfunction JishuList(clickedButton) {\n const dataId = clickedButton.getAttribute('data-id');\n const allJishuLists = $('.jishu');\n allJishuLists.forEach(list => {\n list.style.display = 'none';\n });\n allJishuLists[dataId].style.display = 'block';\n}\n\n\/\/ 保存原生的 fetch 函数\nconst originalFetch = window.fetch;\n\n\/\/ 重新定义全局的 fetch 函数\nwindow.fetch = async function(input, init = {}) {\n const url = typeof input === 'string' ? input : input.url;\n \n \/\/ 准备请求选项\n let requestOptions = {\n method: init.method || 'GET',\n headers: {\n 'Accept': 'text\/html,application\/xhtml+xml,application\/xml;q=0.9,*\/*;q=0.8',\n 'Accept-Encoding': 'gzip, deflate, br',\n 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',\n ...init.headers\n },\n cache: 'no-cache',\n ...init\n };\n\n \/\/ 处理请求体\n if (init.body) {\n const body = init.body;\n if (!requestOptions.headers['Content-Type']) {\n requestOptions.headers['Content-Type'] = typeof body === 'object' \n ? 'application\/json'\n : 'application\/x-www-form-urlencoded';\n }\n \n if (typeof body === 'object' && !(body instanceof FormData) && !(body instanceof URLSearchParams)) {\n requestOptions.body = JSON.stringify(body);\n } else if (typeof body === 'string') {\n requestOptions.body = body.replace(\/\\+\/g, '%2B');\n } else {\n requestOptions.body = body;\n }\n }\n\n \/\/ 如果没有请求体但存在 Content-Type 头,则删除它\n if (!init.body && requestOptions.headers['Content-Type']) {\n delete requestOptions.headers['Content-Type'];\n }\n\n \/\/ 使用降级策略\n return await fetchWithFallback(input, init, url, requestOptions);\n};\n\n\/\/ 降级策略函数\nasync function fetchWithFallback(input, init, url, requestOptions) {\n \/\/ 确定当前可用的方法级别\n let methodLevel = 1; \/\/ 从最高级别开始尝试\n \n while (methodLevel <= 2) {\n try {\n switch (methodLevel) {\n case 1: \/\/ 第一级:legado\n if (typeof window.run === 'function') {\n console.log('Using legado (level 1)');\n return await useWindowRun(url, requestOptions);\n }\n break;\n \n case 2: \/\/ 第二级:原生 fetch\n console.log('Using fetch (level 2)');\n return await originalFetch(input, init);\n }\n } catch (error) {\n console.warn(`Method level ${methodLevel} failed:`, error);\n \/\/ 当前级别失败,尝试下一级别\n }\n \n methodLevel++; \/\/ 尝试下一级别\n }\n \n \/\/ 使用原生 fetch 作为最终保底\n console.error('All method levels failed, using native fetch as final fallback');\n return await originalFetch(input, init);\n}\n\n\/\/ 使用 legado 发送请求\nasync function useWindowRun(url, requestOptions) {\n const origin = window.location.origin;\n const rawText = await window.run(`\n let response = java.connect('${\/^http\/.test(url) ? url : (origin + url)},${JSON.stringify(requestOptions)}');\n response.code() + '#|#' + response.body();\n `);\n const raw = rawText.split(\"#|#\");\n \n \/\/ 判断状态码\n let status = parseInt(raw[0]);\n \n \/\/ 状态码对应的文本\n const statusTexts = {\n 100: 'Continue',\n 101: 'Switching Protocols',\n 102: 'Processing',\n 200: 'OK',\n 201: 'Created',\n 202: 'Accepted',\n 203: 'Non-Authoritative Information',\n 204: 'No Content',\n 205: 'Reset Content',\n 206: 'Partial Content',\n 207: 'Multi-Status',\n 208: 'Already Reported',\n 226: 'IM Used',\n 300: 'Multiple Choices',\n 301: 'Moved Permanently',\n 302: 'Found',\n 303: 'See Other',\n 304: 'Not Modified',\n 305: 'Use Proxy',\n 307: 'Temporary Redirect',\n 308: 'Permanent Redirect',\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 402: 'Payment Required',\n 403: 'Forbidden',\n 404: 'Not Found',\n 405: 'Method Not Allowed',\n 406: 'Not Acceptable',\n 407: 'Proxy Authentication Required',\n 408: 'Request Timeout',\n 409: 'Conflict',\n 410: 'Gone',\n 411: 'Length Required',\n 412: 'Precondition Failed',\n 413: 'Payload Too Large',\n 414: 'URI Too Long',\n 415: 'Unsupported Media Type',\n 416: 'Range Not Satisfiable',\n 417: 'Expectation Failed',\n 418: 'I\\'m a teapot',\n 421: 'Misdirected Request',\n 422: 'Unprocessable Entity',\n 423: 'Locked',\n 424: 'Failed Dependency',\n 425: 'Too Early',\n 426: 'Upgrade Required',\n 428: 'Precondition Required',\n 429: 'Too Many Requests',\n 431: 'Request Header Fields Too Large',\n 451: 'Unavailable For Legal Reasons',\n 500: 'Internal Server Error',\n 501: 'Not Implemented',\n 502: 'Bad Gateway',\n 503: 'Service Unavailable',\n 504: 'Gateway Timeout',\n 505: 'HTTP Version Not Supported',\n 506: 'Variant Also Negotiates',\n 507: 'Insufficient Storage',\n 508: 'Loop Detected',\n 510: 'Not Extended',\n 511: 'Network Authentication Required'\n };\n \n \/\/ 提取响应体\n const responseBody = raw.length > 1 ? raw[1] : '';\n \n \/\/ 判断Content-Type\n let contentType = 'text\/plain;charset=utf-8';\n \n \/\/ 先检查URL扩展名\n const urlLower = url.toLowerCase();\n if (urlLower.includes('.ts') || urlLower.includes('.m3u8') || urlLower.includes('.mpeg')) {\n contentType = 'video\/mp2t';\n } else if (urlLower.includes('.mp4') || urlLower.includes('.m4v')) {\n contentType = 'video\/mp4';\n } else if (urlLower.includes('.webm')) {\n contentType = 'video\/webm';\n } else if (urlLower.includes('.avi')) {\n contentType = 'video\/x-msvideo';\n } else if (urlLower.includes('.mov')) {\n contentType = 'video\/quicktime';\n } else if (urlLower.includes('.flv')) {\n contentType = 'video\/x-flv';\n } else if (urlLower.includes('.mkv')) {\n contentType = 'video\/x-matroska';\n } else if (urlLower.includes('.jpg') || urlLower.includes('.jpeg')) {\n contentType = 'image\/jpeg';\n } else if (urlLower.includes('.png')) {\n contentType = 'image\/png';\n } else if (urlLower.includes('.gif')) {\n contentType = 'image\/gif';\n } else if (urlLower.includes('.svg')) {\n contentType = 'image\/svg+xml';\n } else if (urlLower.includes('.webp')) {\n contentType = 'image\/webp';\n } else if (urlLower.includes('.json')) {\n contentType = 'application\/json;charset=utf-8';\n } else if (urlLower.includes('.xml')) {\n contentType = 'application\/xml;charset=utf-8';\n } else if (urlLower.includes('.html') || urlLower.includes('.htm')) {\n contentType = 'text\/html;charset=utf-8';\n } else if (urlLower.includes('.css')) {\n contentType = 'text\/css;charset=utf-8';\n } else if (urlLower.includes('.js')) {\n contentType = 'application\/javascript;charset=utf-8';\n } else if (urlLower.includes('.pdf')) {\n contentType = 'application\/pdf';\n } else if (urlLower.includes('.zip')) {\n contentType = 'application\/zip';\n } else if (urlLower.includes('.rar')) {\n contentType = 'application\/x-rar-compressed';\n } else if (urlLower.includes('.7z')) {\n contentType = 'application\/x-7z-compressed';\n } else if (urlLower.includes('.tar')) {\n contentType = 'application\/x-tar';\n } else if (urlLower.includes('.gz')) {\n contentType = 'application\/gzip';\n } else {\n \/\/ 如果没有通过URL判断出来,尝试通过内容判断\n if (responseBody && responseBody.length > 0) {\n \/\/ 尝试判断是否为JSON\n try {\n \/\/ 检查是否以{或[开头\n const trimmed = responseBody.trim();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n JSON.parse(responseBody);\n contentType = 'application\/json;charset=utf-8';\n }\n } catch (e) {\n \/\/ 不是JSON,继续检查\n }\n \n \/\/ 检查是否为HTML\n if (responseBody.includes('<!DOCTYPE') || \n responseBody.includes('<html') || \n responseBody.includes('<head>') || \n responseBody.includes('<body>')) {\n contentType = 'text\/html;charset=utf-8';\n }\n \n \/\/ 检查是否为CSS\n else if (responseBody.includes('{') && responseBody.includes('}') && \n (responseBody.includes('color:') || \n responseBody.includes('font-size:') || \n responseBody.includes('background:'))) {\n contentType = 'text\/css;charset=utf-8';\n }\n \n \/\/ 检查是否为JavaScript\n else if (responseBody.includes('function') || \n responseBody.includes('var ') || \n responseBody.includes('const ') || \n responseBody.includes('let ') || \n responseBody.includes('console.log')) {\n contentType = 'application\/javascript;charset=utf-8';\n }\n \n \/\/ 检查是否包含大量非ASCII字符或控制字符(可能是二进制数据)\n else if (responseBody.length > 100) {\n let binaryCount = 0;\n for (let i = 0; i < Math.min(responseBody.length, 1000); i++) {\n const charCode = responseBody.charCodeAt(i);\n \/\/ 控制字符(除了常见的空白字符)或非ASCII字符\n if ((charCode < 32 && charCode !== 9 && charCode !== 10 && charCode !== 13) || charCode > 127) {\n binaryCount++;\n }\n }\n \n \/\/ 如果超过5%的字符是二进制特征,认为是二进制数据\n if (binaryCount > Math.min(responseBody.length, 1000) * 0.05) {\n contentType = 'application\/octet-stream';\n }\n }\n }\n }\n \n \/\/ 创建并返回Response对象\n return new Response(responseBody, {\n status: status,\n statusText: statusTexts[status] || 'Unknown Status',\n headers: new Headers({\n 'Content-Type': contentType\n })\n });\n}\n\n\/\/ fetchRequest 函数\nasync function fetchRequest(url, headers, body = null) {\n try {\n const init = {\n headers: headers\n };\n \n if (body) {\n init.method = 'POST';\n init.body = body;\n }\n\n const response = await fetch(url, init);\n \n if (!response.ok) {\n throw new Error(`HTTP error! Status: ${response.status}`);\n }\n\n const rawText = await response.text();\n\n \/\/ 尝试解析 JSON\n try {\n const jsonData = JSON.parse(rawText);\n return jsonData;\n } catch (e) {\n \/\/ 如果不是 JSON,返回原始文本\n return rawText;\n }\n } catch (error) {\n weblog(error, '请求失败:', true);\n return null;\n }\n}\n\n\/\/ 全局时间管理器\nconst TimeManager = {\n currentVideoUrl: '',\n headtime: 0,\n endtime: 0,\n listeners: [],\n\n init(videoUrl) {\n this.currentVideoUrl = videoUrl;\n this._loadFromStorage();\n this._setupListeners();\n },\n\n _loadFromStorage() {\n const head = localStorage.getItem('head' + this.currentVideoUrl);\n const end = localStorage.getItem('end' + this.currentVideoUrl);\n this.headtime = head ? parseFloat(head) : 0;\n this.endtime = end ? parseFloat(end) : 0;\n this._notifyAll();\n },\n\n _setupListeners() {\n window.addEventListener('storage', (e) => {\n if (e.key === 'head' + this.currentVideoUrl) {\n this.headtime = e.newValue ? parseFloat(e.newValue) : 0;\n this._notify('head', this.headtime);\n }\n if (e.key === 'end' + this.currentVideoUrl) {\n this.endtime = e.newValue ? parseFloat(e.newValue) : 0;\n this._notify('end', this.endtime);\n }\n });\n },\n\n setHeadTime(time) {\n this.headtime = time;\n localStorage.setItem('head' + this.currentVideoUrl, time);\n this._notify('head', time);\n },\n\n setEndTime(time) {\n this.endtime = time;\n localStorage.setItem('end' + this.currentVideoUrl, time);\n this._notify('end', time);\n },\n\n clearMarks() {\n localStorage.removeItem('head' + this.currentVideoUrl);\n localStorage.removeItem('end' + this.currentVideoUrl);\n this.headtime = 0;\n this.endtime = 0;\n this._notifyAll();\n },\n\n addListener(callback) {\n this.listeners.push(callback);\n },\n\n _notify(type, value) {\n this.listeners.forEach(cb => cb({ type, value }));\n },\n\n _notifyAll() {\n this._notify('head', this.headtime);\n this._notify('end', this.endtime);\n }\n};\n\n\/\/ 初始设置\nlet player = null;\nlet timeManager = null;\nlet currentHls = null;\nlet isSwitching = false;\n\n\/\/ 初始化播放器\nfunction initializePlayer(sources, currentVideoUrl, defaultHeadtime, defaultEndtime, speed, autonext) {\n const video = $('video')[0];\n video.style.height = '56.25vw';\n const qualityOptions = sources.map(source => parseInt(source.size));\n\n \/\/ 修改图片和透明度\n updateBackground(\n typeof ImageUrl !== 'undefined' ? ImageUrl : undefined,\n typeof Opacity1 !== 'undefined' ? Opacity1 : undefined, \n typeof Opacity2 !== 'undefined' ? Opacity2 : undefined\n );\n\n \/\/ 初始化时间管理器\n if (!timeManager || timeManager.currentVideoUrl !== currentVideoUrl) {\n timeManager = Object.create(TimeManager);\n timeManager.init(currentVideoUrl);\n \n \/\/ 设置默认值(如果localStorage中没有)\n if (!localStorage.getItem('head' + currentVideoUrl) && defaultHeadtime) {\n timeManager.setHeadTime(defaultHeadtime);\n }\n if (!localStorage.getItem('end' + currentVideoUrl) && defaultEndtime) {\n timeManager.setEndTime(defaultEndtime);\n }\n }\n\n \/\/ 如果播放器已存在,则只更新视频源\n if (player) {\n \/\/ 获取当前画质\n let currentQuality = qualityOptions[0];\n if (player.storage && player.storage.get) {\n const savedQuality = player.storage.get('quality');\n if (savedQuality && qualityOptions.includes(parseInt(savedQuality))) {\n currentQuality = parseInt(savedQuality);\n }\n }\n if (!currentQuality) {\n currentQuality = Math.max(...qualityOptions);\n }\n \n \/\/ 加载当前画质的视频源\n const selectedSource = sources.find(source => source.size === currentQuality.toString()) || sources[0];\n const savedProgress = localStorage.getItem(currentVideoUrl);\n const progress = (savedProgress && savedProgress > timeManager.headtime) ? savedProgress : timeManager.headtime;\n \n \/\/ 更新画质配置\n player.config.quality.options = qualityOptions;\n player.config.quality.default = currentQuality;\n player.config.quality.onChange = (newQuality) => {\n changeVideoQuality(newQuality, sources);\n };\n \n \/\/ 更新主菜单的画质显示\n mainMenuQuality(currentQuality)\n \n \/\/ 更新画质菜单选项\n updateQualityOptions(qualityOptions, currentQuality, sources);\n \n \/\/ 更新p标签显示当前源\n $(\"body>p\")[0].innerHTML = `\n <div style=\"display: flex; align-items: center; justify-content: space-between; width: 100%;\">\n <a href=\"legadovideo:\/\/${encodeURIComponent(selectedSource.src)}\">\n ${selectedSource.src}\n <\/a>\n <button \n onclick=\"navigator.clipboard.writeText('${selectedSource.src.replace(\/'\/g, \"\\\\'\")}')\n .then(() => weblog('链接已复制到剪贴板'))\n .catch(() => weblog('复制链接失败'))\"\n >\n 复制\n <\/button>\n <\/div>\n `;\n \n setTimeout(() => (isSwitching = false), 2000);\n \n if (Hls.isSupported() && \/m3u8|hls\/.test(selectedSource.src)) {\n initializeHlsPlayer(selectedSource.src, selectedSource.headersOrReferer, video, progress);\n } else {\n initializeRegularPlayer(selectedSource.src, selectedSource.headersOrReferer, video, progress);\n }\n \n return player;\n }\n\n \/\/ 创建新的播放器实例\n player = new Plyr(video, {\n controls: [\n 'play-large', \/\/ 大播放按钮\n 'rewind', \/\/ 倒退\n 'play', \/\/ 播放\n 'fast-forward', \/\/ 快进\n 'progress', \/\/ 进度条\n 'current-time', \/\/ 当前时间\n 'duration', \/\/ 总时长\n 'mute', \/\/ 静音\n 'volume', \/\/ 音量\n 'captions', \/\/ 字幕\n 'settings', \/\/ 设置\n 'pip', \/\/ 画中画\n 'airplay', \/\/ Airplay\n 'fullscreen' \/\/ 全屏\n ],\n settings: ['quality', 'speed'],\n quality: {\n default: qualityOptions[0],\n options: qualityOptions,\n forced: true,\n onChange: (newQuality) => {\n changeVideoQuality(newQuality, sources);\n }\n },\n fullscreen: {\n enabled: true,\n fallback: true,\n iosNative: true,\n container: null,\n },\n speed: {\n selected: 1, \/\/ 设置默认播放倍数\n options: [8,5,4,3,2.5, 2, 1.5, 1, 0.5, 0.25],\n },\n i18n: {\n restart: '重新开始',\n rewind: '倒退 {seektime} 秒',\n play: '播放',\n pause: '暂停',\n fastForward: '快进 {seektime} 秒',\n seek: '进度',\n seekLabel: '{currentTime} \/ {duration}',\n played: '播放',\n buffered: '缓冲',\n currentTime: '当前时间',\n duration: '持续时间',\n volume: '音量',\n mute: '静音',\n unmute: '取消静音',\n enableCaptions: '启用字幕',\n disableCaptions: '禁用字幕',\n enterFullscreen: '进入全屏',\n exitFullscreen: '退出全屏',\n frameTitle: '播放器',\n captions: '字幕',\n settings: '设置',\n speed: '速度',\n normal: '正常',\n quality: '画质',\n qualityLabel: {\n 0: '自动',\n },\n pip: '画中画',\n loop: '循环',\n start: '开始',\n end: '结束',\n all: '全部',\n reset: '重置',\n disabled: '禁用',\n advertisement: '广告'\n },\n keyboard: {\n focused: true,\n global: true,\n },\n tooltips: {\n controls: true,\n seek: true\n },\n captions: {\n active: true,\n update: true,\n language: 'auto',\n },\n });\n\n player.on('ready', () => {\n video.style.visibility = 'visible';\n\n \/\/ 添加显示集数\n addEpisodeBarToPlayer();\n\n \/\/ 添加跳过菜单项\n addSkipMenuItems(currentVideoUrl, timeManager);\n\n \/\/ 初始化播放器方向控制功能\n initPlayerOrientationControl(player);\n\n \/\/ 设置进度记录\n progressRecording(currentVideoUrl, video);\n\n \/\/ 设置自动下一集监听\n autoNextListener(currentVideoUrl, autonext, video);\n\n \/\/ 添加上下集按钮\n addEpisodeButtons(currentVideoUrl, autonext);\n\n \/\/ 设置手势控制\n gestureControls(player, video, speed);\n\n \/\/ 设置全屏和按钮显示处理\n fullscreenControls(autonext, video);\n\n \/\/ 初始化处理全屏状态\n updateFullscreen(autonext, video);\n\n \/\/ 播放开始自动改为黑色背景\n player.on('play', () => {\n $(\".video-container\")[0].style.background = '#000';\n });\n\n \/\/ 进度大于0时自动改为黑色背景\n player.on('timeupdate', () => {\n if (video.currentTime > 0) $(\".video-container\")[0].style.background = '#000';\n });\n\n \/\/ 获取保存的播放进度\n const savedProgress = localStorage.getItem(currentVideoUrl);\n const progress = (savedProgress && savedProgress > timeManager.headtime) ? parseFloat(savedProgress) : timeManager.headtime;\n if (!isNaN(progress) && progress > 0) {\n video.currentTime = progress;\n }\n\n \/\/ 调用切换画质函数加载默认画质\n changeVideoQuality(qualityOptions[0], sources);\n });\n\n return player;\n}\n\n\/\/ 修改图片和透明度\nfunction updateBackground(imageUrl, opacity1, opacity2) {\n if (!imageUrl || imageUrl === '') imageUrl = 'https:\/\/bg.qyyuapi.com\/img\/background.jpg';\n if (!opacity1 || opacity1 === '') opacity1 = 0.2;\n if (!opacity2 || opacity2 === '') opacity2 = 0.6;\n document.body.style.setProperty('--bg-image', `url('${imageUrl}')`);\n document.body.style.setProperty('--bg-opacity1', opacity1);\n document.body.style.setProperty('--bg-opacity2', opacity2);\n}\n\n\/\/ 更新画质菜单选项\nfunction updateQualityOptions(qualityOptions, currentQuality, sources) {\n if (!player || !player.elements || !player.elements.settings) return;\n \n try {\n const settings = player.elements.settings;\n const qualityPanel = settings.panels.quality;\n if (!qualityPanel) return;\n const menuContainer = qualityPanel.querySelector('[role=\"menu\"]');\n if (!menuContainer) return;\n menuContainer.innerHTML = '';\n qualityOptions.forEach(quality => {\n const isSelected = quality === currentQuality;\n const button = createQualityButton(quality, isSelected, sources);\n menuContainer.appendChild(button);\n });\n } catch (error) {\n console.error('更新画质菜单选项失败:', error);\n }\n}\n\n\/\/ 创建画质按钮\nfunction createQualityButton(quality, isSelected, sources) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'plyr__control';\n button.setAttribute('role', 'menuitemradio');\n button.setAttribute('aria-checked', isSelected ? 'true' : 'false');\n button.setAttribute('data-plyr', 'quality');\n button.value = quality;\n \n \/\/ 根据画质生成对应的按钮\n const badge = getQualityBadge(quality);\n button.innerHTML = `\n <span>${quality}p${badge ? `<span class=\"plyr__menu__value\"><span class=\"plyr__badge\">${badge}<\/span><\/span>` : '<\/span>'}\n `;\n \n \/\/ 绑定点击事件\n button.addEventListener('click', function(e) {\n e.preventDefault();\n e.stopPropagation();\n \n const quality = parseInt(this.value);\n \n \/\/ 1. 触发画质变更事件\n if (player.quality) {\n player.quality.current = quality;\n if (player.quality.set) {\n player.quality.set(quality);\n }\n player.emit('qualitychange', quality);\n }\n \n \/\/ 2. 更新配置\n player.config.quality.current = quality;\n \n \/\/ 3. 保存到Plyr存储\n if (player.storage && player.storage.set) {\n player.storage.set({ quality: quality });\n }\n \n \/\/ 4. 更新画质菜单选中状态\n qualitySelection(quality);\n \n \/\/ 5. 更新主菜单的画质显示\n mainMenuQuality(quality);\n \n \/\/ 6. 调用画质切换函数\n changeVideoQuality(quality, sources);\n \n \/\/ 7. 返回主菜单\n backToMainMenu();\n });\n \n \/\/ 设置初始选中状态\n if (isSelected) {\n button.classList.add('plyr__control--selected');\n const checkSpan = button.querySelector('.plyr__control--checked');\n if (checkSpan) {\n checkSpan.style.display = 'block';\n checkSpan.innerHTML = '✓';\n }\n }\n \n return button;\n}\n\n\/\/ 更新画质菜单选中状态\nfunction qualitySelection(selectedQuality) {\n if (!player || !player.elements.settings) return;\n \n const settings = player.elements.settings;\n const qualityPanel = settings.panels.quality;\n if (!qualityPanel) return;\n \n const menuContainer = qualityPanel.querySelector('[role=\"menu\"]');\n if (!menuContainer) return;\n \n const allButtons = menuContainer.querySelectorAll('[data-plyr=\"quality\"]');\n allButtons.forEach(btn => {\n const quality = parseInt(btn.value);\n const isSelected = quality === selectedQuality;\n \n btn.setAttribute('aria-checked', isSelected ? 'true' : 'false');\n \n if (isSelected) {\n btn.classList.add('plyr__control--selected');\n const checkSpan = btn.querySelector('.plyr__control--checked');\n if (checkSpan) {\n checkSpan.style.display = 'block';\n checkSpan.innerHTML = '✓';\n checkSpan.style.color = 'var(--plyr-control-toggle-checked-background, #00b3ff)';\n }\n } else {\n btn.classList.remove('plyr__control--selected');\n const checkSpan = btn.querySelector('.plyr__control--checked');\n if (checkSpan) {\n checkSpan.style.display = 'none';\n }\n }\n });\n}\n\n\/\/ 在home菜单中查找画质按钮\nfunction findQualityMenu(homeMenu) {\n const allButtons = homeMenu.querySelectorAll('[data-plyr=\"settings\"]');\n \n for (let button of allButtons) {\n const buttonText = button.textContent || button.innerText;\n \n if (buttonText.includes('画质')) {\n return button;\n }\n }\n \n console.log('未找到包含关键词的按钮');\n return null;\n}\n\n\/\/ 更新主菜单的画质显示\nfunction mainMenuQuality(quality) {\n try {\n \/\/ 查找设置弹出层容器\n const menuContainer = document.querySelector('.plyr__menu__container');\n if (!menuContainer) {\n console.log('未找到菜单容器');\n return;\n }\n \n \/\/ 查找主菜单(包含\"-home\"的ID)\n const homeMenu = menuContainer.querySelector('[id*=\"-home\"]');\n if (!homeMenu) {\n console.log('未找到主菜单');\n return;\n }\n \n \/\/ 在主菜单中查找画质按钮\n const qualityButton = findQualityMenu(homeMenu);\n if (!qualityButton) {\n console.log('未找到画质按钮');\n return;\n }\n \n \/\/ 更新画质显示\n const valueSpan = qualityButton.querySelector('.plyr__menu__value');\n if (valueSpan) {\n valueSpan.innerHTML = `${quality}p`;\n } else {\n console.log('未找到valueSpan');\n }\n \n } catch (error) {\n console.error('更新主菜单显示失败:', error);\n }\n}\n\n\/\/ 返回主菜单\nfunction backToMainMenu() {\n try {\n \/\/ 查找设置弹出层容器\n const menuContainer = document.querySelector('.plyr__menu__container');\n if (!menuContainer) {\n console.log('未找到菜单容器');\n return;\n }\n \n \/\/ 更改home菜单和quality菜单显示\n const homeMenu = menuContainer.querySelector('[id*=\"-home\"]');\n const qualityMenu = menuContainer.querySelector('[id*=\"-quality\"]');\n \n if (homeMenu && qualityMenu) {\n homeMenu.removeAttribute('hidden');\n qualityMenu.setAttribute('hidden', '');\n \n return;\n }\n \n console.log('未找到需要的菜单');\n \n } catch (error) {\n console.error('返回主菜单失败:', error);\n }\n}\n\n\/\/ 根据画质返回对应的徽章文本\nfunction getQualityBadge(quality) {\n if (quality >= 2160) return '4K';\n if (quality >= 1440) return '2K';\n if (quality >= 1080) return 'FHD';\n if (quality >= 720) return 'HD';\n if (quality >= 480) return 'SD';\n if (quality >= 360) return 'LD';\n if (quality >= 240) return 'ULD';\n return '';\n}\n\n\/\/ 设置进度记录\nfunction progressRecording(currentVideoUrl, video) {\n const progressInterval = setInterval(() => {\n const currentTime = video.currentTime;\n localStorage.setItem(currentVideoUrl, currentTime.toString());\n localStorage.setItem('HistoryTIME', Date.now());\n }, 5000);\n}\n\n\/\/ 设置自动下一集监听\nfunction autoNextListener(currentVideoUrl, autonext, video) {\n player.on('timeupdate', () => {\n if (!autonext || video.paused || timeManager.endtime === 0 || video.duration <= 0) return;\n if ((video.duration - video.currentTime) < timeManager.endtime) {\n const randomDelay = Math.floor(Math.random() * 200) + 100;\n setTimeout(() => {\n if (isSwitching) return;\n switchToNextEpisode(currentVideoUrl, autonext);\n }, randomDelay);\n }\n });\n\n player.on('ended', () => {\n if (isSwitching || !autonext || video.duration <= 0) return;\n const randomDelay = Math.floor(Math.random() * 200) + 100;\n setTimeout(() => {\n if (isSwitching) return;\n localStorage.setItem('fromEnded', 'true');\n switchToNextEpisode(currentVideoUrl, autonext);\n }, randomDelay);\n });\n}\n\n\/\/ 添加上下集按钮\nfunction addEpisodeButtons(currentVideoUrl, autonext) {\n if (autonext) {\n const style = document.createElement('style');\n style.textContent = `\n .plyr__control--episode-prev,\n .plyr__control--episode-next {\n display: none;\n margin: 1px;\n }\n .plyr__controls [data-plyr=\"rewind\"],\n .plyr__controls [data-plyr=\"fast-forward\"] {\n margin: 1px;\n }\n `;\n document.head.appendChild(style);\n\n const controlsContainer = document.querySelector('.plyr__controls');\n if (controlsContainer) {\n \/\/ 获取参考按钮\n const rewindBtn = controlsContainer.querySelector('[data-plyr=\"rewind\"]');\n const fastForwardBtn = controlsContainer.querySelector('[data-plyr=\"fast-forward\"]');\n\n \/\/ 创建上一集按钮(放在后退前)\n if (rewindBtn && !document.querySelector('.plyr__control--episode-prev')) {\n const prevBtn = document.createElement('button');\n prevBtn.className = 'plyr__control plyr__control--episode-prev';\n prevBtn.innerHTML = `\n <svg aria-hidden=\"true\" focusable=\"false\" width=\"18\" height=\"18\">\n <use xlink:href=\"#plyr-prev-episode\"><\/use>\n <\/svg>\n <span class=\"plyr__tooltip\">上一集<\/span>\n `;\n prevBtn.onclick = (e) => {\n e.preventDefault();\n switchToPrevEpisode(currentVideoUrl, autonext);\n };\n rewindBtn.parentNode.insertBefore(prevBtn, rewindBtn);\n }\n\n \/\/ 创建下一集按钮(放在快进后)\n if (fastForwardBtn && !document.querySelector('.plyr__control--episode-next')) {\n const nextBtn = document.createElement('button');\n nextBtn.className = 'plyr__control plyr__control--episode-next';\n nextBtn.innerHTML = `\n <svg aria-hidden=\"true\" focusable=\"false\" width=\"18\" height=\"18\">\n <use xlink:href=\"#plyr-next-episode\"><\/use>\n <\/svg>\n <span class=\"plyr__tooltip\">下一集<\/span>\n `;\n nextBtn.onclick = (e) => {\n e.preventDefault();\n switchToNextEpisode(currentVideoUrl, autonext);\n };\n fastForwardBtn.parentNode.insertBefore(nextBtn, fastForwardBtn.nextSibling);\n }\n }\n }\n}\n\n\/\/ 设置手势控制\nfunction gestureControls(player, video, speed) {\n const gesture = {\n startX: 0,\n startY: 0,\n startTime: 0,\n isLongPress: false,\n initialSpeed: 1,\n isSeeking: false,\n longPressTimer: null,\n wasPlaying: false,\n lastTime: 0,\n initialBrightness: 1,\n isBrightnessControl: false,\n initialVolume: 1,\n isVolumeControl: false,\n gestureType: null,\n threshold: 15\n };\n\n \/\/ 提示工具\n const controlTooltip = document.createElement('div');\n controlTooltip.className = 'plyr__center-tooltip';\n controlTooltip.innerHTML = `\n <span class=\"control-icon\" style=\"font-weight:bold\"><\/span>\n <span class=\"control-value\"><\/span>\n `;\n document.querySelector('.plyr__video-wrapper').appendChild(controlTooltip);\n\n \/\/ 获取系统亮度(如果支持)\n const getSystemBrightness = () => {\n return parseFloat(localStorage.getItem('systemBrightness')) || 1;\n };\n\n \/\/ 设置系统亮度(模拟实现)\n const setSystemBrightness = (brightness) => {\n localStorage.setItem('systemBrightness', brightness.toString());\n \n if ('screen' in window && 'brightness' in window.screen) {\n try {\n window.screen.brightness = brightness;\n } catch (e) {\n console.log('Screen Brightness API not supported');\n }\n }\n \n let brightnessOverlay = document.getElementById('brightness-overlay');\n if (!brightnessOverlay) {\n brightnessOverlay = document.createElement('div');\n brightnessOverlay.id = 'brightness-overlay';\n brightnessOverlay.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: black;\n pointer-events: none;\n z-index: 2147483647;\n opacity: 0;\n transition: opacity 0.2s ease;\n `;\n document.body.appendChild(brightnessOverlay);\n \n document.addEventListener('fullscreenchange', updateBrightnessOverlay);\n document.addEventListener('webkitfullscreenchange', updateBrightnessOverlay);\n }\n brightnessOverlay.style.opacity = (1 - brightness).toString();\n \n updateBrightnessOverlay();\n };\n\n \/\/ 更新亮度覆盖层位置\n const updateBrightnessOverlay = () => {\n const brightnessOverlay = document.getElementById('brightness-overlay');\n if (!brightnessOverlay) return;\n \n const isFullscreen = document.fullscreenElement || \n document.webkitFullscreenElement ||\n document.mozFullScreenElement ||\n document.msFullscreenElement;\n \n if (isFullscreen) {\n const fullscreenElement = document.fullscreenElement || \n document.webkitFullscreenElement ||\n document.mozFullScreenElement ||\n document.msFullscreenElement;\n \n if (fullscreenElement) {\n if (fullscreenElement !== document.body) {\n if (!fullscreenElement.contains(brightnessOverlay)) {\n fullscreenElement.appendChild(brightnessOverlay);\n }\n brightnessOverlay.style.position = 'absolute';\n brightnessOverlay.style.zIndex = '2147483647';\n } else {\n if (document.body.contains(brightnessOverlay)) {\n document.body.appendChild(brightnessOverlay);\n }\n brightnessOverlay.style.position = 'fixed';\n brightnessOverlay.style.zIndex = '2147483647';\n }\n }\n } else {\n if (!document.body.contains(brightnessOverlay)) {\n document.body.appendChild(brightnessOverlay);\n }\n brightnessOverlay.style.position = 'fixed';\n brightnessOverlay.style.zIndex = '2147483647';\n }\n };\n\n \/\/ 检查是否在全屏模式\n const isFullscreen = () => {\n return document.fullscreenElement || \n document.webkitFullscreenElement ||\n document.mozFullScreenElement ||\n document.msFullscreenElement;\n };\n\n \/\/ 检查是否在控制区域(全屏时排除顶部10%)\n const isInControlArea = (startX, startY) => {\n \/\/ 只有在全屏模式下才排除顶部区域\n if (isFullscreen()) {\n const screenHeight = window.innerHeight;\n \/\/ 全屏时顶部10%区域用于系统手势(如下拉通知栏)\n if (startY < screenHeight * 0.1) {\n return false;\n }\n }\n \n return true;\n };\n\n \/\/ 确定手势类型\n const determineGestureType = (diffX, diffY, startX) => {\n \/\/ 如果已经确定了手势类型,就保持原样\n if (gesture.gestureType) {\n return gesture.gestureType;\n }\n \n const absDiffX = Math.abs(diffX);\n const absDiffY = Math.abs(diffY);\n \n \/\/ 只有当移动距离超过阈值时才确定手势类型\n if (absDiffX < gesture.threshold && absDiffY < gesture.threshold) {\n return null;\n }\n \n \/\/ 判断主要移动方向\n if (absDiffX > absDiffY) {\n \/\/ 主要横向移动 - 进度控制\n return 'seek';\n } else {\n \/\/ 主要纵向移动 - 根据起始位置判断是亮度还是音量\n const isLeftSide = startX < window.innerWidth \/ 2;\n return isLeftSide ? 'brightness' : 'volume';\n }\n };\n\n \/\/ 显示控制提示\n const showControlTooltip = (icon, value = '') => {\n controlTooltip.style.opacity = '1';\n controlTooltip.querySelector('.control-icon').textContent = icon;\n controlTooltip.querySelector('.control-value').textContent = value;\n };\n\n \/\/ 隐藏控制提示\n const hideControlTooltip = () => {\n controlTooltip.style.opacity = '0';\n };\n\n \/\/ 长按倍数播放\n const onTouchStart = (e) => {\n if (e.touches?.length > 1) return;\n\n const clientX = e.clientX || e.touches[0].clientX;\n const clientY = e.clientY || e.touches[0].clientY;\n \n \/\/ 检查是否在控制区域(全屏时排除顶部10%)\n if (!isInControlArea(clientX, clientY)) return;\n\n gesture.startX = clientX;\n gesture.startY = clientY;\n gesture.startTime = video.currentTime;\n gesture.initialSpeed = player.speed;\n gesture.isLongPress = false;\n gesture.isSeeking = false;\n gesture.wasPlaying = !video.paused;\n gesture.lastTime = Date.now();\n gesture.gestureType = null;\n \n gesture.initialBrightness = getSystemBrightness();\n gesture.initialVolume = video.volume;\n gesture.isBrightnessControl = false;\n gesture.isVolumeControl = false;\n\n gesture.longPressTimer = setTimeout(() => {\n if (!gesture.isSeeking && !gesture.gestureType) {\n gesture.isLongPress = true;\n player.speed = speed;\n \n \/\/ 显示长按倍速提示\n showControlTooltip('倍速', `${speed}×`);\n }\n }, 800);\n };\n\n \/\/ 滑动控制进度、亮度和音量\n const onTouchMove = (e) => {\n if (!gesture.startX || gesture.isLongPress) return;\n\n const currentX = e.clientX || e.touches[0].clientX;\n const currentY = e.clientY || e.touches[0].clientY;\n const diffX = currentX - gesture.startX;\n const diffY = currentY - gesture.startY;\n \n \/\/ 确定手势类型(如果尚未确定)\n if (!gesture.gestureType) {\n gesture.gestureType = determineGestureType(diffX, diffY, gesture.startX);\n if (gesture.gestureType) {\n clearTimeout(gesture.longPressTimer);\n }\n }\n \n \/\/ 根据确定的手势类型执行相应操作\n switch (gesture.gestureType) {\n case 'brightness':\n handleBrightnessControl(diffY);\n break;\n \n case 'volume':\n handleVolumeControl(diffY);\n break;\n \n case 'seek':\n handleSeekControl(diffX);\n break;\n \n default:\n \/\/ 手势类型未确定,不执行任何操作\n return;\n }\n \n gesture.lastTime = Date.now();\n };\n\n \/\/ 处理亮度控制\n const handleBrightnessControl = (diffY) => {\n const brightnessChange = -diffY \/ window.innerHeight;\n let newBrightness = Math.max(0.1, Math.min(1, gesture.initialBrightness + brightnessChange));\n \n setSystemBrightness(newBrightness);\n \n showControlTooltip('亮度', `${Math.round(newBrightness * 100)}%`);\n };\n\n \/\/ 处理音量控制\n const handleVolumeControl = (diffY) => {\n const volumeChange = -diffY \/ window.innerHeight;\n let newVolume = Math.max(0, Math.min(1, gesture.initialVolume + volumeChange));\n \n video.volume = newVolume;\n player.volume = newVolume;\n \n const iconText = newVolume === 0 ? '静音' : '音量';\n showControlTooltip(iconText, `${Math.round(newVolume * 100)}%`);\n };\n\n \/\/ 处理进度控制\n const handleSeekControl = (diffX) => {\n if (!gesture.isSeeking) {\n gesture.isSeeking = true;\n gesture.wasPlaying = !video.paused;\n if (gesture.wasPlaying) {\n document.querySelector('.plyr__control--overlaid').classList.add('hidden');\n video.pause();\n }\n }\n \n const seekAmount = Math.round((diffX \/ window.innerWidth) * 300);\n const newTime = Math.max(0, Math.min(\n gesture.startTime + seekAmount,\n video.duration\n ));\n \n if (Math.abs(video.currentTime - newTime) >= 1) {\n $(\".video-container\")[0].style.background = '#000';\n video.currentTime = newTime;\n }\n \n if (newTime <= 0) {\n showControlTooltip('已到开头');\n } else if (newTime >= video.duration) {\n showControlTooltip('已到结尾');\n } else {\n const directionText = seekAmount > 0 ? '前进' : '后退';\n showControlTooltip(directionText, `${Math.abs(seekAmount)}秒`);\n }\n };\n\n \/\/ 触摸结束\n const onTouchEnd = () => {\n clearTimeout(gesture.longPressTimer);\n if (gesture.isLongPress) {\n player.speed = gesture.initialSpeed;\n \/\/ 立即隐藏提示\n hideControlTooltip();\n }\n else if (gesture.wasPlaying && !gesture.isLongPress && gesture.isSeeking) {\n video.play().catch(e => console.log(\"播放恢复失败:\", e));\n setTimeout(() => {\n document.querySelector('.plyr__control--overlaid').classList.remove('hidden');\n }, 100);\n \/\/ 延迟隐藏提示\n setTimeout(hideControlTooltip, 500);\n } else {\n \/\/ 其他情况延迟隐藏提示\n setTimeout(hideControlTooltip, 500);\n }\n \n \/\/ 重置所有状态\n gesture.startX = 0;\n gesture.isSeeking = false;\n gesture.isLongPress = false;\n gesture.isBrightnessControl = false;\n gesture.isVolumeControl = false;\n gesture.gestureType = null;\n };\n\n \/\/ 事件监听\n const playerContainer = document.querySelector('.plyr__video-wrapper');\n playerContainer.addEventListener('touchstart', onTouchStart, { passive: true });\n playerContainer.addEventListener('mousedown', onTouchStart);\n playerContainer.addEventListener('touchmove', onTouchMove, { passive: false });\n playerContainer.addEventListener('mousemove', onTouchMove);\n playerContainer.addEventListener('touchend', onTouchEnd);\n playerContainer.addEventListener('mouseup', onTouchEnd);\n playerContainer.addEventListener('mouseleave', onTouchEnd);\n\n setSystemBrightness(getSystemBrightness());\n\n document.addEventListener('fullscreenchange', updateBrightnessOverlay);\n document.addEventListener('webkitfullscreenchange', updateBrightnessOverlay);\n\n \/\/ 注入CSS样式\n const style = document.createElement('style');\n style.textContent = `\n .plyr__center-tooltip {\n position: absolute;\n top: calc(50% - 75px);\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: transparent;\n color: white;\n padding: 10px 20px;\n border-radius: 5px;\n z-index: 1000;\n font-size: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.3s ease;\n text-shadow: 0 0 5px rgba(0, 0, 0, 0.6), 0 0 10px rgba(0, 0, 0, 0.3);\n }\n .plyr__center-tooltip span { \n font-weight: bold; \n text-shadow: inherit; \n }\n .plyr__center-tooltip .control-value { \n font-weight: normal; \n }\n \n #brightness-overlay {\n background: black;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.2s ease;\n z-index: 2147483647 !important;\n }\n `;\n document.head.appendChild(style);\n}\n\n\/\/ 根据当前方向更新按钮可见状态\nfunction updateButtonVisibility(autonext) {\n if (!document.fullscreenElement || !autonext) return;\n \n const isPortrait = window.innerHeight > window.innerWidth;\n if (isPortrait) {\n $('[data-plyr=\"rewind\"], [data-plyr=\"fast-forward\"]').forEach(btn => {\n btn.style.display = 'none';\n });\n } else {\n $('[data-plyr=\"rewind\"], [data-plyr=\"fast-forward\"]').forEach(btn => {\n btn.style.display = 'flex';\n });\n }\n}\n\n\/\/ 处理全屏状态\nfunction updateFullscreen(autonext, video) {\n if (!document.fullscreenElement) {\n \/\/ 退出全屏时的处理\n video.style.height = '56.25vw';\n video.style.removeProperty('min-height');\n $('.plyr__menu__container > div').forEach(menu => {\n menu.style.maxHeight = '44vw';\n menu.style.overflowY = 'auto';\n });\n if (autonext) {\n $('.plyr__control--episode-prev, .plyr__control--episode-next').forEach(btn => {\n btn.style.display = 'none';\n });\n }\n $('[data-plyr=\"rewind\"], [data-plyr=\"fast-forward\"]').forEach(btn => {\n btn.style.display = 'flex';\n });\n setTimeout(updatePadding, 100);\n } else {\n \/\/ 进入全屏时的处理\n video.style.minHeight = '100%';\n video.style.removeProperty('height');\n $('.plyr__menu__container > div').forEach(menu => {\n menu.style.maxHeight = '';\n menu.style.overflowY = '';\n });\n if (autonext) {\n $('.plyr__control--episode-prev, .plyr__control--episode-next').forEach(btn => {\n btn.style.display = 'flex';\n });\n updateButtonVisibility(autonext); \/\/ 根据当前方向更新按钮\n }\n \n \/\/ 强制重绘\n setTimeout(() => {\n const videoWrapper = document.querySelector('.plyr__video-wrapper');\n if (videoWrapper) {\n videoWrapper.style.display = 'flex';\n videoWrapper.style.alignItems = 'center';\n videoWrapper.style.justifyContent = 'center';\n video.style.maxWidth = '100%';\n video.style.maxHeight = '100%';\n video.style.width = 'auto';\n video.style.height = 'auto';\n }\n }, 100);\n setTimeout(updatePadding, 100);\n }\n}\n\n\/\/ 设置全屏和按钮显示处理\nfunction fullscreenControls(autonext, video) {\n \/\/ 添加事件监听器\n document.addEventListener('fullscreenchange', () => updateFullscreen(autonext, video));\n window.addEventListener('resize', () => updateButtonVisibility(autonext));\n window.addEventListener('orientationchange', () => {\n setTimeout(() => updateButtonVisibility(autonext), 100);\n });\n}\n\n\/\/ 锁定屏幕方向为横屏(实时跟随设备方向)\nfunction lockLandscapeOrientation() {\n if (screen.orientation && screen.orientation.lock) {\n \/\/ 移除之前的监听器\n if (lockLandscapeOrientation.currentListener) {\n window.removeEventListener('deviceorientation', lockLandscapeOrientation.currentListener);\n }\n \n let lastDirection = null;\n let hasSetInitial = false;\n const DEAD_ZONE = 20; \/\/ 死区范围,避免轻微偏转就切换\n \n const handleOrientation = (event) => {\n const gamma = event.gamma;\n const beta = event.beta; \/\/ 前后倾斜角度\n \n \/\/ 在死区内不切换方向\n if (Math.abs(gamma) < DEAD_ZONE) {\n return;\n }\n \n let newDirection;\n \n \/\/ 根据屏幕朝向调整方向判断\n if (Math.abs(beta) > 90) {\n \/\/ 屏幕朝下,需要反向判断\n if (gamma > 0) {\n newDirection = 'landscape-primary'; \/\/ 屏幕朝下时,向右倾斜用右横屏\n } else {\n newDirection = 'landscape-secondary'; \/\/ 屏幕朝下时,向左倾斜用左横屏\n }\n } else {\n \/\/ 屏幕朝上,正常判断\n if (gamma > 0) {\n newDirection = 'landscape-secondary'; \/\/ 设备向右,左横屏\n } else {\n newDirection = 'landscape-primary'; \/\/ 设备向左,右横屏\n }\n }\n \n \/\/ 初始设置:立即根据第一个有效的gamma值设置方向\n if (!hasSetInitial) {\n hasSetInitial = true;\n lastDirection = newDirection;\n console.log('初始设置方向:', newDirection, 'gamma:', gamma, 'beta:', beta, '屏幕朝下:', Math.abs(beta) > 90);\n screen.orientation.lock(newDirection).catch(error => {\n console.warn('初始方向锁定失败:', error);\n });\n return;\n }\n \n \/\/ 后续切换:只有当方向确实改变时才锁定\n if (newDirection !== lastDirection) {\n lastDirection = newDirection;\n console.log('切换横屏方向:', newDirection, 'gamma:', gamma, 'beta:', beta, '屏幕朝下:', Math.abs(beta) > 90);\n screen.orientation.lock(newDirection).catch(error => {\n console.warn('横屏方向切换失败:', error);\n });\n }\n };\n \n \/\/ 开始监听\n window.addEventListener('deviceorientation', handleOrientation);\n lockLandscapeOrientation.currentListener = handleOrientation;\n \n \/\/ 不设置默认方向,等待第一个设备方向事件\n console.log('等待设备方向数据...');\n }\n}\n\n\/\/ 锁定屏幕方向为竖屏\nfunction lockPortraitOrientation() {\n if (screen.orientation && screen.orientation.lock) {\n \/\/ 移除横屏的监听器(如果有)\n if (lockLandscapeOrientation.currentListener) {\n window.removeEventListener('deviceorientation', lockLandscapeOrientation.currentListener);\n lockLandscapeOrientation.currentListener = null;\n }\n \n console.log('锁定竖屏方向');\n \/\/ 强制锁定竖屏,忽略当前设备方向\n screen.orientation.lock('portrait').catch(error => {\n console.warn('竖屏锁定失败:', error);\n });\n }\n}\n\n\/\/ 解锁屏幕方向\nfunction unlockOrientation() {\n if (screen.orientation && screen.orientation.unlock) {\n screen.orientation.unlock();\n }\n \n \/\/ 移除设备方向监听\n if (lockLandscapeOrientation.currentListener) {\n window.removeEventListener('deviceorientation', lockLandscapeOrientation.currentListener);\n lockLandscapeOrientation.currentListener = null;\n }\n}\n\n\/\/ 检测视频方向\nfunction checkVideoOrientation(player) {\n const video = player.media;\n if (video.videoWidth && video.videoHeight) {\n const aspectRatio = video.videoWidth \/ video.videoHeight;\n return aspectRatio > 1; \/\/ 大于1为横屏,小于1为竖屏\n }\n return false;\n}\n\n\/\/ 初始化播放器方向控制\nfunction initPlayerOrientationControl(player) {\n let isLandscapeVideo = false;\n \n \/\/ 视频元数据加载时检测视频方向\n player.on('loadedmetadata', () => {\n isLandscapeVideo = checkVideoOrientation(player);\n console.log('视频方向:', isLandscapeVideo ? '横屏' : '竖屏');\n });\n \n \/\/ 进入全屏时根据视频方向锁定屏幕\n player.on('enterfullscreen', () => {\n isLandscapeVideo = checkVideoOrientation(player);\n console.log('进入全屏,视频方向:', isLandscapeVideo ? '横屏' : '竖屏');\n \n \/\/ 立即锁定方向,不等待\n if (isLandscapeVideo) {\n lockLandscapeOrientation();\n } else {\n lockPortraitOrientation();\n }\n });\n \n \/\/ 退出全屏时解锁方向\n player.on('exitfullscreen', () => {\n console.log('退出全屏,解锁方向');\n unlockOrientation();\n });\n}\n\n\/\/ 添加顶部集数显示(上移动画版)\nfunction addEpisodeBarToPlayer() {\n \/\/ 添加顶部集数显示条\n const episodeBar = document.createElement('div');\n episodeBar.className = 'plyr__episode-bar';\n episodeBar.style.transform = 'translateY(0)';\n episodeBar.style.opacity = '1';\n \n \/\/ 创建标题元素\n const titleElement = document.createElement('div');\n titleElement.className = 'plyr__episode-title';\n \n \/\/ 初始化集数标题\n const updateEpisodeTitle = () => {\n const activeButton = document.querySelector('.jishu button.active');\n if (activeButton) {\n titleElement.textContent = activeButton.textContent.trim();\n }\n };\n \n \/\/ 初始设置标题\n updateEpisodeTitle();\n episodeBar.appendChild(titleElement);\n document.querySelector('.plyr__video-wrapper').appendChild(episodeBar);\n\n \/\/ 同步播放器控件状态(顶部条上移,底部条下移)\n const syncWithControls = () => {\n const controls = player.elements.controls;\n const controlsStyle = window.getComputedStyle(controls);\n \n \/\/ 根据底部控制栏状态决定顶部条状态\n if (controlsStyle.transform === 'none' || controlsStyle.opacity === '1') {\n \/\/ 控制栏显示时:顶部条下滑进入\n episodeBar.style.transform = 'translateY(0)';\n episodeBar.style.opacity = '1';\n } else {\n \/\/ 控制栏隐藏时:顶部条上滑隐藏\n episodeBar.style.transform = 'translateY(-100%)';\n episodeBar.style.opacity = '0';\n }\n };\n\n \/\/ 使用Plyr原生事件同步状态\n player.on('controlsshown', () => {\n episodeBar.style.transform = 'translateY(0)';\n episodeBar.style.opacity = '1';\n });\n \n player.on('controlshidden', () => {\n episodeBar.style.transform = 'translateY(-100%)';\n episodeBar.style.opacity = '0';\n });\n\n \/\/ 监听集数切换事件\n const observeEpisodeChange = () => {\n const activeButtonObserver = new MutationObserver(() => {\n updateEpisodeTitle();\n \/\/ 切换集数时短暂显示\n \/\/episodeBar.style.transform = 'translateY(0)';\n \/\/episodeBar.style.opacity = '1';\n \/\/setTimeout(syncWithControls, 500);\n });\n\n const buttons = document.querySelectorAll('.jishu button');\n buttons.forEach(button => {\n activeButtonObserver.observe(button, { attributes: true });\n });\n };\n\n \/\/ 全屏适配\n player.on('enterfullscreen exitfullscreen', () => {\n if (player.fullscreen.active) {\n episodeBar.style.height = '60px';\n episodeBar.querySelector('.plyr__episode-title').style.fontSize = '18px';\n } else {\n episodeBar.style.height = '40px';\n episodeBar.querySelector('.plyr__episode-title').style.fontSize = '14px';\n }\n });\n\n \/\/ 初始同步\n setTimeout(() => {\n \/\/ 绑定与Plyr相同的鼠标事件\n const container = player.elements.container;\n \n container.addEventListener('mousemove', () => {\n episodeBar.style.transform = 'translateY(0)';\n episodeBar.style.opacity = '1';\n });\n \n container.addEventListener('mouseleave', syncWithControls);\n \n syncWithControls();\n observeEpisodeChange();\n }, 50);\n \n \/\/ 添加样式\n const style = document.createElement('style');\n style.textContent = `\n .plyr__episode-bar {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 40px;\n background: linear-gradient(\n to bottom, \n rgba(0,0,0,0.8) 0%,\n rgba(0,0,0,0.5) 30%,\n rgba(0,0,0,0) 100%\n );\n color: white;\n display: flex;\n align-items: center;\n padding-left: 15px;\n z-index: 3;\n transform: translateY(-100%);\n opacity: 0;\n transition: all 0.4s ease-in-out;\n pointer-events: none;\n }\n .plyr__episode-title {\n font-size: 14px;\n text-shadow: 0 0 5px rgba(0,0,0,0.5);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n width: 75%;\n display: block; \n }\n .plyr--fullscreen .plyr__episode-bar {\n height: 60px;\n padding-left: 20px;\n }\n .plyr--fullscreen .plyr__episode-title {\n font-size: 18px;\n }\n `;\n document.head.appendChild(style);\n}\n\n\/\/ 添加跳过菜单项\nfunction addSkipMenuItems(currentVideoUrl, timeManager) {\n \/\/ 等待设置菜单加载完成\n setTimeout(() => {\n const settingsMenu = document.querySelector('.plyr__menu__container');\n if (!settingsMenu) return;\n\n \/\/ 创建跳过菜单项\n const skipMenuItem = document.createElement('div');\n skipMenuItem.className = 'plyr__skip-menu-item';\n skipMenuItem.style.margin = '0 8px';\n skipMenuItem.style.boxShadow = `\n 0 -1px 0 #ccc, \n 0 -2px 0 var(--plyr-menu-back-border-shadow-color, #fff)\n `;\n skipMenuItem.innerHTML = `\n <span style=\"display: block; text-align: center; font-size: 15px; margin: 8px;\">标记片头\/片尾<\/span>\n `;\n skipMenuItem.setAttribute('role', 'menuitem');\n\n \/\/ 创建子菜单容器\n const submenuContainer = document.createElement('div');\n submenuContainer.className = 'plyr__skip-submenu';\n submenuContainer.setAttribute('hidden', '');\n submenuContainer.setAttribute('role', 'menu');\n \n \/\/ 创建返回项\n const backItem = document.createElement('div');\n backItem.style.margin = '4px 0';\n backItem.style.boxShadow = `\n 0 1px 0 var(--plyr-menu-back-border-shadow-color, #fff), \n 0 2px 0 #ccc\n `;\n backItem.innerHTML = `\n <span style=\"display: block; text-align: center; height: 32px; font-size: 15px; margin: 4px 10px;\">返回上级菜单<\/span>\n `;\n backItem.setAttribute('role', 'menuitem');\n\n \/\/ 实时获取标记时间\n const getMarkTimes = () => ({\n head: localStorage.getItem('head' + currentVideoUrl),\n end: localStorage.getItem('end' + currentVideoUrl)\n });\n\n \/\/ 创建子菜单项\n const submenuItems = `\n <div style=\"display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; margin: 2px 4px;\">\n <div style=\"display: flex; align-items: center;\">\n <div style=\"font-size: 14px;\" data-action=\"mark-start\" role=\"menuitem\">\n ${getMarkTimes().head ? '已设片头' : '标记片头'}\n <\/div>\n <div style=\"font-size: 14px; color: #999; margin-left: 10px;\" class=\"head-time-display\">\n ${getMarkTimes().head ? formatTime(getMarkTimes().head) : '无'}\n <\/div>\n <\/div>\n <\/div>\n \n <div style=\"display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; margin: 2px 4px;\">\n <div style=\"display: flex; align-items: center;\">\n <div style=\"font-size: 14px;\" data-action=\"mark-end\" role=\"menuitem\">\n ${getMarkTimes().end ? '已设片尾' : '标记片尾'}\n <\/div>\n <div style=\"font-size: 14px; color: #999; margin-left: 10px;\" class=\"end-time-display\">\n ${getMarkTimes().end ? formatTime(getMarkTimes().end) : '无'}\n <\/div>\n <\/div>\n <\/div>\n \n <div style=\"display: flex; justify-content: center; padding: 4px 8px; margin: 2px 0 0 0;\">\n <div style=\"font-size: 14px;\" data-action=\"clear-marks\" role=\"menuitem\">\n 清除全部标记\n <\/div>\n <\/div>\n `;\n\n submenuContainer.appendChild(backItem);\n submenuContainer.insertAdjacentHTML('beforeend', submenuItems);\n\n \/\/ 添加到DOM - 插入到菜单最后面\n settingsMenu.appendChild(skipMenuItem);\n settingsMenu.appendChild(submenuContainer);\n\n \/\/ 存储原始菜单项\n const originalMenuItems = Array.from(settingsMenu.children).filter(\n child => !child.classList.contains('plyr__skip-menu-item') && \n !child.classList.contains('plyr__skip-submenu')\n );\n\n \/\/ 更新时间显示函数\n function updateTimeDisplay() {\n const { head, end } = getMarkTimes();\n const headDisplay = submenuContainer.querySelector('.head-time-display');\n const endDisplay = submenuContainer.querySelector('.end-time-display');\n const markStartBtn = submenuContainer.querySelector('[data-action=\"mark-start\"]');\n const markEndBtn = submenuContainer.querySelector('[data-action=\"mark-end\"]');\n \n if (headDisplay) {\n headDisplay.textContent = head ? formatTime(head) : '无';\n headDisplay.style.color = head ? '#4CAF50' : '#999';\n markStartBtn.textContent = head ? '已设片头' : '标记片头';\n }\n \n if (endDisplay) {\n endDisplay.textContent = end ? formatTime(end) : '无';\n endDisplay.style.color = end ? '#FF5722' : '#999';\n markEndBtn.textContent = end ? '已设片尾' : '标记片尾';\n }\n }\n\n \/\/ 添加事件监听\n skipMenuItem.addEventListener('click', (e) => {\n e.stopPropagation();\n updateTimeDisplay();\n originalMenuItems.forEach(item => item.style.display = 'none');\n skipMenuItem.style.display = 'none';\n submenuContainer.removeAttribute('hidden');\n });\n\n \/\/ 返回按钮点击事件\n backItem.addEventListener('click', () => {\n submenuContainer.setAttribute('hidden', '');\n originalMenuItems.forEach(item => item.style.display = '');\n skipMenuItem.style.display = '';\n });\n\n \/\/ 标记片头点击事件\n submenuContainer.querySelector('[data-action=\"mark-start\"]').addEventListener('click', () => {\n let headtime = getMarkTimes().head;\n timeManager.setHeadTime(player.currentTime);\n showTemporaryMessage(`已${headtime ? '重设' : '标记'}片头: ${formatTime(timeManager.headtime)}`);\n });\n\n \/\/ 标记片尾点击事件\n submenuContainer.querySelector('[data-action=\"mark-end\"]').addEventListener('click', () => {\n let endtime = getMarkTimes().end;\n timeManager.setEndTime(player.duration - player.currentTime);\n showTemporaryMessage(`已${endtime ? '重设' : '标记'}片尾: ${formatTime(timeManager.endtime)}`);\n });\n\n \/\/ 清除标记点击事件\n submenuContainer.querySelector('[data-action=\"clear-marks\"]').addEventListener('click', () => {\n timeManager.clearMarks();\n showTemporaryMessage('已清除全部标记');\n });\n\n \/\/ 添加时间更新监听\n timeManager.addListener(({ type }) => {\n updateTimeDisplay();\n \n \/\/ 如果是清除操作,确保UI同步更新\n if (type === 'clear') {\n const markStartBtn = submenuContainer.querySelector('[data-action=\"mark-start\"]');\n const markEndBtn = submenuContainer.querySelector('[data-action=\"mark-end\"]');\n if (markStartBtn) markStartBtn.textContent = '标记片头';\n if (markEndBtn) markEndBtn.textContent = '标记片尾';\n }\n });\n\n \/\/ 显示临时消息\n function showTemporaryMessage(message) {\n const messageEl = document.createElement('div');\n messageEl.className = 'plyr__skip-message';\n messageEl.textContent = message;\n document.querySelector('.plyr').appendChild(messageEl);\n \n setTimeout(() => {\n messageEl.classList.add('fade-out');\n setTimeout(() => messageEl.remove(), 300);\n }, 2000);\n }\n\n \/\/ 添加样式\n if (!document.querySelector('#plyrSkipMessageStyle')) {\n const style = document.createElement('style');\n style.id = 'plyrSkipMessageStyle';\n style.textContent = `\n .plyr__skip-message {\n position: fixed;\n top: 30px;\n left: 15px;\n transform: none;\n background-color: rgba(0, 0, 0, 0.5);\n color: white;\n padding: 10px 20px;\n border-radius: 4px;\n z-index: 1000;\n text-align: left;\n font-size: 14px;\n transition: opacity 0.3s ease;\n }\n .plyr__skip-message.fade-out {\n opacity: 0;\n }\n .plyr__skip-menu-item {\n cursor: pointer;\n }\n .plyr__skip-menu-item:hover {\n background-color: rgba(255,255,255,0.1);\n }\n `;\n document.head.appendChild(style);\n }\n\n \/\/ 初始显示菜单项\n skipMenuItem.style.display = '';\n\n \/\/ 监听显示菜单项\n function observeMainMenuState() {\n const observer = new MutationObserver((mutations) => {\n mutations.forEach((mutation) => {\n const homeMenu = document.querySelector('[id^=\"plyr-settings-\"][id$=\"-home\"]');\n const subMenu = document.querySelector('.plyr__skip-submenu');\n if (homeMenu && subMenu) {\n const homeIsHidden = homeMenu.hasAttribute('hidden');\n const subIsHidden = subMenu.hasAttribute('hidden');\n skipMenuItem.style.display = homeIsHidden || !subIsHidden ? 'none' : '';\n }\n });\n });\n\n \/\/ 监听整个 document(因为菜单可能动态加载)\n observer.observe(document.body, {\n childList: true, \/\/ 监听子元素增删\n subtree: true, \/\/ 监听所有后代元素\n attributes: true, \/\/ 监听属性变化\n attributeFilter: ['hidden', 'id'] \/\/ 只监听 hidden 和 id 变化\n });\n\n \/\/ 初始检查\n const initialHomeMenu = document.querySelector('[id^=\"plyr-settings-\"][id$=\"-home\"]');\n const initialSubMenu = document.querySelector('.plyr__skip-submenu');\n if (initialHomeMenu && initialSubMenu) {\n const homeIsHidden = initialHomeMenu.hasAttribute('hidden');\n const subIsHidden = initialSubMenu.hasAttribute('hidden');\n skipMenuItem.style.display = homeIsHidden || !subIsHidden ? 'none' : '';\n }\n }\n\n \/\/ 启动监听\n observeMainMenuState();\n }, 500);\n}\n\n\/\/ 格式化时间显示\nfunction formatTime(seconds) {\n seconds = parseFloat(seconds);\n const date = new Date(0);\n date.setSeconds(seconds);\n const timeString = date.toISOString().substr(11, 8);\n return timeString.startsWith('00:') ? timeString.substr(3) : timeString;\n}\n\n\/\/ HLS播放-检测网络状况并自适应调整\nfunction getOptimalHlsConfig() {\n const connection = navigator.connection || {};\n const downlink = connection.downlink || 5;\n \n if (downlink > 10) {\n return adaptiveConfigManager.getHighSpeedConfig();\n } else if (downlink > 5) {\n return adaptiveConfigManager.getMediumSpeedConfig();\n } else {\n return adaptiveConfigManager.getLowSpeedConfig();\n }\n}\n\n\/\/ 自适应配置管理器\nconst adaptiveConfigManager = {\n performanceMetrics: {\n loadTimes: [],\n bufferingEvents: 0,\n averageLoadTime: 0,\n lastUpdateTime: 0\n },\n \n \/\/ 配置模板\n configTemplates: {\n highSpeed: {\n \/\/ 缓冲区设置\n maxBufferSize: 800 * 1024 * 1024, \/\/ 800MB\n maxBufferLength: 600, \/\/ 10分钟缓冲区\n maxMaxBufferLength: 1200, \/\/ 20分钟最大缓冲区\n backBufferLength: 300, \/\/ 5分钟后缓冲区\n \n \/\/ 预加载优化\n preloadTime: 90, \/\/ 预加载90秒\n initialLiveManifestSize: 5,\n \n \/\/ 网络优化\n maxLoadingDelay: 1,\n maxSeekHole: 0.5,\n maxFragLookUpTolerance: 0.05,\n highBufferWatchdogPeriod: 0.5,\n nudgeOffset: 0.02,\n nudgeMaxRetry: 15,\n maxStarvationDelay: 1\n },\n mediumSpeed: {\n \/\/ 缓冲区设置\n maxBufferSize: 600 * 1024 * 1024, \/\/ 600MB\n maxBufferLength: 450, \/\/ 7.5分钟缓冲区\n maxMaxBufferLength: 900, \/\/ 15分钟最大缓冲区\n backBufferLength: 240, \/\/ 4分钟后缓冲区\n \n \/\/ 预加载优化\n preloadTime: 75, \/\/ 预加载75秒\n initialLiveManifestSize: 4,\n \n \/\/ 网络优化\n maxLoadingDelay: 1.5,\n maxSeekHole: 1,\n maxFragLookUpTolerance: 0.08,\n highBufferWatchdogPeriod: 1,\n nudgeOffset: 0.03,\n nudgeMaxRetry: 12,\n maxStarvationDelay: 2\n },\n lowSpeed: {\n \/\/ 缓冲区设置\n maxBufferSize: 500 * 1024 * 1024, \/\/ 500MB\n maxBufferLength: 360, \/\/ 6分钟缓冲区\n maxMaxBufferLength: 720, \/\/ 12分钟最大缓冲区\n backBufferLength: 180, \/\/ 3分钟后缓冲区\n \n \/\/ 预加载优化\n preloadTime: 60, \/\/ 预加载60秒\n initialLiveManifestSize: 3,\n \n \/\/ 网络优化\n maxLoadingDelay: 2,\n maxSeekHole: 1.5,\n maxFragLookUpTolerance: 0.1,\n highBufferWatchdogPeriod: 1.5,\n nudgeOffset: 0.05,\n nudgeMaxRetry: 15,\n maxStarvationDelay: 3\n }\n },\n \n \/\/ 根据性能指标调整配置\n adaptConfigBasedOnPerformance() {\n const metrics = this.performanceMetrics;\n \n \/\/ 计算平均加载时间\n if (metrics.loadTimes.length > 0) {\n metrics.averageLoadTime = metrics.loadTimes.reduce((a, b) => a + b) \/ metrics.loadTimes.length;\n }\n\n \/\/ 基于性能指标选择配置\n if (metrics.averageLoadTime < 2000 && metrics.bufferingEvents < 2) {\n console.log('加载状况良好,使用高速配置');\n return this.getHighSpeedConfig();\n } else if (metrics.averageLoadTime < 5000 && metrics.bufferingEvents < 5) {\n console.log('加载状况中等,使用平衡配置');\n return this.getMediumSpeedConfig();\n } else {\n console.log('加载状况较差,使用保守配置');\n return this.getLowSpeedConfig();\n }\n },\n \n getHighSpeedConfig() {\n return { ...this.configTemplates.highSpeed };\n },\n \n getMediumSpeedConfig() {\n return { ...this.configTemplates.mediumSpeed };\n },\n \n getLowSpeedConfig() {\n return { ...this.configTemplates.lowSpeed };\n },\n \n \/\/ 应用配置到HLS实例\n applyConfigToHls(hlsInstance, config) {\n Object.keys(config).forEach(key => {\n if (hlsInstance.hasOwnProperty(key)) {\n hlsInstance[key] = config[key];\n }\n });\n },\n \n \/\/ 动态调整配置\n adaptHlsConfig(hlsInstance) {\n if (this.performanceMetrics.loadTimes.length >= 3) {\n const newConfig = this.adaptConfigBasedOnPerformance();\n this.applyConfigToHls(hlsInstance, newConfig);\n console.log('已根据加载性能动态调整HLS配置');\n }\n },\n \n \/\/ 记录片段加载时间\n recordFragmentLoadTime(duration) {\n this.performanceMetrics.loadTimes.push(duration);\n \n \/\/ 只保留最近10个记录\n if (this.performanceMetrics.loadTimes.length > 10) {\n this.performanceMetrics.loadTimes.shift();\n }\n },\n \n \/\/ 记录缓冲事件\n recordBufferingEvent() {\n this.performanceMetrics.bufferingEvents++;\n },\n \n \/\/ 重置性能指标\n resetMetrics() {\n this.performanceMetrics.loadTimes = [];\n this.performanceMetrics.bufferingEvents = 0;\n this.performanceMetrics.averageLoadTime = 0;\n }\n};\n\n\/\/ HLS播放\nfunction initializeHlsPlayer(source, headersOrReferer = null, video, currentTime, callback) {\n if (currentHls) {\n currentHls.stopLoad();\n currentHls.detachMedia();\n currentHls.destroy();\n }\n\n \/\/ 处理参数兼容性\n let headers = null;\n if (headersOrReferer) {\n if (typeof headersOrReferer === 'string') {\n headers = { 'Referer': headersOrReferer };\n } else {\n headers = headersOrReferer;\n }\n }\n\n \/\/ 广告拦截配置\n const adBlockConfig = {\n enabled: true, \/\/ 设为false来禁用广告拦截\n \/\/ 广告片段识别关键词\n adKeywords: ['\/ad\/', 'ad_', '_ad', 'commercial', 'midroll', 'preroll', 'postroll'],\n \/\/ 处理方式:'skip'=跳过,'empty'=返回空数据\n strategy: 'skip'\n };\n\n \/\/ 获取初始配置\n const initialConfig = getOptimalHlsConfig();\n \n currentHls = new Hls({\n ...initialConfig,\n \/\/ 通用优化设置\n enableWorker: true,\n enableSoftwareAES: true,\n stretchShortVideoTrack: true,\n forceKeyFrameOnDiscontinuity: true,\n optimizeAudioOnly: false,\n lowLatencyMode: false,\n \n \/\/ 使用 xhrSetup\n xhrSetup: function(xhr, url) {\n const startTime = performance.now();\n const connection = navigator.connection || {};\n const downlink = connection.downlink || 5;\n const timeout = downlink > 10 ? 90000 : downlink > 5 ? 60000 : 30000;\n \n \/\/ 设置超时\n xhr.timeout = timeout;\n \n \/\/ 设置响应类型\n xhr.responseType = 'arraybuffer';\n \n \/\/ 检查是否为广告片段\n if (adBlockConfig.enabled && isAdSegment(url)) {\n return handleAdRequest(xhr, url);\n }\n \n \/\/ 添加自定义 headers\n if (headers) {\n Object.keys(headers).forEach(key => {\n xhr.setRequestHeader(key, headers[key]);\n });\n }\n \n \/\/ 监听加载完成事件\n const originalSend = xhr.send;\n xhr.send = function(body) {\n this.addEventListener('loadend', function() {\n if (this.status >= 200 && this.status < 300) {\n const loadTime = performance.now() - startTime;\n adaptiveConfigManager.recordFragmentLoadTime(loadTime);\n \n \/\/ 如果加载时间过长,动态调整配置\n if (loadTime > 5000) {\n adaptiveConfigManager.adaptHlsConfig(currentHls);\n }\n }\n });\n originalSend.call(this, body);\n };\n \n \/\/ 监听超时事件\n xhr.addEventListener('timeout', function() {\n console.warn('XHR request timed out after', timeout, 'ms');\n });\n \n \/\/ 监听错误事件\n xhr.addEventListener('error', function() {\n console.error('XHR request failed');\n });\n }\n });\n\n \/\/ 广告检测函数\n function isAdSegment(url) {\n if (!adBlockConfig.enabled) return false;\n \n const lowerUrl = url.toLowerCase();\n \n \/\/ 检查URL是否包含广告关键词\n for (const keyword of adBlockConfig.adKeywords) {\n if (lowerUrl.includes(keyword.toLowerCase())) {\n return true;\n }\n }\n \n return false;\n }\n\n \/\/ 处理广告请求\n function handleAdRequest(xhr, url) {\n console.log('拦截广告片段:', url.substring(0, 100));\n \n switch (adBlockConfig.strategy) {\n case 'skip':\n \/\/ 返回空数据,让HLS跳过这个片段\n return createMockResponse(xhr);\n \n case 'empty':\n \/\/ 返回一个小型的合法视频片段(避免解码错误)\n return createEmptySegmentResponse(xhr);\n \n default:\n return createMockResponse(xhr);\n }\n }\n\n \/\/ 创建模拟响应(跳过广告)\n function createMockResponse(xhr) {\n \/\/ 设置XHR状态为已完成\n Object.defineProperties(xhr, {\n 'readyState': { value: 4, writable: false },\n 'status': { value: 200, writable: false },\n 'response': { value: new ArrayBuffer(0), writable: false },\n 'responseText': { value: '', writable: false }\n });\n \n \/\/ 异步触发事件\n setTimeout(() => {\n \/\/ 触发readyStateChange\n if (xhr.onreadystatechange) {\n xhr.onreadystatechange.call(xhr);\n }\n \n \/\/ 触发load事件\n if (xhr.onload) {\n xhr.onload.call(xhr);\n }\n \n \/\/ 触发原生事件\n const events = ['readystatechange', 'load', 'loadend'];\n events.forEach(eventName => {\n try {\n xhr.dispatchEvent(new Event(eventName));\n } catch (e) {\n \/\/ 忽略事件分发错误\n }\n });\n }, 0);\n \n \/\/ 阻止实际网络请求\n xhr.send = function() {\n \/\/ 什么都不做,已经模拟了响应\n };\n \n return true;\n }\n\n \/\/ 创建空片段响应(更稳定的方案)\n function createEmptySegmentResponse(xhr) {\n \/\/ 创建一个非常小的视频片段(避免解码器错误)\n \/\/ 这是一个H.264 NAL unit的简单示例(空片段)\n const emptySegment = new Uint8Array([\n 0x00, 0x00, 0x00, 0x01, \/\/ NALU start code\n 0x09, 0xF0, \/\/ NALU type: Access Unit Delimiter\n 0x00, 0x00, 0x00, 0x01, \/\/ NALU start code\n 0x06, 0x05 \/\/ NALU type: SEI\n ]);\n \n Object.defineProperties(xhr, {\n 'readyState': { value: 4, writable: false },\n 'status': { value: 200, writable: false },\n 'response': { value: emptySegment.buffer, writable: false }\n });\n \n setTimeout(() => {\n if (xhr.onload) xhr.onload.call(xhr);\n try {\n xhr.dispatchEvent(new Event('load'));\n } catch (e) {\n \/\/ 忽略错误\n }\n }, 10);\n \n xhr.send = function() {\n \/\/ 阻止实际发送\n };\n \n return true;\n }\n\n \/\/ 监听HLS事件来优化广告检测\n currentHls.on(Hls.Events.FRAG_LOADING, (event, data) => {\n const frag = data.frag;\n \/\/ 可以在片段加载时进行额外检测\n if (frag.duration < 10 && frag.duration > 0) {\n \/\/ 短片段可能是广告,但这里只是记录不处理\n frag._isPotentialAd = true;\n }\n });\n\n currentHls.loadSource(source);\n currentHls.attachMedia(video);\n\n \/\/ 监听缓冲事件\n currentHls.on(Hls.Events.ERROR, (event, data) => {\n if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {\n adaptiveConfigManager.recordBufferingEvent();\n \n \/\/ 如果有多次缓冲事件,立即调整配置\n if (adaptiveConfigManager.performanceMetrics.bufferingEvents >= 3) {\n adaptiveConfigManager.adaptHlsConfig(currentHls);\n }\n }\n \n \/\/ 处理广告拦截可能引起的错误\n if (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR || \n data.details === Hls.ErrorDetails.FRAG_LOAD_TIMEOUT) {\n if (data.frag && isAdSegment(data.frag.url)) {\n console.log('广告片段加载错误已忽略');\n data.recovered = true;\n }\n }\n });\n\n currentHls.on(Hls.Events.MANIFEST_PARSED, () => {\n if (currentTime > 0) {\n video.currentTime = parseFloat(currentTime);\n $(\".video-container\")[0].style.background = '#000';\n }\n if (callback) callback();\n });\n\n \/\/ 定期检查性能并调整配置(每30秒)\n const performanceCheckInterval = setInterval(() => {\n if (adaptiveConfigManager.performanceMetrics.loadTimes.length >= 5) {\n adaptiveConfigManager.adaptHlsConfig(currentHls);\n }\n }, 30000);\n\n \/\/ 清理interval当HLS实例被销毁时\n currentHls.on(Hls.Events.DESTROYING, () => {\n clearInterval(performanceCheckInterval);\n });\n}\n\n\/\/ 可选:提供一个函数来启用\/禁用广告拦截\nfunction setAdBlockEnabled(enabled) {\n if (window.initializeHlsPlayer && window.initializeHlsPlayer.adBlockConfig) {\n window.initializeHlsPlayer.adBlockConfig.enabled = enabled;\n }\n}\n\n\/\/ 可选:添加广告关键词\nfunction addAdKeyword(keyword) {\n if (window.initializeHlsPlayer && window.initializeHlsPlayer.adBlockConfig) {\n if (!window.initializeHlsPlayer.adBlockConfig.adKeywords.includes(keyword)) {\n window.initializeHlsPlayer.adBlockConfig.adKeywords.push(keyword);\n }\n }\n}\n\n\/\/ 普通播放\nfunction initializeRegularPlayer(source, headersOrReferer = null, video, currentTime, callback) {\n video.src = '';\n video.load();\n video.preload = \"auto\";\n\n \/\/ 处理参数兼容性\n let headers = null;\n if (headersOrReferer) {\n if (typeof headersOrReferer === 'string') {\n headers = { 'Referer': headersOrReferer };\n } else {\n headers = headersOrReferer;\n }\n }\n\n if (headers) {\n const xhr = new XMLHttpRequest();\n xhr.open('HEAD', source, true);\n \n Object.keys(headers).forEach(key => {\n xhr.setRequestHeader(key, headers[key]);\n });\n \n xhr.withCredentials = true;\n xhr.timeout = 10000;\n \n xhr.onload = function() {\n const acceptRanges = xhr.getResponseHeader('Accept-Ranges');\n const contentLength = xhr.getResponseHeader('Content-Length');\n \n console.log('服务器响应:', {\n acceptRanges: acceptRanges,\n contentLength: contentLength,\n status: xhr.status\n });\n \n if (acceptRanges === 'bytes' && contentLength) {\n console.log('服务器支持range请求,使用MediaSource播放');\n setVideoSourceWithHeaders();\n } else {\n console.log('服务器不支持range请求,直接加载');\n directLoadVideo();\n }\n };\n \n xhr.onerror = function() {\n console.error('HEAD请求失败');\n directLoadVideo();\n };\n \n xhr.ontimeout = function() {\n console.warn('HEAD请求超时');\n directLoadVideo();\n };\n \n try {\n xhr.send();\n } catch (error) {\n console.error('发送HEAD请求异常:', error);\n directLoadVideo();\n }\n } else {\n directLoadVideo();\n }\n\n function setVideoSourceWithHeaders() {\n if ('MediaSource' in window && MediaSource.isTypeSupported('video\/mp4')) {\n console.log('使用MediaSource API');\n const mediaSource = new MediaSource();\n video.src = URL.createObjectURL(mediaSource);\n \n mediaSource.addEventListener('sourceopen', function() {\n const mimeCodec = 'video\/mp4; codecs=\"avc1.42E01E,mp4a.40.2\"';\n const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);\n \n const xhr = new XMLHttpRequest();\n xhr.open('GET', source, true);\n xhr.responseType = 'arraybuffer';\n \n Object.keys(headers).forEach(key => {\n xhr.setRequestHeader(key, headers[key]);\n });\n \n xhr.onload = function() {\n if (xhr.status >= 200 && xhr.status < 300) {\n console.log('视频数据加载完成,大小:', (xhr.response.byteLength \/ 1024 \/ 1024).toFixed(2), 'MB');\n sourceBuffer.appendBuffer(xhr.response);\n sourceBuffer.addEventListener('updateend', function() {\n if (!sourceBuffer.updating) {\n mediaSource.endOfStream();\n handleVideoReady();\n }\n });\n } else {\n console.error('视频数据请求失败,状态码:', xhr.status);\n directLoadVideo();\n }\n };\n \n xhr.onerror = function() {\n console.error('视频数据请求网络错误');\n directLoadVideo();\n };\n \n xhr.send();\n });\n } else {\n console.log('浏览器不支持MediaSource,直接设置src');\n video.src = source;\n video.addEventListener('loadedmetadata', function() {\n handleVideoReady();\n }, { once: true });\n video.load();\n }\n }\n\n function directLoadVideo() {\n video.src = source;\n video.load();\n handleVideoReady();\n }\n\n function handleVideoReady() {\n const onLoaded = () => {\n if (currentTime > 0) {\n video.currentTime = parseFloat(currentTime);\n $(\".video-container\")[0].style.background = '#000';\n }\n if (callback) callback();\n };\n\n if (video.readyState >= 1) {\n setTimeout(onLoaded, 0);\n } else {\n video.addEventListener('loadedmetadata', onLoaded, { once: true });\n }\n }\n}\n\n\/\/ 上一集函数\nfunction switchToPrevEpisode(currentVideoUrl, autonext) {\n if (isSwitching || !autonext) return;\n\n isSwitching = true;\n localStorage.removeItem(currentVideoUrl);\n const currentButton = $('.jishu button.active')[0];\n const prevButton = currentButton?.previousElementSibling;\n\n if (prevButton && prevButton.innerText !== \"\" && !localStorage.getItem(currentVideoUrl)) {\n prevButton.click();\n } else {\n weblog('已经是第一集了!');\n }\n}\n\n\/\/ 下一集函数\nfunction switchToNextEpisode(currentVideoUrl, autonext) {\n if (isSwitching || !autonext) return;\n\n isSwitching = true;\n localStorage.removeItem(currentVideoUrl);\n const currentButton = $('.jishu button.active')[0];\n const nextButton = currentButton?.nextElementSibling;\n\n if (nextButton && nextButton.innerText !== \"\") {\n nextButton.click();\n } else {\n weblog('没有下一集了!');\n }\n}\n\n\/\/ 切换视频质量的函数\nasync function changeVideoQuality(quality, sources) {\n const video = $('video')[0];\n const selectedSource = sources.find(source => source.size === quality.toString());\n const currentTime = video.currentTime;\n const wasPlaying = !video.paused;\n\n if (wasPlaying) {\n $(\".video-container\")[0].style.background = '#000';\n }\n $(\"body>p\")[0].innerHTML = `\n <div style=\"display: flex; align-items: center; justify-content: space-between; width: 100%;\">\n <a href=\"legadovideo:\/\/${encodeURIComponent(selectedSource.src)}\">\n ${selectedSource.src}\n <\/a>\n <button \n onclick=\"navigator.clipboard.writeText('${selectedSource.src.replace(\/'\/g, \"\\\\'\")}')\n .then(() => weblog('链接已复制到剪贴板'))\n .catch(() => weblog('复制链接失败'))\"\n >\n 复制\n <\/button>\n <\/div>\n `;\n\n if (selectedSource) {\n if (Hls.isSupported() && \/m3u8|hls\/.test(selectedSource.src)) {\n initializeHlsPlayer(selectedSource.src, selectedSource.headersOrReferer, video, currentTime, () => {\n if (wasPlaying) {\n video.play();\n }\n });\n } else {\n initializeRegularPlayer(selectedSource.src, selectedSource.headersOrReferer, video, currentTime, () => {\n if (wasPlaying) {\n video.play();\n }\n });\n }\n }\n}\n\n\/\/ 页面信息高度调整\nfunction updatePadding() {\n var videoheight = $(\"video\")[0].offsetHeight;\n var h3height = $(\"h3\")[0].offsetHeight;\n var selectedButton = $('#selected-jiekou > button')[0];\n $(\"img\")[0].style.top = `calc(16px + ${videoheight}px + ${h3height}px)`;\n $(\".all-info\")[0].style.paddingTop = `calc(20px + ${videoheight}px + ${h3height}px)`;\n if ($('#selected-jiekou')[0] && $('#jiekou-list')[0]) {\n $('#selected-jiekou')[0].style.top = `calc(16px + ${videoheight}px + ${h3height}px)`;\n $('#jiekou-list')[0].style.top = `calc(15.9px + ${videoheight}px + ${h3height}px + ${selectedButton.offsetHeight}px)`;\n $('#jiekou-list')[0].style.width = `${selectedButton.offsetWidth - 1.9}px`;\n }\n}\nwindow.addEventListener('resize', updatePadding);\n\n\/\/ SVG图标JS动态注入\ndocument.write(`\n<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"display:none\">\n <symbol id=\"plyr-next-episode\" viewBox=\"0 0 18 18\">\n <path d=\"M4 1v16l10-8z\" stroke-width=\"0.5\"\/>\n <rect x=\"14\" y=\"1\" width=\"4\" height=\"16\" rx=\"1\" stroke-width=\"0.5\"\/>\n <\/symbol>\n <symbol id=\"plyr-prev-episode\" viewBox=\"0 0 18 18\">\n <path d=\"M14 1v16L4 9z\" stroke-width=\"0.5\"\/>\n <rect x=\"0\" y=\"1\" width=\"4\" height=\"16\" rx=\"1\" stroke-width=\"0.5\"\/>\n <\/symbol>\n<\/svg>\n`);\n\ndocument.write(`\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE svg PUBLIC \"-\/\/W3C\/\/DTD SVG 1.1\/\/EN\" \"http:\/\/www.w3.org\/Graphics\/SVG\/1.1\/DTD\/svg11.dtd\">\n<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" xmlns:xlink=\"http:\/\/www.w3.org\/1999\/xlink\" style=\"display: none;\">\n <symbol id=\"plyr-airplay\" viewBox=\"0 0 18 18\">\n <path d=\"M16 1H2a1 1 0 00-1 1v10a1 1 0 001 1h3v-2H3V3h12v8h-2v2h3a1 1 0 001-1V2a1 1 0 00-1-1z\"\/>\n <path d=\"M4 17h10l-5-6z\"\/>\n <\/symbol>\n <symbol id=\"plyr-captions-off\" viewBox=\"0 0 18 18\">\n <path d=\"M1 1c-.6 0-1 .4-1 1v11c0 .6.4 1 1 1h4.6l2.7 2.7c.2.2.4.3.7.3.3 0 .5-.1.7-.3l2.7-2.7H17c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1H1zm4.52 10.15c1.99 0 3.01-1.32 3.28-2.41l-1.29-.39c-.19.66-.78 1.45-1.99 1.45-1.14 0-2.2-.83-2.2-2.34 0-1.61 1.12-2.37 2.18-2.37 1.23 0 1.78.75 1.95 1.43l1.3-.41C8.47 4.96 7.46 3.76 5.5 3.76c-1.9 0-3.61 1.44-3.61 3.7 0 2.26 1.65 3.69 3.63 3.69zm7.57 0c1.99 0 3.01-1.32 3.28-2.41l-1.29-.39c-.19.66-.78 1.45-1.99 1.45-1.14 0-2.2-.83-2.2-2.34 0-1.61 1.12-2.37 2.18-2.37 1.23 0 1.78.75 1.95 1.43l1.3-.41c-.28-1.15-1.29-2.35-3.25-2.35-1.9 0-3.61 1.44-3.61 3.7 0 2.26 1.65 3.69 3.63 3.69z\" fill-rule=\"evenodd\" fill-opacity=\".5\"\/>\n <\/symbol>\n <symbol id=\"plyr-captions-on\" viewBox=\"0 0 18 18\">\n <path d=\"M1 1c-.6 0-1 .4-1 1v11c0 .6.4 1 1 1h4.6l2.7 2.7c.2.2.4.3.7.3.3 0 .5-.1.7-.3l2.7-2.7H17c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1H1zm4.52 10.15c1.99 0 3.01-1.32 3.28-2.41l-1.29-.39c-.19.66-.78 1.45-1.99 1.45-1.14 0-2.2-.83-2.2-2.34 0-1.61 1.12-2.37 2.18-2.37 1.23 0 1.78.75 1.95 1.43l1.3-.41C8.47 4.96 7.46 3.76 5.5 3.76c-1.9 0-3.61 1.44-3.61 3.7 0 2.26 1.65 3.69 3.63 3.69zm7.57 0c1.99 0 3.01-1.32 3.28-2.41l-1.29-.39c-.19.66-.78 1.45-1.99 1.45-1.14 0-2.2-.83-2.2-2.34 0-1.61 1.12-2.37 2.18-2.37 1.23 0 1.78.75 1.95 1.43l1.3-.41c-.28-1.15-1.29-2.35-3.25-2.35-1.9 0-3.61 1.44-3.61 3.7 0 2.26 1.65 3.69 3.63 3.69z\" fill-rule=\"evenodd\"\/>\n <\/symbol>\n <symbol id=\"plyr-download\" viewBox=\"0 0 18 18\">\n <path d=\"M9 13c.3 0 .5-.1.7-.3L15.4 7 14 5.6l-4 4V1H8v8.6l-4-4L2.6 7l5.7 5.7c.2.2.4.3.7.3zm-7 2h14v2H2z\"\/>\n <\/symbol>\n <symbol id=\"plyr-enter-fullscreen\" viewBox=\"0 0 18 18\">\n <path d=\"M10 3h3.6l-4 4L11 8.4l4-4V8h2V1h-7zM7 9.6l-4 4V10H1v7h7v-2H4.4l4-4z\"\/>\n <\/symbol>\n <symbol id=\"plyr-exit-fullscreen\" viewBox=\"0 0 18 18\">\n <path d=\"M1 12h3.6l-4 4L2 17.4l4-4V17h2v-7H1zM16 .6l-4 4V1h-2v7h7V6h-3.6l4-4z\"\/>\n <\/symbol>\n <symbol id=\"plyr-fast-forward\" viewBox=\"0 0 18 18\">\n <path d=\"M7.875 7.171L0 1v16l7.875-6.171V17L18 9 7.875 1z\"\/>\n <\/symbol>\n <symbol id=\"plyr-logo-vimeo\" viewBox=\"0 0 18 18\">\n <path d=\"M17 5.3c-.1 1.6-1.2 3.7-3.3 6.4-2.2 2.8-4 4.2-5.5 4.2-.9 0-1.7-.9-2.4-2.6C5 10.9 4.4 6 3 6c-.1 0-.5.3-1.2.8l-.8-1c.8-.7 3.5-3.4 4.7-3.5 1.2-.1 2 .7 2.3 2.5.3 2 .8 6.1 1.8 6.1.9 0 2.5-3.4 2.6-4 .1-.9-.3-1.9-2.3-1.1.8-2.6 2.3-3.8 4.5-3.8 1.7.1 2.5 1.2 2.4 3.3z\"\/>\n <\/symbol>\n <symbol id=\"plyr-logo-youtube\" viewBox=\"0 0 18 18\">\n <path d=\"M16.8 5.8c-.2-1.3-.8-2.2-2.2-2.4C12.4 3 9 3 9 3s-3.4 0-5.6.4C2 3.6 1.3 4.5 1.2 5.8 1 7.1 1 9 1 9s0 1.9.2 3.2c.2 1.3.8 2.2 2.2 2.4C5.6 15 9 15 9 15s3.4 0 5.6-.4c1.4-.3 2-1.1 2.2-2.4.2-1.3.2-3.2.2-3.2s0-1.9-.2-3.2zM7 12V6l5 3-5 3z\"\/>\n <\/symbol>\n <symbol id=\"plyr-muted\" viewBox=\"0 0 18 18\">\n <path d=\"M12.4 12.5l2.1-2.1 2.1 2.1 1.4-1.4L15.9 9 18 6.9l-1.4-1.4-2.1 2.1-2.1-2.1L11 6.9 13.1 9 11 11.1zM3.786 6.008H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z\"\/>\n <\/symbol>\n <symbol id=\"plyr-pause\" viewBox=\"0 0 18 18\">\n <path d=\"M6 1H3c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1zm6 0c-.6 0-1 .4-1 1v14c0 .6.4 1 1 1h3c.6 0 1-.4 1-1V2c0-.6-.4-1-1-1h-3z\"\/>\n <\/symbol>\n <symbol id=\"plyr-pip\" viewBox=\"0 0 18 18\">\n <path d=\"M13.293 3.293L7.022 9.564l1.414 1.414 6.271-6.271L17 7V1h-6z\"\/>\n <path d=\"M13 15H3V5h5V3H2a1 1 0 00-1 1v12a1 1 0 001 1h12a1 1 0 001-1v-6h-2v5z\"\/>\n <\/symbol>\n <symbol id=\"plyr-play\" viewBox=\"0 0 18 18\">\n <path d=\"M15.562 8.1L3.87.225c-.818-.562-1.87 0-1.87.9v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z\"\/>\n <\/symbol>\n <symbol id=\"plyr-restart\" viewBox=\"0 0 18 18\">\n <path d=\"M9.7 1.2l.7 6.4 2.1-2.1c1.9 1.9 1.9 5.1 0 7-.9 1-2.2 1.5-3.5 1.5-1.3 0-2.6-.5-3.5-1.5-1.9-1.9-1.9-5.1 0-7 .6-.6 1.4-1.1 2.3-1.3l-.6-1.9C6 2.6 4.9 3.2 4 4.1 1.3 6.8 1.3 11.2 4 14c1.3 1.3 3.1 2 4.9 2 1.9 0 3.6-.7 4.9-2 2.7-2.7 2.7-7.1 0-9.9L16 1.9l-6.3-.7z\"\/>\n <\/symbol>\n <symbol id=\"plyr-rewind\" viewBox=\"0 0 18 18\">\n <path d=\"M10.125 1L0 9l10.125 8v-6.171L18 17V1l-7.875 6.171z\"\/>\n <\/symbol>\n <symbol id=\"plyr-settings\" viewBox=\"0 0 18 18\">\n <path d=\"M16.135 7.784a2 2 0 01-1.23-2.969c.322-.536.225-.998-.094-1.316l-.31-.31c-.318-.318-.78-.415-1.316-.094a2 2 0 01-2.969-1.23C10.065 1.258 9.669 1 9.219 1h-.438c-.45 0-.845.258-.997.865a2 2 0 01-2.969 1.23c-.536-.322-.999-.225-1.317.093l-.31.31c-.318.318-.415.781-.093 1.317a2 2 0 01-1.23 2.969C1.26 7.935 1 8.33 1 8.781v.438c0 .45.258.845.865.997a2 2 0 011.23 2.969c-.322.536-.225.998.094 1.316l.31.31c.319.319.782.415 1.316.094a2 2 0 012.969 1.23c.151.607.547.865.997.865h.438c.45 0 .845-.258.997-.865a2 2 0 012.969-1.23c.535.321.997.225 1.316-.094l.31-.31c.318-.318.415-.781.094-1.316a2 2 0 011.23-2.969c.607-.151.865-.547.865-.997v-.438c0-.451-.26-.846-.865-.997zM9 12a3 3 0 110-6 3 3 0 010 6z\"\/>\n <\/symbol>\n <symbol id=\"plyr-volume\" viewBox=\"0 0 18 18\">\n <path d=\"M15.6 3.3c-.4-.4-1-.4-1.4 0-.4.4-.4 1 0 1.4C15.4 5.9 16 7.4 16 9c0 1.6-.6 3.1-1.8 4.3-.4.4-.4 1 0 1.4.2.2.5.3.7.3.3 0 .5-.1.7-.3C17.1 13.2 18 11.2 18 9s-.9-4.2-2.4-5.7z\"\/>\n <path d=\"M11.282 5.282a.909.909 0 000 1.316c.735.735.995 1.458.995 2.402 0 .936-.425 1.917-.995 2.487a.909.909 0 000 1.316c.145.145.636.262 1.018.156a.725.725 0 00.298-.156C13.773 11.733 14.13 10.16 14.13 9c0-.17-.002-.34-.011-.51-.053-.992-.319-2.005-1.522-3.208a.909.909 0 00-1.316 0zm-7.496.726H.714C.286 6.008 0 6.31 0 6.76v4.512c0 .452.286.752.714.752h3.072l4.071 3.858c.5.3 1.143 0 1.143-.602V2.752c0-.601-.643-.977-1.143-.601L3.786 6.008z\"\/>\n <\/symbol>\n<\/svg>\n`);\n\n\/\/ localStorage数据\nconst allStorage = {};\nfor (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n try {\n allStorage[key] = JSON.parse(localStorage.getItem(key));\n } catch (e) {\n allStorage[key] = localStorage.getItem(key);\n }\n}\n\/\/weblog(JSON.stringify(allStorage), 'localStorage 数据:', true);\n<\/script>\n<\/body>\n<\/html>\n<js>\nresult\n.replace(\/:\\s*\/g,':')\n.replace(\/\\{\\{.*播放源\\)\/g,'')\n.replace(\/<p>(?!.*集数)(.*:)<\\\/p>\/gm, '<p style=\"display:none;\">$1<\/p>');\n<\/js>",
"ruleImage": "img.img-placeholder@src",
"ruleLink": "a@href",
"ruleNextPage": "page",
"ruleTitle": "div.video-item-title@text",
"singleUrl": false,
"sortUrl": "搜索::\/search?text={{(source.getVariable()==''||source.getVariable()==null)?'丝袜':source.getVariable()}}&p={{page}}\n乱伦::\/main_ctg?id=8&p={{page}}\n強姦凌辱::\/main_ctg?id=2&p={{page}}\n内射受孕::\/main_ctg?id=12&p={{page}}\n巨乳美乳::\/main_ctg?id=9&p={{page}}\n出軌::\/main_ctg?id=7&p={{page}}\n角色劇情::\/main_ctg?id=6&p={{page}}\n絲襪美腿::\/main_ctg?id=1&p={{page}}\n制服誘惑::\/main_ctg?id=4&p={{page}}\n巨乳美乳::\/main_ctg?id=9&p={{page}}\n巨乳美乳::\/main_ctg?id=9&p={{page}}\n美腿::\/ctg?id=129&p={{page}}\n人妻::\/ctg?id=18&p={{page}}\n母親::\/ctg?id=88&p={{page}}\n痴女::\/ctg?id=65&p={{page}}\n女教师::\/ctg?id=69&p={{page}}",
"sourceIcon": "https:\/\/hohoj.tv\/resources\/img\/logo.png",
"sourceName": "HOHOJ",
"sourceUrl": "https:\/\/hohoj.tv\/",
"style": "* {\n z-index: 0;\n margin: 0;\n padding: 0;\n}\n\nbody {\n margin: auto;\n background: #bbb;\n width: 100%;\n}\n\nbody::before {\n content: '';\n position: fixed;\n top: calc(56.25vw + 15px);\n left: 0;\n width: 100%;\n height: calc(100% - (56.25vw + 15px));\n background-image: var(--bg-image);\n background-size: cover;\n background-position: center;\n opacity: var(--bg-opacity1);\n z-index: -1;\n pointer-events: none;\n}\n\nbody>p:first-of-type {\n width: 100%;\n position: fixed;\n top: 0px;\n text-indent: 0px;\n height: 16px;\n font-size: 0.7rem;\n border-radius: 0px 0px 0px 0px;\n background: #000;\n z-index: 200;\n display: flex;\n align-items: center;\n}\n\nbody>p:first-of-type a {\n color: #888;\n text-decoration: none;\n white-space: nowrap;\n overflow-x: auto;\n overflow-y: hidden;\n display: block;\n flex: 1;\n min-width: 0;\n}\n\nbody>p:first-of-type button {\n width: 30px;\n height: 16px;\n font-size: 0.6rem;\n white-space: nowrap;\n background: #333;\n border: 0;\n color: #888;\n cursor: pointer;\n flex-shrink: 0;\n position: relative;\n z-index: 201;\n}\n\nvideo {\n visibility: hidden;\n}\n\n.video-container {\n position: fixed;\n top: 15px;\n width: 100%;\n height: 56.25vw;\n z-index: 200;\n}\n\n#player {\n position: relative;\n width: 100%;\n}\n\n:root {\n color-scheme: only light;\n --plyr-color-main: #00aaff;\/* 播放器主要颜色 *\/\n --plyr-control-color: #fff;\/* 播放器控件图标颜色 *\/\n --plyr-control-background: transparent;\/* 播放器控件背景颜色 *\/\n --plyr-video-background: transparent;\/* 视频背景颜色 *\/\n --plyr-range-fill-background: #0099ee;\/* 进度条已填充部分的颜色 *\/\n --plyr-range-thumb-background: #fff;\/* 进度条滑块的颜色 *\/\n}\n\n.plyr {\n height: 100% !important;\n width: 100% !important;\n object-fit: cover;\n}\n\n.plyr__video-wrapper * {\n touch-action: none !important;\n}\n\n.plyr__video-wrapper video {\n touch-action: none !important;\n}\n\n.plyr__controls {\n touch-action: none !important;\n}\n\n.plyr__control--overlaid {\n background: transparent;\n border: 0;\n border-radius: 100%;\n color: #fff;\n left: calc(50% - 25px);\n top: calc(50% - 45px);\n transform: none;\n width: 60px;\n height: 48px;\n padding: 0;\n z-index: 2;\n}\n\n.plyr__control--overlaid svg {\n width: 50px;\n height: 50px;\n left: calc(50% - 25px);\n top: calc(50% - 10px);\n transform: none;\n fill: #fff;\/* 大播放器控件图标颜色 *\/\n filter: \n drop-shadow(0 0 20px rgba(0, 0, 0, 0.3))\n drop-shadow(0 0 30px rgba(0, 0, 0, 0.1));\n}\n\n.plyr__control--overlaid.hidden {\n display: none !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n.plyr--video .plyr__control.plyr__tab-focus,.plyr--video .plyr__control:hover,.plyr--video .plyr__control[aria-expanded=true] {\n background: transparent;\/* 播放器控件悬停\/点击背景颜色 *\/\n color: #00aaff;\/* 播放器控件悬停\/点击图标颜色 *\/\n}\n\n.plyr__controls .plyr__controls__item {\n margin-left: auto;\n margin: calc(var(--plyr-control-spacing,10px)\/4);\n}\n\n.plyr__time--duration {\n display: inline-block !important;\n}\n\n.plyr__time+.plyr__time:before {\n margin-right: 8px !important;\n}\n\n@media (max-width: 640px) {\n .plyr__captions {\n margin-bottom:-8px\n }\n\n .plyr__progress__container {\n margin-right: 5px\n }\n\n .plyr__time {\n position: absolute;\n bottom: 29px;\n }\n\n .plyr__time--current {\n left: 106px\n }\n\n .plyr__time+.plyr__time:before {\n content: \"\"!important\n }\n\n .plyr__time--duration {\n right: 110px;\n }\n\n .plyr__volume {\n width: auto;\n max-width: 32px!important;\n min-width: 32px!important\n }\n\n input[id^=plyr-volume-] {\n display: none!important;\n }\n\n .plyr--airplay-supported [data-plyr=airplay],.plyr--captions-enabled [data-plyr=captions],.plyr--pip-supported [data-plyr=pip] {\n display: none!important;\n }\n}\n\ndetails {\n width: 100%;\n height: auto;\n margin: auto;\n}\n\ndetails>img {\n position: fixed;\n width: 100%;\n max-height: 90vw;\n object-fit: contain;\n background: #000;\n border-bottom: 0.5px solid #333;\n top: calc(56.25vw + 16px + 1.5em);\n z-index: 50;\n}\n\ndetails[open]>summary {\n background: #bbb;\/* 标题背景颜色 *\/\n}\n\nsummary {\n position: fixed;\n background: rgba(221, 221, 221, var(--bg-opacity2));\/* 标题背景颜色和透明度 *\/\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n color: #111;\/* 标题文字颜色 *\/\n box-shadow: 0 0.5px 3px #555;\/* 标题阴影颜色 *\/\n list-style: none;\n width: 100%;\n padding-top: calc(56.25vw + 16px);\n outline: none;\n line-height: 1.5;\n text-align: left;\n word-wrap: break-word;\n z-index: 100;\n}\n\nsummary>h3 {\n width: 95%;\n margin: auto;\n}\n\nsummary::-webkit-details-marker {\n display: none;\n}\n\n.all-info {\n position: relative;\n color: #333;\/* 详情信息文字颜色 *\/\n margin: auto;\n width: 100%;\n height: auto;\n padding-top: calc(56.25vw + 24px + 1.5em);\n}\n\n.all-info>div {\n width: 100%;\n margin: auto;\n}\n\n.all-info>p {\n text-indent: 0px;\n}\n\n.all-info>div>p {\n width: 90%;\n margin: 5px 5%;\n outline: none;\n text-align: left;\n word-wrap: break-word;\n}\n\n.jiekou {\n display: flex;\n}\n\n#selected-jiekou {\n display: flex;\n position: fixed;\n justify-content: flex-end;\n top: calc(56.25vw + 19.9px + 1.5em);\n width: 150px;\n right: 5px;\n}\n\n#selected-jiekou button {\n width: 125px;\n height: 28px;\n padding: 2px;\n background: rgba(204, 204, 204, var(--bg-opacity2));\/* 已选接口背景颜色 *\/\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n overscroll-behavior-y: none;\n -webkit-overflow-scrolling: auto;\n color: #4a89a9;\n border: 1px solid #666;\n border-radius: 0 0 3px 3px;\n opacity: 0.5;\n}\n\n#jiekou-list {\n display: none;\n position: fixed;\n overflow-y: auto;\n max-height: 200px;\n top: 0;\n right: 5px;\n background: rgba(204, 204, 204, var(--bg-opacity2));\/* 接口列表背景颜色 *\/\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n border: 1px solid #888;\n border-top: none;\n border-radius: 0 0 3px 3px;\n z-index: 100;\n margin: auto;\n}\n\n#jiekou-list button {\n display: block;\n width: 100%;\n height: 28px;\n margin: auto;\n padding: 2px;\n background: rgba(187, 187, 187, 0.1);\/* 待选接口按钮背景颜色 *\/\n color: #333;\/* 待选接口按钮文字颜色 *\/\n border: 1px solid #888;\n border-top: none;\n border-left: none;\n border-right: none;\n border-radius: 0px;\n cursor: pointer;\n}\n\n#jiekou-list button:last-of-type {\n border-bottom: none;\n border-radius: 0 0 3px 3px;\n}\n\nsup {\n font-size: 0.4em;\n vertical-align: top;\n position: relative;\n top: -0.6em;\n}\n\n.jishu button:hover {\n background: rgba(204, 204, 204, var(--bg-opacity2));\/* 按钮悬停背景颜色 *\/\n border: 1px solid #4a89a9;\/* 按钮悬停边框颜色 *\/\n}\n\n.jishu button {\n width: 30%;\n margin: 1.25%;\n padding: 5px;\n outline: none;\n border-radius: 8px;\n border: 1px solid #666;\n background: rgba(204, 204, 204, var(--bg-opacity2));\/* 集数按钮背景颜色 *\/\n color: #333;\/* 集数按钮文字颜色 *\/\n font-size: 0.7rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.jishu button.active {\n background: rgba(204, 204, 204, var(--bg-opacity2));\/* 已选集数按钮背景颜色 *\/\n color: #4a89a9;\n border: 1px solid #4a89a9;\n position: sticky;\n left: 0;\n right: 0;\n}\n\n.popup {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%) scale(0.95);\n background-color: #ccc;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);\n z-index: 2147483647;\n width: 80%;\n max-height: 80vh;\n overflow: auto;\n font-family: system-ui, -apple-system, sans-serif;\n opacity: 0;\n transition: all 0.3s ease;\n pointer-events: auto;\n}\n\n.titleElement {\n margin: 0 0 16px 0;\n color: #333;\n font-size: 18px;\n font-weight: 600;\n}\n\n.contentElement {\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n max-height: calc(80vh - 150px);\n overflow-y: auto;\n padding: 16px;\n background-color: #bbb;\n color: #000;\n border-radius: 6px;\n margin-bottom: 20px;\n font-size: 14px;\n line-height: 1.6;\n border: 1px solid #999;\n}"
}