VirtualTaboo直播优化
https://zh.virtualtaboo.live##吉吉国王
CVGghhv (8592)11/05 09:11
修复发现页我的收藏部分版本不显示问题
网页播放器新增HLS level 热切换,切换线路和清晰度不断流,视频容器和排版优化,不再默认禁音
{
"bookSourceComment": "VirtualTaboo 直播(视频源)\n\n【目录 \/ 播放】\n· 视频类型:章节 URL 为 m3u8,调用 Legado 内置播放器\n· 文本类型:章节 URL 为主播页,正文打开网页播放器\n· 发现页顶部「🎬 播放方式」可切换:内置播放器 \/ 网页播放器\n· 分辨率优先用站点 presets,为空时统一用「自动(_auto)」,避免 480p 等档位 404\n· 卡顿时在目录切换域名线路(域名一\/二\/三…);切类型\/播放方式后刷新目录生效\n\n【内置播放器弹幕】\n· ruleContent.subContent 读取站点聊天(\/v2\/models\/{id}\/chat)生成 Bilibili XML 弹幕\n· 普通消息白色、打赏橙色;章节加载时生成一次(静态),切线路\/切集\/重进章节刷新\n\n【网页播放器】\n· 完整页面 + 移动端适配;视频固定顶栏,信息\/收藏\/输入框固定在底栏\n· 播放器高度按视频比例自适应(最高 75vh),线路\/清晰度\/静音\/横竖屏\/全屏\n· 清晰度热切换:优先无缝换流,失败自动回退重载(1080p\/720p\/480p\/240p)\n· 首屏优先加载视频:预连接 CDN,聊天\/历史延后到视频起播后初始化\n· 清晰度\/线路切换失败:聊天区提示原因,并回滚到原清晰度\/原线路\n· 线路热切换:新线路后台加载,起播后无缝接管,不中断当前画面;失败自动回退\n· 音频默认带声并记住上次静音状态,切清晰度\/线路保持音量\n· 点画面只显示控制条,中央按钮负责播放\/暂停\n· 弹幕实时接收(WebSocket 断线自动重连并刷新 token);打开时加载最近 50 条历史\n· 消息按时间顺序排列、自动滚动(往上翻不打断);聊天区最高 60vh,DOM 保留最近 300 条\n· 输入框右侧表情按钮:38 个站点表情,点击插入 :表情代码:(图片按 Apple emoji 渲染)\n· 注意:站点不允许纯表情消息,单独发表情会被拦截,需带一点文字\n· 弹幕发送:POST \/api\/front\/models\/{id}\/chat,带 Cookie + CSRF\n· 底栏 ❤️ 收藏按钮:收藏\/取消与站点「我的收藏」同步\n\n【书架同步收藏】\n· 加入书架(addBookShelf)→ 自动 PUT 到站点「我的收藏」\n· 移出书架(delBookShelf)→ 自动 DELETE 取消收藏\n· OkHttp 静默发送(Cookie + CSRF),toast 提示结果\n\n【发现页】\n· 💕 {登录名} 的收藏:登录后出现,在线优先;观众数缺失时按用户名回查真实在看人数\n· 「🔥 在线热门」「🔥 人气趋势」分类\n· 付费主播显示\/隐藏:源变量 showPaid=1 显示、0 隐藏\n· 书名只显示用户名,不追加(付费\/离线\/一对一)等状态后缀\n· 标签分类覆盖全部 liveTags,未覆盖标签归入「🔥 更多热门」\n\n【详情页】\n· 分类:国家、免费\/付费、在看人数(收藏\/详情会自动回查)\n· 目标进度:已筹\/目标\/剩余\n· 收藏\/关注\/私信订阅状态;公开录像是否可看;群秀标题与价格\n· 最新章节:本次开播 \/ 上次在线时间\n· 简介:账号代币、会员、互动能力、可录、粉丝团、主播资料、标签、兴趣、价格\n\n【登录】\n· 登录界面已移除,点「登录」直接打开网站登录页(loginUrl),Cookie 供全源使用",
"bookSourceGroup": "视频直播",
"bookSourceName": "VirtualTaboo直播优化",
"bookSourceType": 4,
"bookSourceUrl": "https:\/\/zh.virtualtaboo.live##吉吉国王",
"customButton": false,
"customOrder": 1,
"enabled": true,
"enabledCookieJar": true,
"enabledExplore": true,
"eventListener": true,
"exploreUrl": "@js:var sort=[],push=function(t,u,p){sort.push({title:t,url:u,style:{layout_flexBasisPercent:p}});};\nvar LIVE='https:\/\/zh.virtualtaboo.live';\nfunction getDisplay(tag){return (typeof lgTag==='function')?lgTag(tag):String(tag||'').replace(\/([A-Z])\/g,' $1').trim();}\nvar DEVICE=['doStrapon','doAnalToys','autoTagInteractiveToy','doDildoOrVibrator','fuckMachine','doNippleToys','doSexToys','autoTagKiiroo','autoTagLovense'];\nvar FETISH=['doSmoking','subcultureCuckold','specificsPiercing','cockRating','heels','corset','doFootFetish','specificJeans','subcultureBdsm','leather','subcultureMistresses','latex','nylon','specificsTattoos','specificPregnant','specificSportGear'];\nvar PERF=['striptease','eroticDance','twerk','oilShow','shower','topless','flashing','upskirt'];\nfunction buildChannel(cfg){\n var tag=cfg.tag;\n var data=JSON.parse(java.ajax(LIVE+'\/api\/front\/models\/liveTags?primaryTag='+tag+'&withMixedTags=true&sac=true'));\n var groups=data.liveTagGroups||[],details=data.liveTagDetails||{};\n function url(t){return LIVE+'\/api\/front\/models?removeShows=false&recInFeatured=false&limit=60&offset={{(page-1)*60}}&primaryTag='+tag+'&filterGroupTags='+encodeURIComponent(JSON.stringify([[t]]))+'&sortBy=stripRanking&parentTag='+t+'&userRole=user&sac=true';}\n var used={};\n function render(list,title){if(!list||!list.length)return;push('❤️ '+title+' ❤️',null,1);for(var i=0;i<list.length;i++){var t=list[i];if(!t)continue;used[t]=1;push(getDisplay(t),url(t),0.29);}}\n function findGroup(alias){for(var i=0;i<groups.length;i++)if(groups[i].alias===alias)return groups[i];return null;}\n function sortTags(list){list.sort(function(a,b){var ca=details[a]?details[a].modelsLive||0:0,cb=details[b]?details[b].modelsLive||0:0;return cb-ca;});return list;}\n function validOf(list){var v=[];for(var i=0;i<list.length;i++){var t=list[i];if(details[t]&&details[t].modelsLive>0)v.push(t);}return v;}\n function renderGroup(alias,title){var g=findGroup(alias);if(!g)return;render(sortTags(validOf(g.tags||[])),title);}\n function renderManual(list,title){render(sortTags(validOf(list)),title);}\n if(cfg.header)push(cfg.header,null,1);\n if(cfg.before)cfg.before(details,{push:push,url:url});\n for(var gi=0;gi<cfg.groups.length;gi++)renderGroup(cfg.groups[gi][0],cfg.groups[gi][1]);\n var langTags=[],subTags=[];\n for(var t in details){if(details[t].modelsLive>0&&t.indexOf('-')===-1){if(t.indexOf('tagLanguage')===0)langTags.push(t);else if(t.indexOf('subculture')===0)subTags.push(t);}}\n render(sortTags(langTags),'🌐 语言分类');\n render(sortTags(subTags),'🎭 亚文化');\n var all=findGroup('allTags'),act=[],body=[],perf=[];\n if(all){var at=all.tags||[];for(var i=0;i<at.length;i++){var t=at[i];if(!details[t]||details[t].modelsLive<=0)continue;if(t.indexOf('do')===0)act.push(t);else if(t.indexOf('specific')===0)body.push(t);for(var k=0;k<PERF.length;k++){if(t.indexOf(PERF[k])!==-1){perf.push(t);break;}}}}\n render(sortTags(act),'🎬 活动分类');\n render(sortTags(body),'🔍 身体特征');\n render(sortTags(perf),'💃 表演类型');\n renderManual(DEVICE,'📱 设备分类');\n renderManual(FETISH,'🖤 恋物癖');\n var rest=[];\n if(all){var at2=all.tags||[];for(var i2=0;i2<at2.length;i2++){var t2=at2[i2];if(!details[t2]||details[t2].modelsLive<=0)continue;if(!used[t2])rest.push(t2);}}\n rest.sort(function(a,b){var ca=details[a]?details[a].modelsLive||0:0,cb=details[b]?details[b].modelsLive||0:0;return cb-ca;});\n if(rest.length>20)rest=rest.slice(0,20);\n render(rest,'🔥 更多热门');\n}\nvar G1=[['specials','🔥 特别推荐'],['age','🎂 年龄'],['ethnicity','🌍 人种'],['bodyType','💪 体型'],['hairColor','💇 发色'],['privatePrice','💰 价格']];\nvar GM=[['specials','🔥 特别推荐'],['orientation','💖 性取向'],['age','🎂 年龄'],['ethnicity','🌍 人种'],['bodyType','💪 体型'],['hairColor','💇 发色'],['privatePrice','💰 价格']];\nbuildChannel({tag:'girls',header:null,groups:G1,before:function(details,api){\n api.push('🔥 在线热门',LIVE+'\/api\/front\/models?removeShows=false&recInFeatured=false&limit=60&offset=0&primaryTag=girls&sortBy=stripRanking&userRole=user&sac=true',1);\n api.push('🔥 人气趋势',LIVE+'\/api\/front\/models?removeShows=false&recInFeatured=false&limit=60&offset={{(page-1)*60}}&primaryTag=girls&sortBy=trending&userRole=user&sac=true',1);\n try{\n var ck=String(cookie.getCookie(LIVE+'\/')||''),uid='',uname='',ps=ck.split('; ');\n for(var pi=0;pi<ps.length;pi++){if(ps[pi].indexOf('AMP_')===0){var raw=ps[pi].substring(ps[pi].indexOf('=')+1);var o=JSON.parse(decodeURIComponent(java.base64Decode(raw)));uid=o.userId||'';}}\n if(!uid){try{var aj=JSON.parse(String(java.ajax(LIVE+'\/api\/front\/v3\/config\/initial-dynamic')));var ad=(aj&&aj.initialDynamic)||{};uid=(ad.user&&ad.user.id)||'';uname=(ad.user&&(ad.user.username||ad.user.login))||'';}catch(eu){}}\n if(uid&&!uname){try{var acs=null;try{acs=cache.get('lg_acc');}catch(ea){}if(!acs){acs=java.ajax(LIVE+'\/api\/front\/v3\/config\/initial-dynamic');try{cache.put('lg_acc',acs,120);}catch(eb){}}var ad2=(JSON.parse(String(acs)).initialDynamic)||{};uname=(ad2.user&&(ad2.user.username||ad2.user.login))||'';}catch(ec){}}\n if(uid){\n var favUrl=null;\n try{favUrl=cache.get('lg_fav_url');}catch(e0){}\n if(!favUrl){\n try{\n var fj=JSON.parse(String(java.ajax(LIVE+'\/api\/front\/users\/'+uid+'\/favorites')));\n var ids=(fj.modelIds||[]).slice(0,60);\n if(ids.length){\n try{\n var rp=java.post(LIVE+'\/api\/front\/models\/get-statuses',JSON.stringify({ids:ids}),{'Content-Type':'application\/json','Accept':'application\/json'});\n var st=JSON.parse(String(rp.body())).statuses||[],sm={};\n for(var si=0;si<st.length;si++){sm[String(st[si].id)]=st[si].status;}\n ids.sort(function(a,b){var sa=sm[String(a)]||'',sb=sm[String(b)]||'';return (sa!=='off'?0:1)-(sb!=='off'?0:1);});\n }catch(e2){}\n favUrl=LIVE+'\/api\/front\/models\/list?modelIds[]='+ids.join('&modelIds[]=');\n try{cache.put('lg_fav_url',favUrl,60);}catch(e3){}\n }\n }catch(e4){}\n }\n if(favUrl)api.push('💕 '+(uname?uname+' 的收藏':'我的收藏'),favUrl,1);\n }\n }catch(e){}\n var ql=[['🇨🇳 中文','tagLanguageChinese'],['🇯🇵 日语','tagLanguageJapanese'],['🇻🇳 越南语','tagLanguageVietnamese']];\n for(var i=0;i<ql.length;i++){var c=details[ql[i][1]]?details[ql[i][1]].modelsLive||0:0;if(c>0)api.push(ql[i][0],api.url(ql[i][1]),0.29);}\n}});\nbuildChannel({tag:'couples',header:'💕 情侣专区 💕',groups:G1});\nbuildChannel({tag:'men',header:'👨 男主播专区 👨',groups:GM});\nbuildChannel({tag:'trans',header:'⚧️ 跨性别专区 ⚧️',groups:G1});\nvar __sd={};try{__sd=JSON.parse(String(source.getVariable()||'{}'));}catch(e){}\nvar __pm='native';try{__pm=(String(__sd.playMode)==='web')?'web':'native';}catch(e){}\nsort.unshift({title:'🎬 播放方式',url:'',type:'select',chars:['内置播放器','网页播放器'],default:(__pm==='web'?'网页播放器':'内置播放器'),action:\"lgPlaySet(infoMap['🎬 播放方式'])\",style:{layout_flexGrow:1,layout_flexBasisPercent:1}});\nsort.unshift({title:'🎭 付费主播',url:'',type:'select',chars:['显示','隐藏'],default:(String(__sd.showPaid)==='0'?'隐藏':'显示'),action:\"lgPaidSet(infoMap['🎭 付费主播'])\",style:{layout_flexGrow:1,layout_flexBasisPercent:1}});\nJSON.stringify(sort);",
"header": "{\"User-Agent\":\"Mozilla\/5.0 (Linux; Android 13; Pixel 7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/120.0.0.0 Mobile Safari\/537.36\",\"Accept\":\"application\/json, text\/plain, *\/*\",\"Accept-Language\":\"zh-CN,zh;q=0.9,en;q=0.8\",\"Referer\":\"https:\/\/zh.virtualtaboo.live\/\"}",
"jsLib": "\/\/ ===== Legado 公共 JS 库:紧凑映射表 + 格式化 + 渲染 =====\nvar MAP_RAW=\"69^69式|eyeColorBlack^黑瞳|eyeColorBrown^棕瞳|eyeColorBlue^蓝瞳|eyeColorGreen^绿瞳|eyeColorGray^灰瞳|eyeColorHazel^榛瞳|subcultureGlamour^高贵|subcultureAnime^动漫|subcultureEgirl^电子女孩|subcultureKpop^韩流|subcultureStudent^学生|subcultureNerds^书呆子|subcultureGamers^玩家|subcultureBdsm^虐恋|subcultureCuckold^出轨|subcultureGoth^哥特|subcultureEmo^Emo|subcultureSwingers^交换伴侣|subcultureMistresses^情妇|subcultureHousewives^家庭主妇|subcultureGymBabe^健身宝贝|subcultureRomantic^浪漫|subculturePunks^朋克|subcultureClubber^夜店女郎|subcultureBottom^Bottom|subcultureTop^Top|subcultureChubbies^胖熊|subcultureCubs^小熊|subcultureDragQueens^变装皇后|subcultureEboy^E-boy|subcultureGymRats^健身狂|subcultureHunks^猛男|subcultureJocks^运动男|subcultureOtters^水獭|subcultureStuds^种马|en^英语|zh^中文|zt^繁体中文|jp^日语|ja^日语|ru^俄语|es^西语|fr^法语|de^德语|pt^葡语|vi^越南语|th^泰语|ar^阿语|hi^印地语|tr^土耳其语|pl^波兰语|ro^罗马尼亚语|it^意大利语|nl^荷兰语|id^印尼语|fil^菲律宾语|ko^韩语|mn^蒙古语|he^希伯来语|fa^波斯语|bn^孟加拉语|ta^泰米尔语|te^泰卢固语|gu^古吉拉特语|mr^马拉地语|kn^卡纳达语|ml^马拉雅拉姆语|or^奥里亚语|pa^旁遮普语|si^僧伽罗语|ne^尼泊尔语|ur^乌尔都语|sw^斯瓦希里语|am^阿姆哈拉语|ha^豪萨语|yo^约鲁巴语|ig^伊博语|zu^祖鲁语|xh^科萨语|st^塞索托语|sn^绍纳语|so^索马里语|rw^基尼亚隆达语|mg^马尔加什语|hairLengthLong^长发|hairLengthMedium^中发|hairLengthShort^短发|hairLengthBald^光头|mobile^手机直播|groupShow^群组秀|tagGroupSex^群交|fuckMachine^炮机|videoGames^游戏|asmr^ASMR|latex^乳胶|leather^皮革|corset^紧身衣|boots^靴子|heels^高跟鞋|nylon^丝袜|i18n_ar^阿语|i18n_zh^中文|ageYoung^年轻|ageTeen^18+|ageMilf^熟女|ageMature^成熟|sexting^文爱|BBW^大码|Big^丰满|Medium^匀称|Slim^纤细|Curvy^曲线|Petite^娇小|Athletic^运动|Average^匀称|Large^大码|Thin^纤细|Black^黑发|Brown^棕发|Blonde^金发|Red^红发|Colorful^彩发|Hairless^无毛|Asian^亚洲|White^白人|Ebony^黑人|Latino^拉丁|Indian^印度|MiddleEastern^中东|Multiracial^混血|Eight^8代币\/分|SixteenToTwentyFour^16-24代币\/分|ThirtyTwoSixty^32-60代币\/分|NinetyPlus^90+代币\/分|Handy^手活|HandyJob^手活|Pump^抽插机|Hairy^多毛|blowjob^口交|coffee^咖啡|cooking^烹饪|dirty-talk^骚话|foot-fetish^恋足|french-kiss^法式吻|gardening^园艺|gym^健身|hiking^徒步|sex-toys^性玩具|squirt^潮吹|underwear^内衣|yoga^瑜伽|massage^按摩|travel^旅行|music^音乐|movies^电影|reading^阅读|napping^小憩|mistress^情妇|acting^表演|adventure^冒险|classic^古典|diy^手工|gentle-sex^温柔性爱|object-sexuality^物品恋|role-play^角色扮演|role-playing^角色扮演|submission^顺从|submissive^顺从|cosplay^Cosplay|kissing^亲吻|dancing^跳舞|singing^唱歌|gaming^游戏|fashion^时尚|fitness^健身|photography^摄影|art^艺术|females^女|female^女|males^男|male^男|trans^跨性别|tranny^跨性别|couples^情侣|maleFemale^情侣|couple^情侣|group^情侣|girls^女主播|men^男主播|jerkOffInstruction^射精指导|cockRating^大屌评分|orientationBisexual^双性恋|orientationGay^男同|orientationStraight^异性恋|ageDaddies^大叔|tagMenTwinks^男同小鲜肉|subcultureNone^无|全裸^全裸|smallAudience^观众少|easter^复活节|trannies^跨性别|privateP2PNinetyPlus^90+代币\/分|privateP2PPriceThirtyTwoSixty^32-60代币\/分|privateP2PPriceSixteenToTwentyFour^16-24代币\/分|privateP2PPriceEight^8代币\/分|beach^海滩|bondage^束缚|deepthroat^深喉|hardcore^硬核|porn^色情|swinging^换偶|pop^流行乐|basketball^篮球|swimming^游泳|cafes-restaurants^咖啡馆\/餐厅|k-pop^韩流|mystery^悬疑|playing-musical-instruments^乐器|psychology^心理学|romance^浪漫|seafood^海鲜|shopping^购物|tennis^网球|wine^红酒|anal^肛交|cuckold^出轨|group-sex^群交|chinese^中文|erotica^情色|hentai^动漫色情|photo^摄影|voyeurism^窥视|action^动作|art-galleries^美术馆|barbeque^烧烤|board-games^桌游|country^乡村音乐|sweet-tooth^甜食|astronomy^天文|football-soccer^足球|handmade^手工|makeup^化妆|r-and-b^R&B|rock^摇滚|sci-fi^科幻|techno^电子乐|interestedInMen^喜欢男性|interestedInWomen^喜欢女性|interestedInEverybody^都喜欢|18 years^18岁|big squirt^大潮吹|bigass^大屁股|bigboobs^大胸|bigcock^大屌|elegant^优雅|bdsm^BDSM\/束缚|billiards^台球|hip-hop^嘻哈|jazz^爵士|latino^拉丁|night-clubs^夜店|parties^派对|surfing^冲浪|videomaking^视频制作|subcultureBears^胖熊|femaleTranny^跨性别|astrology^占星|tarot^塔罗|anals^肛交|cum^射精|cum bigcock^大屌射精|cum show facial squirt^颜射潮吹秀|cumshow^射精秀|eatcum^吃精|gameplay^游戏|gamerboy^游戏男孩|gush^喷涌|halloween^万圣节|jerkoff^打飞机|lovense domi^Lovense Domi|privateP2PPriceTwelveTwentyFour^12-24代币\/分|sexGayCouples^男同情侣|smallcock^小屌|smalltits^小胸|anime^动漫|bars^酒吧|beer^啤酒|blogging^写博客|bowling^保龄球|boxing^拳击|burgers^汉堡|camping^露营|cartoon^卡通|cocktails^鸡尾酒|comedy^喜剧|comic-books^漫画|concerts^演唱会|cycling^骑行|drama^戏剧|edm^电子舞曲|family-vacation^家庭度假|folk^民谣|foreign-languages^外语|healthy^健康|house^浩室音乐|karaoke^卡拉OK|meditation^冥想|mindfulness^正念|mountaineering^登山|music-festivals^音乐节|picnics^野餐|poker^扑克|reality-tv^真人秀|road-trips^自驾游|running^跑步|standup^脱口秀|steak^牛排|technology-computers^电脑科技|thriller^惊悚|triathlon^铁人三项|twerk^电臀舞|vlogging^拍Vlog|volleyball^排球|vr-ar-games^VR\/AR游戏|watching-tv^追剧|whiskey^威士忌|writing^写作|emo^Emo|ms^马来语|maleTranny^跨性别|ageOld^老年\";\nvar P_RAW={\"ethnicity\":\"Asian^亚洲|White^白人|Ebony^黑人|Latino^拉丁|Indian^印度|MiddleEastern^中东|Multiracial^混血\",\"bodyType\":\"Thin^纤细|Petite^娇小|Athletic^运动型|Medium^匀称|Curvy^曲线型|BBW^大码|Big^丰满|Slim^纤细|Muscular^肌肉型|Skinny^瘦\",\"hairColor\":\"Blonde^金发|Brown^棕发|Black^黑发|Red^红发|Colorful^彩发|Hairless^无毛\",\"specific\":\"Shaven^剃毛|Trimmed^修剪|BigTits^巨乳|SmallTits^小胸|Pregnant^孕妇|Interracial^跨种族|Tomboy^假小子|Queer^酷儿|NonBinary^非二元|BigClit^大阴蒂|BigNipples^大乳头|HairyArmpits^腋毛|Jeans^牛仔裤|SportGear^运动装备|POV^POV|SneakersSocks^球鞋白袜|Uncut^未割|BigBalls^大睾丸|Uniform^制服|PowerTop^强势攻|Mustache^胡子|MtF^MtF|Beardy^络腮胡|Bubblebutt^蜜桃臀|Femboy^伪娘|FtM^女转男|Shemale^人妖|TG^跨性别|TS^变性|Boots^靴子|Lesbians^女同|BBC^黑人大屌|Side^侧位|Sissy^伪娘|SmallCock^小屌|TV^变装|Vers^全能\",\"specifics\":\"Tattoos^纹身|Piercing^穿孔|BigAss^大屁股|Hairy^多毛|BigTits^巨乳|Shaven^剃毛|Pregnant^孕妇|BigCock^大屌\",\"do\":\"Talk^聊天|Masturbation^自慰|Striptease^脱衣舞|EroticDance^色情舞|Twerk^电臀舞|Smoking^抽烟|Yoga^瑜伽|Cooking^烹饪|Massage^按摩|Shower^淋浴|Flashing^快闪|Upskirt^裙底|Office^办公室|Cosplay^Cosplay|RolePlay^角色扮演|SexToys^性玩具|Music^音乐|VideoGames^游戏|Reading^阅读|Drawing^绘画|Singing^唱歌|Dancing^跳舞|Blowjob^口交|DeepThroat^深喉|Anal^后庭|DildoOrVibrator^震动棒|NippleToys^乳头玩具|Strapon^骑跨|69Position^69式|Cowgirl^女上|DoggyStyle^后入|Missionary^传教士|SpikeSpike^尖峰对尖|AssToMouth^后庭接吻|DoublePenetration^双重|Orgasm^高潮|CreamPie^内射|Cumshot^颜射|Bukkake^集体颜射|Gape^开肛|Gagging^塞口|Spanking^打屁股|Humiliation^羞辱|OilShow^精油|Ejaculation^射精|TittyFuck^乳交|Suck^吮吸|Fisting^拳交|Squirt^失禁|Threesome^三人|4some^四人|Sextape^自拍|CamToCam^对脸|Teledildonics^遥控玩具|Lovense^Lovense|Kiiroo^Kiiroo|PublicPlace^公共场所|Hardcore^硬核|Handy^手活|Pump^抽插机|Ahegao^高潮脸|Topless^露乳|Nylon^丝袜|Corset^紧身衣|Heels^高跟鞋|FootFetish^恋足|ggy-style^后入|minant^主导|Handjob^手活|Fingering^指交|Facesitting^骑脸|AnalToys^后庭玩具|JerkOffInstruction^射精指导|Facial^颜射|GloryHole^厕板洞|Cei^吃精|CumPlay^精液玩法|Pegging^穿戴阳具|Swallow^吞精|Precum^前液|Footjob^足交|Kissing^接吻|PussyLicking^舔阴|Flexing^秀肌肉|Gangbang^群体|CamelToe^骆驼趾|PenisRing^阴茎环|Outdoor^户外|Swingers^换偶|Cuckold^出轨|Latex^乳胶|Leather^皮革|Mistress^情妇|CockRating^大屌评分|mination^支配|Rubbing^摩擦|Sph^小鸟羞辱|Foursome^四人|Rimming^舔肛|Selfsucking^自舔\",\"autoTag\":\"Spy^可窥视|P2P^P2P|InteractiveToy^互动玩具|RecordablePrivate^可录制私秀|BestPrivates^最佳私秀|New^新主播|Vr^VR|Lovense^Lovense|Kiiroo^Kiiroo|NonNude^非全裸|Handy^手活|Hd^高清|RecordablePublic^可录公开|Lesbian^女同|OldYoung^老少配\",\"tagLanguage\":\"Chinese^中文|Taiwanese^台湾|Malaysian^马来语|Filipino^菲律宾语|Vietnamese^越南语|Ukrainian^乌克兰语|African^非洲裔|Algerian^阿尔及利亚|Argentinian^阿根廷|Armenian^亚美尼亚|Assamese^阿萨姆|Australian^澳大利亚|Austrian^奥地利人|Bangladeshi^孟加拉|Belgian^比利时|Bengali^孟加拉语|Brazilian^巴西|Bulgarian^保加利亚人|Canadian^加拿大|Chilean^智利|Colombian^哥伦比亚|Croatian^克罗地亚|Czech^捷克|Danish^丹麦|Dominican^多米尼加|Dutch^荷兰|Ecuadorian^厄瓜多尔|Egyptian^埃及|Estonian^爱沙尼亚|European^欧洲女孩|Korean^韩语|Finnish^芬兰|French^法语|Georgian^格鲁吉亚|GermanSpeaking^德语|Greek^希腊|Gujarati^古吉拉特|Hindi^印地|Hungarian^匈牙利|Indonesian^印度尼西亚|Irish^爱尔兰|Israeli^以色列|Italian^意大利|Jamaican^牙买加|Japanese^日语|Kannada^卡纳达|Kenyan^肯尼亚|Kiwi^新西兰|Latvian^拉脱维亚|Lithuanian^立陶宛|Malagasy^马尔加什|Malayalam^马拉雅拉姆|Marathi^马拉地|Mexican^墨西哥|Moroccan^摩洛哥|Nigerian^尼日利亚|Nordic^北欧|Norwegian^挪威|Odia^奥里亚|Peruvian^秘鲁|Polish^波兰|Portuguese^葡萄牙|PortugueseSpeaking^葡萄牙语|Punjabi^旁遮普|Romanian^罗马尼亚|RussianSpeaking^俄罗斯|Serbian^塞尔维亚|Slovakian^斯洛伐克|SouthAfrican^南非|Spanish^西班牙语|SpanishSpeaking^西班牙|SriLankan^斯里兰卡|Swedish^瑞典|Swiss^瑞士|Tamil^泰米尔|Telugu^泰卢固|Thai^泰国|Turkish^土耳其|UKModels^英国|USModels^美国|Ugandan^乌干达|Venezuelan^委内瑞拉|Zimbabwean^津巴布韦\",\"privatePrice\":\"Eight^8代币\/分|SixteenToTwentyFour^16-24代币\/分|ThirtyTwoSixty^32-60代币\/分|NinetyPlus^90+代币\",\"country:\":\"us^美国|cn^中国|ru^俄罗斯|br^巴西|in^印度|id^印尼|th^泰国|vn^越南|tr^土耳其|pl^波兰|es^西班牙|mx^墨西哥|fr^法国|de^德国|it^意大利|uk^英国|gb^英国|ua^乌克兰|co^哥伦比亚|ve^委内瑞拉|pe^秘鲁|cl^智利|ar^阿根廷|cr^哥斯达黎加|pa^巴拿马|do^多米尼加|gt^危地马拉|hn^洪都拉斯|ni^尼加拉瓜|sv^萨尔瓦多|bo^玻利维亚|py^巴拉圭|ec^厄瓜多尔|uy^乌拉圭|za^南非|ke^肯尼亚|eg^埃及|ng^尼日利亚|gh^加纳|ma^摩洛哥|dz^阿尔及利亚|tn^突尼斯|il^以色列|sa^沙特|ae^阿联酋|my^马来西亚|ph^菲律宾|sg^新加坡|jp^日本|kr^韩国|tw^台湾|hk^香港|mo^澳门|au^澳大利亚|nz^新西兰|ca^加拿大|mm^缅甸|kh^柬埔寨|la^老挝|np^尼泊尔|pk^巴基斯坦|bd^孟加拉|lk^斯里兰卡|af^阿富汗|kz^哈萨克斯坦|uz^乌兹别克斯坦|mn^蒙古|ge^格鲁吉亚|am^亚美尼亚|az^阿塞拜疆|by^白俄罗斯|md^摩尔多瓦|ro^罗马尼亚|hu^匈牙利|cz^捷克|sk^斯洛伐克|at^奥地利|ch^瑞士|be^比利时|nl^荷兰|lu^卢森堡|dk^丹麦|no^挪威|se^瑞典|fi^芬兰|is^冰岛|ie^爱尔兰|pt^葡萄牙|gr^希腊|hr^克罗地亚|si^斯洛文尼亚|rs^塞尔维亚|mk^北马其顿|ba^波黑|me^黑山|bg^保加利亚|lt^立陶宛|lv^拉脱维亚|ee^爱沙尼亚|cy^塞浦路斯|mt^马耳他|ls^莱索托|bw^博茨瓦纳|zw^津巴布韦|zm^赞比亚|mw^马拉维|mg^马达加斯加|mu^毛里求斯|sc^塞舌尔|ru2^俄罗斯\"};\nfunction lgSplit(s){var o={};if(!s)return o;var a=s.split('|');for(var i=0;i<a.length;i++){var p=a[i].indexOf('^');if(p>0)o[a[i].slice(0,p)]=a[i].slice(p+1);}return o;}\nvar LG_MAP=lgSplit(MAP_RAW);\nvar LG_P={};for(var __g in P_RAW)LG_P[__g]=lgSplit(P_RAW[__g]);\nvar LG_PREFIXES=[\"tagLanguage\",\"privatePrice\",\"autoTag\",\"bodyType\",\"hairColor\",\"ethnicity\",\"specifics\",\"specific\",\"do\",\"country:\"];\nfunction lgLook(k){if(LG_MAP[k])return LG_MAP[k];for(var i=0;i<LG_PREFIXES.length;i++){var p=LG_PREFIXES[i];if(k.indexOf(p)===0&&k!==p){var r=k.slice(p.length);var m=LG_P[p];if(m&&m[r]!==undefined)return m[r];}}return null;}\nfunction lgMap(k){var v=lgLook(String(k));return v===null?undefined:v;}\nfunction lgTag(k){var x=String(k);var v=lgLook(x);if(v!==null)return v;if(x.indexOf('-')>-1){var ps=x.split('-'),r=[];for(var qi=0;qi<ps.length;qi++){r.push(lgTag(ps[qi]));}return r.join('\/');}return x;}\nfunction lgText(k){return lgTag(k);}\nfunction lgInt(k){var v=lgLook(String(k));return v===null?String(k):v;}\nfunction lgCountry(k){if(!k)return '';var l=String(k).toLowerCase();var v=(LG_P.country&&LG_P.country[l]!==undefined)?LG_P.country[l]:null;return v!==null?v:String(k).toUpperCase();}\nfunction lgGender(g){var v=lgLook(String(g));return v===null?String(g):v;}\nfunction lgNum(n){var s=String(n),o='',c=0;for(var i=s.length-1;i>=0;i--){o=s.charAt(i)+o;c++;if(c%3===0&&i>0)o=','+o;}return o;}\nfunction lgTime(iso){if(!iso)return '';var t=new Date(iso);if(isNaN(t.getTime()))return '';function p(n){return n<10?'0'+n:''+n;}return p(t.getMonth()+1)+'-'+p(t.getDate())+' '+p(t.getHours())+':'+p(t.getMinutes());}\nfunction lgLangList(u){if(!u||!u.languages||!u.languages.length)return '';var a=[];for(var i=0;i<u.languages.length&&i<6;i++)a.push(lgTag(u.languages[i]));return a.join('\/');}\nfunction lgRates(u,labelP2P,withRec){if(!u)return '';var a=[];if(withRec&&u.publicRecordingsRate!=null)a.push('录像 '+u.publicRecordingsRate);if(u.privateRate!=null)a.push('私秀 '+u.privateRate);if(u.p2pRate!=null)a.push((labelP2P||'1v1')+' '+u.p2pRate);if(u.spyRate!=null)a.push('窥视 '+u.spyRate);return a.join(' · ');}\n\n\/\/ ===== 规则渲染函数 =====\nvar ACC_URL='https:\/\/zh.virtualtaboo.live\/api\/front\/v3\/config\/initial-dynamic';\nfunction lgViewerUrl(u){var g={'female':'girls','male':'men','group':'couples','tranny':'trans','trans':'trans','couples':'couples','maleFemale':'couples'}[(u.broadcastGender||u.gender||'')]||'girls';return 'https:\/\/zh.virtualtaboo.live\/api\/front\/v4\/models\/search\/group\/username?query='+encodeURIComponent(u.username)+'&limit=20&offset=0&primaryTag='+g+'&uniq=';}\nfunction lgDetailIntro(result,accountJson,viewerJson,J){var d=(typeof result==='string')?JSON.parse(result):result,u=(d.user&&d.user.user)||{},c=(d.cam||{}),out=[],acc={};try{var cfg=JSON.parse(String(accountJson||'{}'));acc=(cfg.initialDynamic&&cfg.initialDynamic.user)||cfg.user||{};}catch(e){}\nif(acc&&acc.username){out.push('👤 当前账号:'+acc.username);var line='🪙 剩余代币:'+(acc.tokens==null?'—':acc.tokens);if(acc.tokensPaid!=null)line+='(累计付费 '+acc.tokensPaid+')';out.push(line);var bd=[];if(acc.isUltimate)bd.push('Ultimate');if(acc.isExGreen)bd.push('ExGreen');if(acc.isGreen)bd.push('Green');if(acc.isRegular)bd.push('Regular');if(acc.isGold)bd.push('Gold');if(bd.length)out.push('🎖️ 会员:'+bd.join(' · '));}else{out.push('👤 当前账号:未登录');}\nvar vc=null,isNew=false,fc=(d.cam&&d.cam.userFanClub)||null;if(viewerJson){try{var arr=(JSON.parse(String(viewerJson)).models)||[];for(var i=0;i<arr.length;i++){if(String(arr[i].id)===String(u.id)){vc=arr[i].viewersCount!=null?arr[i].viewersCount:null;isNew=!!arr[i].isNew;break;}}}catch(e){}}\nif(u.isLive&&vc==null){try{vc=lgViewersCount(u,J);}catch(e){}}\nout.push('📡 状态:'+(u.isLive?'在线':'离线')+(u.isHd?' · 高清':'')+(u.isVr?' · VR':'')+(vc!=null?' · 观众 '+vc:''));\nvar cap=[];if(u.doPrivate)cap.push('私秀');if(u.doP2p)cap.push('1v1');if(u.doSpy)cap.push('窥视');out.push('🎫 互动:'+(cap.length?'可'+cap.join(' \/ '):'仅公开'));\nvar extra=[];if(u.isStorePrivateRecordings!==undefined)extra.push('🎬 可录私秀:'+(u.isStorePrivateRecordings?'是':'否'));var st=lgTime((d.cam&&d.cam.show&&d.cam.show.createdAt)?d.cam.show.createdAt:(u.statusChangedAt||''));if(st)extra.push('🕒 最近开播:'+st);if(extra.length)out.push(extra.join(' · '));\nvar meta=[];if(u.favoritedCount!=null)meta.push('⭐ 收藏 '+lgNum(u.favoritedCount));if(isNew)meta.push('🆕 新主播');if(fc&&fc.isActive!==undefined)meta.push('💠 粉丝团'+(fc.isActive?'已开通':'未开通')+(fc.subscription?'(已订阅)':''));if(meta.length)out.push(meta.join(' · '));\nvar m=[];if(u.age)m.push('年龄 '+u.age);if(u.gender)m.push(lgGender(u.gender));if(u.ethnicity)m.push(lgTag(u.ethnicity));if(u.bodyType)m.push(lgTag(u.bodyType));if(u.hairColor)m.push(lgTag(u.hairColor));if(u.country)m.push(String(u.country).toUpperCase());var lgs=lgLangList(u);if(lgs)m.push('语言 '+lgs);if(m.length)out.push('📋 主播:'+m.join(' · '));\nvar tags=(d.user&&d.user.tags)||[];if(tags.length){var tl=[];for(var ti=0;ti<tags.length&&tl.length<10;ti++){var tv=lgTag(tags[ti]);if(tl.join('|').indexOf(tv)<0)tl.push(tv);}if(tl.length)out.push('🏷️ 标签:'+tl.join(' · '));}\nvar iv=u.interests||[];if(iv.length){var il=[];for(var ii=0;ii<iv.length&&il.length<8;ii++)il.push(lgInt(iv[ii]));out.push('💡 兴趣:'+il.join(' \/ '));}\nvar rt=lgRates(u,'1v1',false);if(rt)out.push('💰 价格(token\/分):'+rt);var __goal=c.goal;if(__goal&&__goal.isEnabled){var __sp=(__goal.spent||0),__tt=(__goal.goal||0),__lf=(__goal.left!=null?__goal.left:(__tt-__sp));out.push('🎯 目标:'+(__goal.description||'')+'('+__sp+'\/'+__tt+',剩余 '+__lf+')');}\nvar __flags=[];if(d.user){__flags.push(d.user.isInFavorites?'❤️ 已收藏':'🤍 未收藏');if(d.user.isSubscribed)__flags.push('👤 已关注');if(d.user.isPmSubscribed)__flags.push('💌 私信订阅');}if(__flags.length)out.push(__flags.join(' · '));\nvar __recs=[];if(u.isStorePublicRecordings!==undefined)__recs.push('公开录像 '+(u.isStorePublicRecordings?'可看':'不可看'));if(__recs.length)out.push('🎞️ '+__recs.join(' · '));\nvar __gs=c.show&&c.show.details&&c.show.details.groupShow;if(__gs){out.push('🎟️ 群秀:'+(__gs.topic||'').slice(0,60)+( __gs.rate?('('+__gs.rate+' 代币\/分)'):''));}\nreturn out.join('\\n');}\nfunction lgSearchIntro(result){var u=(typeof result==='string')?JSON.parse(result):result,lines=[],st={'public':'免费','groupShow':'付费','p2p':'一对一','off':'离线'};lines.push('状态: '+(u.isLive?'在线':'离线')+' · '+(st[u.status]||u.status||'未知'));\nvar meta=[];if(u.age)meta.push('年龄 '+u.age);if(u.gender)meta.push({'male':'男','female':'女','trans':'跨性别','maleFemale':'双性'}[u.gender]||u.gender);if(u.ethnicity)meta.push('人种 '+lgText(u.ethnicity));if(u.country){var ccv=lgCountry(u.country);if(ccv)meta.push('国家 '+ccv);}if(u.city)meta.push('城市 '+u.city);if(u.region)meta.push('地区 '+u.region);if(meta.length)lines.push(meta.join(' · '));\nvar look=[];if(u.bodyType)look.push('体型 '+lgText(u.bodyType));if(u.hairColor)look.push('发 '+lgText(u.hairColor));if(u.eyeColor)look.push('瞳 '+lgText(u.eyeColor));if(u.subculture)look.push('风格 '+lgText(u.subculture));if(look.length)lines.push(look.join(' · '));\nvar lgs=lgLangList(u);if(lgs)lines.push('语言: '+lgs);if(u.specifics&&u.specifics.length)lines.push('特征: '+u.specifics.slice(0,6).map(lgTag).join(' '));if(u.publicActivities&&u.publicActivities.length)lines.push('活动: '+u.publicActivities.slice(0,8).map(lgTag).join(' '));if(u.privateActivities&&u.privateActivities.length)lines.push('私秀活动: '+u.privateActivities.slice(0,4).map(lgTag).join(' '));\nvar rt=lgRates(u,'一对一',true);if(rt)lines.push('价格(token\/分): '+rt);var caps=[];if(u.doSpy)caps.push('可窥视');if(u.doP2p)caps.push('可1v1');if(u.doPrivate)caps.push('可私秀');if(u.isStorePrivateRecordings)caps.push('有私录');if(u.isStorePublicRecordings)caps.push('有公录');if(u.isHd)caps.push('高清');if(u.isVr)caps.push('VR');if(u.isMobile)caps.push('手机');if(u.isGold)caps.push('金卡');if(u.hallOfFame)caps.push('名人堂');if(caps.length)lines.push('能力: '+caps.join(' '));if(u.favoritedCount)lines.push('收藏: '+lgNum(u.favoritedCount));return lines.join('\\n');}\nfunction lgBuildToc(result,text,J){ return (function(){var d=(typeof result==='string')?JSON.parse(result):result;var it=d.item||{};var paid=(it.status==='groupShow'||it.status==='p2p');var presets=((it.settings&&it.settings.presets)||[]).filter(function(p){return p&&p.indexOf('blurred')===-1});if(paid){presets=[];}if(!presets.length){presets=['auto'];if(paid){presets=['160p_blurred'];}}var lv=it.isLive?'在线':'离线';var pt={'public':'免费','groupShow':'付费','p2p':'一对一','private':'私秀','exclusivePrivate':'专属私秀','idle':'空闲','off':'离线'}[it.status]||'未知';var np=lv;if(pt&&pt!==lv)np+=' · '+pt;if(it.isLive&&J){var vn=null;try{vn=lgViewersCount({id:it.modelId,username:it.username,isLive:it.isLive},J);}catch(e){}if(vn!=null)np+=' · '+vn+'人';}var cdns=['doppiocdn.media','doppiocdn.net','doppiocdn.org','doppiocdn.live','doppiocdn.com','doppiocdn1.com'];var cn=['一','二','三','四','五','六'];var out=[];for(var i=0;i<cdns.length;i++){var c=cdns[i];out.push(JSON.stringify({n:'域名'+cn[i],u:'',v:1}));for(var j=0;j<presets.length;j++){var p=presets[j];out.push(JSON.stringify({n:np+' · '+(p==='auto'?'自动':p),u:(text?'https:\/\/zh.virtualtaboo.live\/'+it.username:'https:\/\/edge-hls.'+c+'\/hls\/'+it.modelId+'\/master\/'+it.modelId+'_'+p+'.m3u8?pkey=bXorqTB5ZhP5FcpX'),v:0}));}}return out})(); }\n\nfunction lgPaidSet(k){var S=this.source,J=this.java;try{if(!S&&typeof source!=='undefined')S=source;}catch(e){}try{if(!J&&typeof java!=='undefined')J=java;}catch(e){}var v={};try{v=JSON.parse(String(S.getVariable()||'{}'));}catch(e){}v.showPaid=(String(k).indexOf('隐藏')>-1)?'0':'1';try{S.putVariable(JSON.stringify(v));}catch(e){}try{J.refreshExplore();}catch(e){}}\n\nfunction lgPlaySet(k){var S=this.source,J=this.java;try{if(!S&&typeof source!=='undefined')S=source;}catch(e){}try{if(!J&&typeof java!=='undefined')J=java;}catch(e){}var v={};try{v=JSON.parse(String(S.getVariable()||'{}'));}catch(e){}v.playMode=(String(k).indexOf('网页')>-1)?'web':'native';try{S.putVariable(JSON.stringify(v));}catch(e){}try{J.refreshExplore();}catch(e){}try{J.refreshBookToc();}catch(e){}try{J.refreshContent();}catch(e){}}\n\nfunction lgViewersCount(u,J){\n J=J||null; if(!J) return null;\n var id=u&&u.id; if(id==null) return null;\n var gmap={'female':'girls','male':'men','group':'couples','trans':'trans','couples':'couples','maleFemale':'couples'};\n var pg=gmap[(u.broadcastGender||u.gender||'')]||'girls';\n try{\n var ju=JSON.parse(String(J.ajax('https:\/\/zh.virtualtaboo.live\/api\/front\/v4\/models\/search\/group\/username?query='+encodeURIComponent(u.username||'')+'&limit=20&offset=0&primaryTag='+pg+'&uniq=')));\n var arr=ju.models||[];\n for(var i=0;i<arr.length;i++){if(String(arr[i].id)===String(id)){if(arr[i].viewersCount!=null)return arr[i].viewersCount;break;}}\n }catch(e){}\n var tags=[pg];\n var all=['girls','couples','men','trans'];\n for(var t=0;t<all.length;t++){if(all[t]!==pg)tags.push(all[t]);}\n for(var k=0;k<tags.length;k++){\n try{\n var lj=JSON.parse(String(J.ajax('https:\/\/zh.virtualtaboo.live\/api\/front\/models?removeShows=false&recInFeatured=false&limit=60&offset=0&primaryTag='+tags[k]+'&sortBy=stripRanking&userRole=user&sac=true&uniq=')));\n var ms=lj.models||[];\n for(var j=0;j<ms.length;j++){if(String(ms[j].id)===String(id))return ms[j].viewersCount;}\n }catch(e){}\n }\n return null;\n}\n",
"lastUpdateTime": "1793841096375",
"loginUrl": "https:\/\/zh.virtualtaboo.live\/",
"respondTime": 7396,
"ruleBookInfo": {
"author": "{{$.user.user.name}}",
"coverUrl": "@js:(function(){var d=(typeof result==='string')?JSON.parse(result):result;var u=(d.user&&d.user.user)||{},c=d.cam||{};if(u.snapshotTimestamp&&c.streamName)return 'https:\/\/img.doppiocdn.com\/thumbs\/'+u.snapshotTimestamp+'\/'+c.streamName+'_webp';return u.avatarUrlThumb||u.avatarUrl||'';})()",
"intro": "@js:(function(){\nvar d=(typeof result==='string')?JSON.parse(result):result;\nvar u=(d.user&&d.user.user)||{};\nvar acc='',vw='';\ntry{var c=cache.get('lg_acc');if(!c||String(c).indexOf('initialDynamic')<0){c=java.ajax(ACC_URL);cache.put('lg_acc',c,120);}acc=c;}catch(e){}\ntry{if(u.isLive&&u.username&&String(u.username).length>=3)vw=java.ajax(lgViewerUrl(u));}catch(e){}\nreturn lgDetailIntro(result,acc,vw,java);\n})()",
"kind": "@js:(function(){\nvar d=result;\nif(typeof d==='string'){try{d=JSON.parse(d);}catch(e){d=null;}}\nif(!d)return '';\nvar u=(d.user&&d.user.user)||d;\nvar parts=[];\nif(u.country)parts.push(String(u.country).toUpperCase());\nparts.push({'public':'免费','groupShow':'付费'}[u.status||d.status]||'离线');\nvar vw=(u.viewersCount!=null)?u.viewersCount:d.viewersCount;\nif(u.isLive&&(vw==null||vw===0)){try{vw=lgViewersCount(u,java);}catch(e){}}\nif(u.isLive)parts.push((vw!=null?vw:0)+'在看');\nreturn parts.join(' | ');\n})()",
"lastChapter": "@js:(function(){var d=(typeof result==='string')?JSON.parse(result):result;var u=(d.user&&d.user.user)||{},c=d.cam||{};var t=(c.show&&c.show.createdAt)?c.show.createdAt:(u.statusChangedAt||'');if(u.isLive)return '本次开播:'+lgTime(t);if(u.wentIdleAt)return '上次在线:'+lgTime(u.wentIdleAt);return t?('最近开播:'+lgTime(t)):'';})()",
"name": "{{$.user.user.username}}",
"tocUrl": "https:\/\/zh.virtualtaboo.live\/api\/front\/v2\/broadcasts\/{{$.user.user.id}}?uniq=ix9yufd20we8bscq"
},
"ruleContent": {
"callBackJs": "(function(){\n try{\n var ev=String(event||'');\n if(ev!=='addBookShelf' && ev!=='delBookShelf') return true;\n var m=String((book&&book.bookUrl)||'').match(\/models\\\/(\\d+)\/);\n var id=m?m[1]:'';\n if(!id) return true;\n var ck=String(cookie.getCookie('https:\/\/zh.virtualtaboo.live\/')||'');\n if(!ck) return true;\n var cfg=JSON.parse(String(java.ajax('https:\/\/zh.virtualtaboo.live\/api\/front\/v3\/config\/initial-dynamic')));\n var d=(cfg&&cfg.initialDynamic)||{};\n var uid=(d.user&&d.user.id)||'';\n if(!uid) return true;\n var csrf=d.csrfToken||'',ts=d.csrfTimestamp||'',nt=d.csrfNotifyTimestamp||'';\n var body=JSON.stringify({csrfToken:csrf,csrfTimestamp:ts,csrfNotifyTimestamp:nt,uniq:'u'+Date.now()});\n var client=new Packages.okhttp3.OkHttpClient();\n var mt=Packages.okhttp3.MediaType.parse('application\/json');\n var rb=Packages.okhttp3.RequestBody.create(mt,body);\n var builder=new Packages.okhttp3.Request.Builder()\n .url('https:\/\/zh.virtualtaboo.live\/api\/front\/users\/'+uid+'\/favorites\/'+id)\n .addHeader('Cookie',ck)\n .addHeader('Accept','application\/json')\n .addHeader('User-Agent','Mozilla\/5.0 (Linux; Android 13) Chrome\/120 Mobile');\n if(ev==='addBookShelf'){ builder.put(rb); }\n else { builder.delete(rb); }\n var resp=client.newCall(builder.build()).execute();\n if(resp.code()===200){ java.toast(ev==='addBookShelf'?'已同步到站点收藏':'已取消站点收藏'); }\n else { java.toast('收藏同步失败('+resp.code()+')'); }\n }catch(e){ try{java.toast('收藏同步异常:'+e);}catch(x){} }\n return true;\n})()",
"content": "@js:(function(){\nvar LG_PLAYER_HTML=\"<!DOCTYPE html>\\n<html lang=\\\"zh-CN\\\">\\n<head>\\n <meta charset=\\\"UTF-8\\\">\\n <meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\\\">\\n <link rel=\\\"preconnect\\\" href=\\\"https:\/\/cdn.jsdelivr.net\\\" crossorigin>\\n <link rel=\\\"preconnect\\\" href=\\\"https:\/\/edge-hls.doppiocdn.media\\\" crossorigin>\\n <link rel=\\\"dns-prefetch\\\" href=\\\"\/\/edge-hls.doppiocdn.media\\\">\\n <link rel=\\\"dns-prefetch\\\" href=\\\"\/\/edge-hls.doppiocdn.net\\\">\\n <title><\/title>\\n<\/head>\\n <style>\\n * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Microsoft Yahei', sans-serif; }\\n body { color: #333; padding: 0.1rem; min-height: 100vh; background: #f5f5f5; }\\n .app-container { display: grid; grid-template-columns: 1fr; gap: 1rem; max-width: 1200px; margin: 0 auto; }\\n .live-section { border-radius: 12px; overflow: hidden; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); border: 1px solid #eaeaea; background: #fff; }\\n .topic-section { padding: 14px 20px; border-bottom: 1px solid #eaeaea; background: #fff; display: none; }\\n .topic-label { font-size: 12px; color: #666; margin-bottom: 6px; display: flex; align-items: center; gap: 8px; font-weight: 500; }\\n .topic-content { font-size: 15px; color: #333; font-weight: 500; line-height: 1.4; }\\n .chat-section { border-radius: 12px; padding: 1rem; display: flex; flex-direction: column; height: 500px; border: 1px solid #eaeaea; background: #fff; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); }\\n .chat-messages { flex: 1; overflow-y: auto; padding-right: 0.5rem; width: 100%; }\\n .chat-messages::-webkit-scrollbar { width: 6px; }\\n .chat-messages::-webkit-scrollbar-track { background: #f0f0f0; border-radius: 3px; }\\n .chat-messages::-webkit-scrollbar-thumb { background: #c0c0c0; border-radius: 3px; }\\n .message-wrapper { display: block; margin-bottom: 0.3rem; width: 100%; }\\n .message-item { padding: 0.3rem 0.6rem; border-radius: 16px; font-size: 0.85rem; line-height: 1.3; border: 1px solid rgba(224, 224, 224, 0.6); display: block; background: #f8f8f8; color: #333; width: 100%; word-break: break-word; box-sizing: border-box; }\\n .lovense-item { border-color: #e0e0e0; background: #f8f8f8; }\\n .tip-item { border-color: #e0e0e0; background: #f8f8f8; }\\n .system-message { border-color: #e0e0e0; color: #666; font-size: 0.85rem; margin: 0 auto 0.3rem auto; background: #f8f8f8; }\\n .user-name { font-weight: 600; color: #222; margin-left: 0.4rem; }\\n .level-tag { font-size: 0.75rem; color: #666; background: #f0f0f0; padding: 0.1rem 0.4rem; border-radius: 8px; }\\n .tip-amount { color: #e74c3c; font-weight: 600; }\\n .live-header { padding: 16px 20px; border-bottom: 1px solid #eaeaea; display: flex; align-items: center; gap: 15px; background: #fff; }\\n .avatar-container { width: 56px; height: 56px; flex-shrink: 0; }\\n .avatar { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; border: 3px solid #007aff; }\\n .host-info { flex: 1; min-width: 0; }\\n .host-info h2 { font-size: 18px; margin-bottom: 6px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #333; }\\n .status-container { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }\\n .live-badge { display: inline-flex; align-items: center; background: #ff3b30; color: white; padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; letter-spacing: 0.3px; }\\n .live-badge::before { content: ''; display: inline-block; width: 6px; height: 6px; background-color: white; border-radius: 50%; margin-right: 6px; animation: blink 1.5s infinite; }\\n @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\\n .viewer-count { font-size: 13px; color: #666; display: flex; align-items: center; gap: 5px; }\\n .player-container { position: relative; background: #000000; overflow: hidden; }\\n .player-container:fullscreen { display: flex; align-items: center; justify-content: center; }\\n .player-container:-webkit-full-screen { display: flex; align-items: center; justify-content: center; }\\n #dplayer { width: 100% !important; height: 100% !important; display: flex; align-items: center; }\\n .dplayer-bezel, .dplayer-bezel-icon { display: none !important; }\\n .dplayer-mobile-play { display: none !important; }\\n .center-play-btn { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 58px; height: 58px; border: none; border-radius: 50%; background: rgba(0,0,0,0.45); color: #fff; font-size: 22px; display: none; align-items: center; justify-content: center; z-index: 1300; cursor: pointer; -webkit-tap-highlight-color: transparent; pointer-events: auto !important; touch-action: manipulation; }\\n .player-container.controls-visible .center-play-btn, .player-container.video-paused .center-play-btn { display: flex; }\\n #dplayer video { object-fit: contain !important; }\\n .dplayer-controller { display: none !important; }\\n .dplayer-notice { display: none !important; }\\n .custom-controls { position: absolute; bottom: 0; left: 0; right: 0; padding: 12px 15px 6px; display: flex; align-items: center; justify-content: space-between; opacity: 0; transition: opacity 0.2s; z-index: 1000; pointer-events: none; }\\n .player-container.controls-visible .custom-controls { opacity: 1; pointer-events: auto; }\\n .player-container.controls-hidden .custom-controls { opacity: 0; pointer-events: none; }\\n .left-controls, .right-controls { display: flex; align-items: center; gap: 8px; pointer-events: auto; }\\n .rotate-btn { background: rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.25); color: rgba(255, 255, 255, 0.9); padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; white-space: nowrap; display: flex; align-items: center; justify-content: center; transition: all 0.15s; min-width: 52px; pointer-events: auto; }\\n .rotate-btn:hover { background: rgba(255, 255, 255, 0.25); color: white; }\\n .control-btn { background: rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.25); color: #ffffff; width: 36px; height: 36px; border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 16px; transition: all 0.15s; pointer-events: auto; }\\n .control-btn:disabled { opacity: 0.5; cursor: not-allowed; }\\n .selector-btn { background: rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.25); color: #ffffff; padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; white-space: nowrap; display: flex; align-items: center; gap: 5px; transition: all 0.15s; min-width: 65px; justify-content: center; pointer-events: auto; }\\n .selector-menu { position: absolute; bottom: 42px; background: #ffffff; border-radius: 8px; min-width: 90px; display: none; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16); border: 1px solid #d1d1d6; overflow: hidden; z-index: 1001; pointer-events: auto; }\\n .menu-item { padding: 8px 12px; color: #333; cursor: pointer; font-size: 13px; transition: all 0.15s; border-bottom: 1px solid #eaeaea; display: flex; align-items: center; justify-content: space-between; pointer-events: auto; }\\n .menu-item:last-child { border-bottom: none; }\\n .menu-item:hover:not(:disabled) { background: #007aff; color: white; }\\n .menu-item.active { background: rgba(10, 132, 255, 0.1); color: #007aff; }\\n .menu-item.active:after { content: \\\"✓\\\"; font-size: 12px; color: #007aff; }\\n .loading-indicator { position: absolute; top: 15px; left: 15px; background: rgba(0, 0, 0, 0.8); color: white; padding: 6px 12px; border-radius: 4px; font-size: 12px; z-index: 1002; display: none; border: 1px solid rgba(255, 255, 255, 0.1); }\\n .floating-button { position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; border-radius: 50%; background: #007aff; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3); z-index: 10000; border: none; font-size: 24px; }\\n .floating-panel { position: fixed; bottom: 90px; right: 20px; width: 400px; background: #ffffff; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); padding: 20px; z-index: 9999; border: 1px solid #eaeaea; display: none; max-height: 80vh; overflow-y: auto; }\\n .floating-panel::-webkit-scrollbar { width: 6px; }\\n .floating-panel::-webkit-scrollbar-track { background: #f0f0f0; border-radius: 3px; }\\n .floating-panel::-webkit-scrollbar-thumb { background: #c0c0c0; border-radius: 3px; }\\n .floating-panel-title { font-size: 18px; font-weight: 600; margin-bottom: 15px; color: #333; padding-bottom: 10px; border-bottom: 1px solid #eaeaea; }\\n .floating-info-section { margin-bottom: 20px; }\\n .floating-info-section-title { font-size: 15px; color: #007aff; margin-bottom: 12px; font-weight: 600; display: flex; align-items: center; gap: 8px; }\\n .floating-info-item { margin-bottom: 12px; display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f5f5f5; }\\n .floating-info-item:last-child { border-bottom: none; margin-bottom: 0; }\\n .floating-info-label { font-size: 13px; color: #666; min-width: 120px; }\\n .floating-info-value { font-size: 14px; color: #333; font-weight: 500; text-align: right; flex: 1; }\\n .floating-content { background: #f8f9fa; border: 1px solid #eaeaea; border-radius: 8px; padding: 12px; font-size: 14px; color: #333; line-height: 1.5; margin-bottom: 15px; }\\n .floating-price-list { display: flex; flex-direction: column; gap: 8px; }\\n .floating-price-item { background: #ffffff; border: 1px solid #eaeaea; border-radius: 8px; padding: 10px 12px; display: flex; align-items: center; justify-content: space-between; transition: all 0.2s; }\\n .floating-price-item:hover { border-color: #007aff; }\\n .floating-price-activity { font-size: 14px; color: #333; flex: 1; }\\n .floating-price-value { font-size: 14px; color: #ff3b30; font-weight: 600; }\\n @media (max-width: 768px) {\\n .floating-panel { width: 350px; right: 10px; bottom: 80px; }\\n .floating-button { width: 50px; height: 50px; font-size: 20px; right: 15px; bottom: 15px; }\\n .live-header { padding: 14px 16px; }\\n .avatar-container { width: 48px; height: 48px; }\\n .host-info h2 { font-size: 16px; }\\n }\\n @media (max-width: 480px) {\\n .floating-panel { width: 300px; }\\n .chat-section { height: 400px; }\\n .app-container { padding: 0.5rem; gap: 1rem; }\\n }\\n \\n \/* ===== 移动端适配 ===== *\/\\n html, body { width: 100%; max-width: 100%; overflow-x: hidden; }\\n body { padding: 0 !important; }\\n .app-container { padding: 8px !important; gap: 8px !important; }\\n .live-section, .chat-section { border-radius: 10px; }\\n .player-container { width: 100%; aspect-ratio: 16\/9; }\\n #dplayer, #dplayer video { width: 100% !important; height: 100% !important; }\\n .chat-section { height: 42vh; min-height: 220px; padding: 0.6rem; }\\n .live-header { padding: 10px 12px; gap: 10px; }\\n .avatar-container { width: 44px; height: 44px; }\\n .host-info h2 { font-size: 16px; }\\n .topic-content { font-size: 14px; }\\n .custom-controls { padding: 10px 10px 4px; }\\n .selector-btn, .control-btn, .rotate-btn { min-width: 44px; min-height: 36px; font-size: 13px; }\\n .control-btn { width: 40px; height: 40px; }\\n .floating-button { width: 44px; height: 44px; right: 12px; bottom: 12px; font-size: 18px; }\\n .floating-panel { width: auto !important; left: 8px; right: 8px; bottom: 68px; max-height: 68vh; padding: 14px; }\\n .menu-item { padding: 10px 12px; }\\n @media (max-width: 480px) {\\n .app-container { padding: 6px !important; gap: 6px !important; }\\n .chat-section { height: 38vh; min-height: 200px; }\\n .floating-panel { width: auto !important; left: 8px; right: 8px; }\\n }\\n \/* ===== 切换清晰度冻结帧 ===== *\/\\n .freeze-layer { position: absolute; inset: 0; z-index: 1200; background-color: #000; background-position: center; background-repeat: no-repeat; background-size: contain; pointer-events: none; }\\n \/* ===== 固定底栏(弹幕输入 + 信息入口) ===== *\/\\n #bottomBar { position: fixed; left: 50%; right: auto; transform: translateX(-50%); width: 100%; max-width: 1200px; bottom: 0; display: flex; align-items: center; gap: 8px;\\n padding: 8px 10px calc(8px + env(safe-area-inset-bottom)); background: #fff;\\n border-top: 1px solid #eee; box-shadow: 0 -2px 12px rgba(0,0,0,0.06); z-index: 2000; box-sizing: border-box; }\\n #bottomBar .floating-button { position: static; left: auto; right: auto; bottom: auto; width: 42px; height: 42px;\\n font-size: 17px; margin: 0; flex: 0 0 auto; }\\n .bar-btn { position: static; width: 42px; height: 42px; border: none; border-radius: 50%; background: #f2f3f7; color: #8a9099; font-size: 18px; display: flex; align-items: center; justify-content: center; cursor: pointer; margin: 0; padding: 0; flex: 0 0 auto; }\\n .bar-btn:active { background: #ffe3e8; }\\n #chatbar { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; width: 100%;\\n margin: 0; padding: 0; border: none; box-sizing: border-box; }\\n #ci { flex: 1 1 auto; min-width: 0; width: 100%; box-sizing: border-box; height: 42px; padding: 0 14px;\\n border: 1px solid #ddd; border-radius: 21px; font-size: 16px; line-height: 42px; outline: none;\\n background: #f7f7f8; color: #222; -webkit-appearance: none; }\\n #ci:focus { border-color: #e91e63; background: #fff; }\\n #cs { flex: 0 0 auto; height: 42px; padding: 0 18px; border: none; border-radius: 21px; background: #e91e63;\\n color: #fff; font-size: 15px; font-weight: 600; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; }\\n #cs:active { opacity: .85; }\\n .app-container { margin-bottom: calc(66px + env(safe-area-inset-bottom)); }\\n .floating-panel { bottom: 80px; }\\n @media (max-width: 480px) {\\n #bottomBar { gap: 6px; padding: 6px 8px calc(6px + env(safe-area-inset-bottom)); }\\n #bottomBar .floating-button { width: 44px; height: 44px; }\\n #ci { height: 44px; line-height: 44px; padding: 0 12px; }\\n #cs { height: 44px; padding: 0 16px; }\\n .app-container { margin-bottom: calc(70px + env(safe-area-inset-bottom)) !important; }\\n }\\n \/* ===== 聊天区自适应高度:随消息增多而变长 ===== *\/\\n .chat-section { height: auto !important; min-height: 220px; }\\n .chat-messages { flex: 1 1 auto; overflow-y: auto; min-height: 160px; max-height: 60vh; }\\n \/* ===== 播放器固定顶栏 ===== *\/\\n #playerContainer { position: fixed !important; top: 0; left: 50% !important; right: auto !important; transform: translateX(-50%); width: 100% !important; max-width: 1200px; z-index: 1500; border-radius: 0 !important; aspect-ratio: 16\/9; min-height: 140px; }\\n #playerContainer:fullscreen, #playerContainer:-webkit-full-screen { left: 0 !important; transform: none !important; max-width: none !important; width: 100vw !important; }\\n .app-container { padding-top: min(56.25vw, 75vh); }\\n \/* ===== 视觉优化 ===== *\/\\n .chat-emoji-img { width: 1.3em; height: 1.3em; vertical-align: -0.25em; margin: 0 1px; display: inline-block; }\\n \/* ===== 表情面板 ===== *\/\\n #emojiBtn { flex: 0 0 auto; width: 42px; height: 42px; border: none; border-radius: 50%; background: #f2f3f7; color: #ff3b5c; font-size: 19px; display: flex; align-items: center; justify-content: center; cursor: pointer; margin: 0; padding: 0; }\\n #emojiBtn:active { background: #ffe3e8; }\\n .emoji-panel { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(58px + env(safe-area-inset-bottom)); width: 100%; max-width: 1200px; max-height: 45vh; background: #fff; border-radius: 14px 14px 0 0; box-shadow: 0 -8px 24px rgba(0,0,0,0.16); z-index: 2500; display: none; flex-direction: column; overflow: hidden; }\\n .emoji-panel.open { display: flex; }\\n .emoji-panel-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; font-size: 14px; font-weight: 700; color: #1f2329; border-bottom: 1px solid #eee; }\\n .emoji-close { border: none; background: transparent; font-size: 22px; line-height: 1; color: #8a9099; cursor: pointer; padding: 0 4px; }\\n .emoji-grid { flex: 1 1 auto; display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; padding: 10px; overflow-y: auto; }\\n .emoji-item { display: flex; align-items: center; justify-content: center; height: 42px; border: none; background: transparent; border-radius: 10px; cursor: pointer; padding: 0; -webkit-tap-highlight-color: transparent; }\\n .emoji-item:active { background: #f2f3f7; }\\n .emoji-item img { width: 28px; height: 28px; display: block; pointer-events: none; }\\n @media (max-width: 480px) { .emoji-grid { grid-template-columns: repeat(6, 1fr); } #emojiBtn { width: 44px; height: 44px; } }\\n :root { --accent: #ff3b5c; --accent2: #ff7a45; --card: #ffffff; --bg: #f2f3f7; --text: #1f2329; --muted: #8a9099; }\\n html, body { background: var(--bg) !important; color: var(--text); }\\n .app-container { padding-left: 10px !important; padding-right: 10px !important; }\\n .live-section { border: none !important; box-shadow: none !important; background: transparent !important; overflow: visible !important; }\\n .live-header { padding: 10px 14px !important; border-radius: 14px; background: var(--card); border: none; box-shadow: 0 4px 16px rgba(0,0,0,0.06); gap: 12px; }\\n .avatar-container { width: 46px; height: 46px; }\\n .avatar { border: 2px solid var(--accent); }\\n .host-info h2 { font-size: 17px; font-weight: 700; color: var(--text); }\\n .host-info p { color: var(--muted); }\\n #playerContainer { border-radius: 0 0 16px 16px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.28); }\\n .custom-controls { background: linear-gradient(to top, rgba(0,0,0,0.72), rgba(0,0,0,0)); padding: 22px 12px 8px; }\\n .selector-btn, .control-btn { border-radius: 10px !important; backdrop-filter: blur(6px); }\\n .chat-section { border: none !important; box-shadow: 0 4px 16px rgba(0,0,0,0.06) !important; background: var(--card) !important; border-radius: 14px !important; padding: 10px 10px 6px !important; }\\n .chat-messages { padding: 2px 0 6px !important; }\\n .chat-messages::-webkit-scrollbar { width: 4px; }\\n .chat-messages::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.18); border-radius: 4px; }\\n .chat-messages:empty::after { content: '暂无弹幕,来说点什么吧~'; display: block; text-align: center; color: var(--muted); font-size: 13px; padding: 40px 0; }\\n .message-wrapper { margin-bottom: 7px !important; }\\n .message-item { padding: 8px 12px !important; border-radius: 14px !important; border: none !important; background: #f6f7f9 !important; font-size: 14px !important; line-height: 1.45 !important; color: var(--text); box-shadow: 0 1px 2px rgba(0,0,0,0.03); }\\n .message-item .user-name { color: var(--accent); font-weight: 700; margin-right: 4px; }\\n .level-tag { display: inline-block; padding: 1px 6px; margin-right: 5px; border-radius: 8px; font-size: 11px; font-weight: 700; color: #7a4b00; background: linear-gradient(135deg, #ffe08a, #ffc44d); }\\n .tip-item { background: linear-gradient(135deg, #fff4e5, #ffe6c7) !important; border: 1px solid #ffd8a8 !important; }\\n .tip-item .tip-amount { color: var(--accent2); font-weight: 800; }\\n .system-message { background: transparent !important; text-align: center; color: var(--muted) !important; font-size: 12px !important; box-shadow: none !important; padding: 4px 0 !important; }\\n .floating-panel { border-radius: 18px !important; border: none !important; box-shadow: 0 16px 48px rgba(0,0,0,0.22) !important; padding: 18px !important; }\\n .floating-panel-title { font-size: 17px; font-weight: 800; color: var(--text); margin-bottom: 12px; }\\n .floating-info-section-title { color: var(--accent) !important; font-weight: 700; }\\n #bottomBar .floating-button { background: linear-gradient(135deg, var(--accent), var(--accent2)) !important; box-shadow: 0 4px 14px rgba(255,59,92,0.35); font-size: 18px; }\\n #cs { background: linear-gradient(135deg, var(--accent), var(--accent2)) !important; box-shadow: 0 3px 10px rgba(255,59,92,0.28); }\\n #ci { background: #f2f3f7 !important; border-color: transparent !important; }\\n #ci:focus { background: #fff !important; border-color: var(--accent) !important; }\\n .loading-indicator { border-radius: 20px !important; background: rgba(0,0,0,0.65) !important; color: #fff !important; padding: 8px 18px !important; font-size: 13px; }\\n \/* 点击画面只显示控制条(中央按钮负责播放\/暂停) *\/\\n \/* ===== 控制栏移动端压缩间距(单行不重叠) ===== *\/\\n @media (max-width: 520px) {\\n .custom-controls { padding: 16px 8px 6px; gap: 4px; }\\n .left-controls, .right-controls { gap: 4px; }\\n .control-btn { width: 32px; height: 32px; font-size: 14px; }\\n .rotate-btn { padding: 5px 7px; font-size: 11px; }\\n .selector-btn { padding: 4px 6px; font-size: 11px; min-width: 44px; gap: 3px; }\\n .selector-menu { bottom: 36px; }\\n }\\n <\/style>\\n<body>\\n <div id=\\\"config\\\" style=\\\"display:none;\\\">\\n <div id=\\\"liveData\\\" style=\\\"display:none;\\\">__LIVE_DATA__<\/div>\\n <\/div>\\n \\n <div class=\\\"app-container\\\">\\n <div class=\\\"live-section\\\">\\n \\n \\n <div class=\\\"live-header\\\">\\n <div class=\\\"avatar-container\\\">\\n <img class=\\\"avatar\\\" id=\\\"avatarImg\\\" src=\\\"\\\" alt=\\\"\\\">\\n <\/div>\\n <div class=\\\"host-info\\\">\\n <h2 id=\\\"hostName\\\">正在加载...<\/h2>\\n <div class=\\\"status-container\\\">\\n <span class=\\\"live-badge\\\">直播中<\/span>\\n <div class=\\\"viewer-count\\\">\\n <i class=\\\"fas fa-users\\\"><\/i>\\n <span id=\\\"membersCount\\\">--<\/span> 人\\n <\/div>\\n <\/div>\\n <\/div>\\n <\/div>\\n \\n <div class=\\\"player-container\\\" id=\\\"playerContainer\\\">\\n <div id=\\\"dplayer\\\"><\/div>\\n <button id=\\\"centerPlayBtn\\\" class=\\\"center-play-btn\\\" title=\\\"播放\/暂停\\\"><i class=\\\"fas fa-play\\\"><\/i><\/button>\\n <div class=\\\"loading-indicator\\\" id=\\\"loadingIndicator\\\">正在切换...<\/div>\\n <div class=\\\"custom-controls\\\">\\n <div class=\\\"left-controls\\\">\\n <button class=\\\"control-btn\\\" id=\\\"refreshBtn\\\" title=\\\"刷新播放\\\">\\n <i class=\\\"fas fa-redo-alt\\\"><\/i>\\n <\/button>\\n <button class=\\\"control-btn\\\" id=\\\"volumeBtn\\\" title=\\\"静音\/取消静音\\\">\\n <i class=\\\"fas fa-volume-up\\\"><\/i>\\n <\/button>\\n <div class=\\\"rotate-btn\\\" id=\\\"rotateBtn\\\">竖屏<\/div>\\n <\/div>\\n <div class=\\\"right-controls\\\">\\n <div class=\\\"line-selector\\\">\\n <div class=\\\"selector-btn\\\" id=\\\"lineBtn\\\" title=\\\"选择线路\\\">线路1 <i class=\\\"fas fa-chevron-down\\\"><\/i><\/div>\\n <div class=\\\"selector-menu\\\" id=\\\"lineMenu\\\"><\/div>\\n <\/div>\\n <div class=\\\"quality-selector\\\">\\n <div class=\\\"selector-btn\\\" id=\\\"qualityBtn\\\" title=\\\"选择清晰度\\\">默认 <i class=\\\"fas fa-chevron-down\\\"><\/i><\/div>\\n <div class=\\\"selector-menu\\\" id=\\\"qualityMenu\\\"><\/div>\\n <\/div>\\n <button class=\\\"control-btn\\\" id=\\\"fullscreenBtn\\\" title=\\\"全屏\\\">\\n <i class=\\\"fas fa-expand\\\"><\/i>\\n <\/button>\\n <\/div>\\n <\/div>\\n <\/div>\\n <\/div>\\n \\n <div class=\\\"chat-section\\\">\\n <div class=\\\"chat-messages\\\" id=\\\"chatContainer\\\"><\/div>\\n <\/div>\\n <\/div>\\n \\n <div id=\\\"bottomBar\\\">\\n <button class=\\\"floating-button\\\" id=\\\"floatingBtn\\\" title=\\\"直播信息\\\"><i class=\\\"fas fa-info\\\"><\/i><\/button>\\n <button id=\\\"favBtn\\\" class=\\\"bar-btn\\\" title=\\\"收藏\\\"><i class=\\\"fas fa-heart\\\"><\/i><\/button>\\n <div id=\\\"chatbar\\\"><input id=\\\"ci\\\" placeholder=\\\"发送弹幕…\\\" maxlength=\\\"200\\\"><button id=\\\"emojiBtn\\\" title=\\\"表情\\\"><i class=\\\"fas fa-smile\\\"><\/i><\/button><button id=\\\"cs\\\">发送<\/button><\/div>\\n <\/div>\\n\\n <div id=\\\"emojiPanel\\\" class=\\\"emoji-panel\\\">\\n <div class=\\\"emoji-panel-head\\\"><span>发送表情<\/span><button id=\\\"emojiClose\\\" class=\\\"emoji-close\\\">×<\/button><\/div>\\n <div class=\\\"emoji-grid\\\" id=\\\"emojiGrid\\\"><\/div>\\n <\/div>\\n \\n <div class=\\\"floating-panel\\\" id=\\\"floatingPanel\\\">\\n <div class=\\\"floating-panel-title\\\">直播信息<\/div>\\n \\n <div class=\\\"floating-info-section\\\" id=\\\"floatingTopicSection\\\">\\n <div class=\\\"floating-info-section-title\\\">\\n <i class=\\\"fas fa-bullhorn\\\"><\/i> 直播标题\\n <\/div>\\n <div class=\\\"floating-content\\\" id=\\\"floatingTopic\\\"><\/div>\\n <\/div>\\n <div class=\\\"floating-info-section\\\">\\n <div class=\\\"floating-info-section-title\\\">\\n <i class=\\\"fas fa-user\\\"><\/i> 主播信息\\n <\/div>\\n <div class=\\\"floating-info-item\\\">\\n <span class=\\\"floating-info-label\\\">主播名称:<\/span>\\n <span class=\\\"floating-info-value\\\" id=\\\"floatingHostName\\\">--<\/span>\\n <\/div>\\n <div class=\\\"floating-info-item\\\">\\n <span class=\\\"floating-info-label\\\">历史最高排行:<\/span>\\n <span class=\\\"floating-info-value\\\" id=\\\"floatingTopBestPlace\\\">--<\/span>\\n <\/div>\\n <div class=\\\"floating-info-item\\\">\\n <span class=\\\"floating-info-label\\\">总人数:<\/span>\\n <span class=\\\"floating-info-value\\\" id=\\\"floatingMembersCount\\\">--<\/span>\\n <\/div>\\n <div class=\\\"floating-info-item\\\">\\n <span class=\\\"floating-info-label\\\">首次开播日期:<\/span>\\n <span class=\\\"floating-info-value\\\" id=\\\"floatingTipMenuCreatedAt\\\">--<\/span>\\n <\/div>\\n <div class=\\\"floating-info-item\\\">\\n <span class=\\\"floating-info-label\\\" id=\\\"floatingDurationLabel\\\">时长标签:<\/span>\\n <span class=\\\"floating-info-value\\\" id=\\\"floatingDuration\\\">--<\/span>\\n <\/div>\\n <div class=\\\"floating-info-item\\\">\\n <span class=\\\"floating-info-label\\\" id=\\\"floatingTimeLabel\\\">时间标签:<\/span>\\n <span class=\\\"floating-info-value\\\" id=\\\"floatingStatusTime\\\">--<\/span>\\n <\/div>\\n <\/div>\\n \\n <div class=\\\"floating-info-section\\\" id=\\\"floatingUserDescriptionSection\\\">\\n <div class=\\\"floating-info-section-title\\\">\\n <i class=\\\"fas fa-user-circle\\\"><\/i> 主播介绍\\n <\/div>\\n <div class=\\\"floating-content\\\" id=\\\"floatingUserDescription\\\"><\/div>\\n <\/div>\\n \\n <div class=\\\"floating-info-section\\\" id=\\\"floatingGoalSection\\\">\\n <div class=\\\"floating-info-section-title\\\">\\n <i class=\\\"fas fa-bullseye\\\"><\/i> 直播目标\\n <\/div>\\n <div class=\\\"floating-content\\\" id=\\\"floatingGoalDescription\\\"><\/div>\\n <\/div>\\n \\n <div class=\\\"floating-info-section\\\" id=\\\"floatingFanClubSection\\\">\\n <div class=\\\"floating-info-section-title\\\">\\n <i class=\\\"fas fa-users\\\"><\/i> 粉丝团留言\\n <\/div>\\n <div class=\\\"floating-content\\\" id=\\\"floatingFanClubDescription\\\"><\/div>\\n <\/div>\\n \\n <div class=\\\"floating-info-section\\\">\\n <div class=\\\"floating-info-section-title\\\">\\n <i class=\\\"fas fa-gift\\\"><\/i> 打赏菜单\\n <\/div>\\n <div class=\\\"floating-price-list\\\" id=\\\"floatingPriceList\\\"><\/div>\\n <\/div>\\n \\n <div class=\\\"floating-info-section\\\">\\n <div class=\\\"floating-info-section-title\\\">\\n <i class=\\\"fas fa-tv\\\"><\/i> 播放设置\\n <\/div>\\n <div class=\\\"floating-info-item\\\">\\n <span class=\\\"floating-info-label\\\">当前线路:<\/span>\\n <span class=\\\"floating-info-value\\\" id=\\\"floatingLine\\\">线路1<\/span>\\n <\/div>\\n <div class=\\\"floating-info-item\\\">\\n <span class=\\\"floating-info-label\\\">清晰度:<\/span>\\n <span class=\\\"floating-info-value\\\" id=\\\"floatingQuality\\\">默认<\/span>\\n <\/div>\\n <\/div>\\n <\/div>\\n\\n <script src=\\\"https:\/\/cdn.jsdelivr.net\/npm\/dplayer@1.27.1\/dist\/DPlayer.min.js\\\"><\/script>\\n <script src=\\\"https:\/\/cdn.jsdelivr.net\/npm\/hls.js@1.4.10\/dist\/hls.min.js\\\"><\/script>\\n <script defer src=\\\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/6.0.0\/js\/all.min.js\\\"><\/script>\\n <script>\\n const liveDataElement = document.getElementById('liveData');\\n let liveData = {};\\n \\n if (liveDataElement.textContent) {\\n try {\\n liveData = JSON.parse(liveDataElement.textContent);\\n } catch (e) {}\\n }\\n \\n const chatContainer = document.getElementById('chatContainer');\\n let chatWs = null; let __lastSendId = 0;\\n\\n function addSystemMessage(text) {\\n const systemDiv = document.createElement('div');\\n systemDiv.className = 'message-item system-message';\\n systemDiv.textContent = text;\\n chatContainer.appendChild(systemDiv);\\n while (chatContainer.children.length > 300) {\\n chatContainer.removeChild(chatContainer.firstElementChild);\\n }\\n chatContainer.scrollTop = chatContainer.scrollHeight;\\n }\\n \\n function initChat() {\\n if (!liveData.modelId || !liveData.websocketUrl || !liveData.websocketToken) return;\\n\\n addSystemMessage('开始连接弹幕服务器');\\n \\n const timeoutMs = 10000;\\n let connectTimeoutTimer = setTimeout(() => {\\n if (chatWs && chatWs.readyState !== WebSocket.OPEN) {\\n chatWs.close();\\n addSystemMessage('连接超时:未能成功连接弹幕服务器');\\n }\\n }, timeoutMs);\\n \\n if (chatWs && chatWs.readyState === WebSocket.OPEN) {\\n chatWs.close();\\n }\\n \\n chatWs = new WebSocket(`${liveData.websocketUrl}`);\\n window.__chatWs = chatWs;\\n __lastSendId = 3;\\n const sendQueue = [\\n '{\\\"connect\\\":{\\\"token\\\":\\\"' + liveData.websocketToken + '\\\"},\\\"id\\\":1}',\\n `{\\\"subscribe\\\":{\\\"channel\\\":\\\"newChatMessage@${liveData.modelId}\\\"},\\\"id\\\":2}`\\n ];\\n \\n function getUserLevelTag(userData) {\\n return userData?.userRanking?.level ? `<span class=\\\"level-tag\\\">Lv.${userData.userRanking.level}<\/span>` : '';\\n }\\n \\n const CHAT_EMOJI = {\\\"grinning\\\":\\\"1f600\\\",\\\"tongue\\\":\\\"1f61b\\\",\\\"thumbsup\\\":\\\"1f44d\\\",\\\"lol\\\":\\\"1f602\\\",\\\"wink\\\":\\\"1f609\\\",\\\"flirting\\\":\\\"1f618\\\",\\\"inlove\\\":\\\"1f60d\\\",\\\"heart\\\":\\\"2764-fe0f\\\",\\\"flirt\\\":\\\"1f60f\\\",\\\"ok\\\":\\\"1f44c\\\",\\\"kissing\\\":\\\"1f617\\\",\\\"airkiss\\\":\\\"1f618\\\",\\\"cheekkiss\\\":\\\"1f619\\\",\\\"flower\\\":\\\"1f337\\\",\\\"behind\\\":\\\"1f351\\\",\\\"blowjob\\\":\\\"1f4a6\\\",\\\"onbed\\\":\\\"1f6cf-fe0f\\\",\\\"ass\\\":\\\"1f351\\\",\\\"movingboobs\\\":\\\"1f44c\\\",\\\"wanker\\\":\\\"270a\\\",\\\"crazyguy\\\":\\\"1f92a\\\",\\\"ahh\\\":\\\"1f62e\\\",\\\"devil\\\":\\\"1f608\\\",\\\"dancing\\\":\\\"1f483\\\",\\\"shy\\\":\\\"1f633\\\",\\\"fire\\\":\\\"1f525\\\",\\\"asking\\\":\\\"1f64f\\\",\\\"angry\\\":\\\"1f620\\\",\\\"facepalm\\\":\\\"1f926\\\",\\\"cry\\\":\\\"1f622\\\",\\\"sad\\\":\\\"1f61e\\\",\\\"hideeyes\\\":\\\"1f648\\\",\\\"king\\\":\\\"1f451\\\",\\\"lgbt\\\":\\\"1f3f3-fe0f-200d-1f308\\\",\\\"knight\\\":\\\"1f6e1-fe0f\\\",\\\"gift\\\":\\\"1f381\\\",\\\"please\\\":\\\"1f97a\\\",\\\"bye\\\":\\\"1f44b\\\"};\\n const CHAT_EMOJI_RE = new RegExp(':(?:' + Object.keys(CHAT_EMOJI).join('|') + '):', 'g');\\n function escapeHtml(s) { return String(s == null ? '' : s).replace(\/&\/g, '&').replace(\/<\/g, '<').replace(\/>\/g, '>').replace(\/\\\"\/g, '"'); }\\n function renderChatText(s) { return escapeHtml(s).replace(CHAT_EMOJI_RE, function (mm) { const cp = CHAT_EMOJI[mm.slice(1, -1)]; if (!cp) return mm; const name = mm.slice(1, -1); return '<img class=\\\"chat-emoji-img\\\" alt=\\\"' + name + '\\\" src=\\\"https:\/\/cdn.jsdelivr.net\/npm\/emoji-datasource-apple@15.1.2\/img\/apple\/64\/' + cp + '.png\\\">'; }); }\\n\\n function createMessageElement(data) {\\n const messageDiv = document.createElement('div');\\n messageDiv.className = 'message-item';\\n \\n const { type, userData, details } = data.message;\\n const levelTag = getUserLevelTag(userData);\\n \\n let content = '';\\n switch(type) {\\n case 'text':\\n messageDiv.className += ' text-item';\\n content = `${levelTag}<span class=\\\"user-name\\\">${escapeHtml(userData.username)}<\/span>: ${renderChatText(details.body)}`;\\n break;\\n case 'tip':\\n messageDiv.className += ' tip-item';\\n content = `${levelTag}<span class=\\\"user-name\\\">${escapeHtml(userData.username)}<\/span>: 已支付<span class=\\\"tip-amount\\\">${details.amount}<\/span>代币${details.body ? ' · ' + renderChatText(details.body) : ''}`;\\n break;\\n case 'lovense':\\n messageDiv.className += ' lovense-item';\\n const { power, time, amount } = details.lovenseDetails.detail;\\n const powerText = power === 'low' ? '低' : power === 'medium' ? '中' : '高';\\n const clientLevelTag = getUserLevelTag(details.lovenseDetails.clientUserInfo);\\n content = `${clientLevelTag}<span class=\\\"user-name\\\">${details.lovenseDetails.clientUserInfo.username}<\/span>: ${powerText}强度 · ${time}秒`;\\n break;\\n default:\\n return null;\\n }\\n \\n messageDiv.innerHTML = content;\\n return messageDiv;\\n }\\n \\n function addMessageToPanel(messageData) {\\n const messageElement = createMessageElement(messageData);\\n if (messageElement) {\\n const messageWrapper = document.createElement('div');\\n messageWrapper.className = 'message-wrapper';\\n messageWrapper.appendChild(messageElement);\\n const nearBottom = chatContainer.scrollHeight - chatContainer.scrollTop - chatContainer.clientHeight < 90;\\n chatContainer.appendChild(messageWrapper);\\n while (chatContainer.children.length > 300) {\\n chatContainer.removeChild(chatContainer.firstElementChild);\\n }\\n if (nearBottom) chatContainer.scrollTop = chatContainer.scrollHeight;\\n }\\n }\\n \\n if (!window.__historyLoaded) {\\n window.__historyLoaded = true;\\n fetch('\/api\/front\/v2\/models\/' + liveData.modelId + '\/chat?limit=50', { credentials: 'include' })\\n .then(r => r.json())\\n .then(j => { ((j && j.messages) || []).forEach(mm => { try { addMessageToPanel({ message: mm }); } catch (e) {} }); })\\n .catch(() => {});\\n }\\n\\n chatWs.onopen = () => {\\n clearTimeout(connectTimeoutTimer);\\n sendQueue.forEach(cmd => chatWs.send(cmd));\\n addSystemMessage('弹幕服务器连接正常');\\n };\\n \\n chatWs.onmessage = (e) => {\\n if (e.data.trim() === '{}') {\\n chatWs.send('{}');\\n return;\\n }\\n try {\\n const data = JSON.parse(e.data);\\n if (data.ping) {\\n chatWs.send(JSON.stringify({ pong: data.ping }));\\n }\\n if (data.id === __lastSendId) {\\n if (data.error) { addSystemMessage('发送失败:' + (data.error.message || data.error.code)); }\\n else { addSystemMessage('发送成功'); }\\n }\\n if (data.push?.channel === `newChatMessage@${liveData.modelId}`) {\\n addMessageToPanel(data.push.pub.data);\\n }\\n } catch (error) {}\\n };\\n \\n chatWs.onerror = (err) => {\\n const errorDiv = document.createElement('div');\\n errorDiv.className = 'message-item system-message';\\n errorDiv.textContent = '连接错误';\\n chatContainer.appendChild(errorDiv);\\n chatContainer.scrollTop = chatContainer.scrollHeight;\\n };\\n \\n chatWs.onclose = (e) => {\\n const closeDiv = document.createElement('div');\\n closeDiv.className = 'message-item system-message';\\n closeDiv.textContent = `连接关闭 | 错误码: ${e.code}`;\\n chatContainer.appendChild(closeDiv);\\n chatContainer.scrollTop = chatContainer.scrollHeight;\\n };\\n }\\n \\n const elements = {\\n avatarImg: document.getElementById('avatarImg'),\\n hostName: document.getElementById('hostName'),\\n refreshBtn: document.getElementById('refreshBtn'),\\n volumeBtn: document.getElementById('volumeBtn'),\\n lineBtn: document.getElementById('lineBtn'),\\n lineMenu: document.getElementById('lineMenu'),\\n qualityBtn: document.getElementById('qualityBtn'),\\n qualityMenu: document.getElementById('qualityMenu'),\\n fullscreenBtn: document.getElementById('fullscreenBtn'),\\n loadingIndicator: document.getElementById('loadingIndicator'),\\n playerContainer: document.getElementById('playerContainer'),\\n membersCount: document.getElementById('membersCount'),\\n floatingBtn: document.getElementById('floatingBtn'),\\n floatingPanel: document.getElementById('floatingPanel'),\\n floatingHostName: document.getElementById('floatingHostName'),\\n floatingTopBestPlace: document.getElementById('floatingTopBestPlace'),\\n floatingMembersCount: document.getElementById('floatingMembersCount'),\\n floatingTipMenuCreatedAt: document.getElementById('floatingTipMenuCreatedAt'),\\n floatingTopic: document.getElementById('floatingTopic'),\\n floatingTopicSection: document.getElementById('floatingTopicSection'),\\n floatingGoalDescription: document.getElementById('floatingGoalDescription'),\\n floatingGoalSection: document.getElementById('floatingGoalSection'),\\n floatingUserDescription: document.getElementById('floatingUserDescription'),\\n floatingUserDescriptionSection: document.getElementById('floatingUserDescriptionSection'),\\n floatingFanClubDescription: document.getElementById('floatingFanClubDescription'),\\n floatingFanClubSection: document.getElementById('floatingFanClubSection'),\\n floatingPriceList: document.getElementById('floatingPriceList'),\\n floatingDurationLabel: document.getElementById('floatingDurationLabel'),\\n floatingDuration: document.getElementById('floatingDuration'),\\n floatingTimeLabel: document.getElementById('floatingTimeLabel'),\\n floatingStatusTime: document.getElementById('floatingStatusTime'),\\n floatingLine: document.getElementById('floatingLine'),\\n floatingQuality: document.getElementById('floatingQuality'),\\n topicContainer: document.getElementById('topicContainer'),\\n topic: document.getElementById('topic'),\\n rotateBtn: document.getElementById('rotateBtn')\\n };\\n \\n let dp = null;\\n let isMuted = false;\\n let isFullscreen = false;\\n let hlsInstance = null;\\n let durationInterval = null;\\n let config = {};\\n let hideControlsTimer = null;\\n \\n const getStreamUrl = (lineIndex = config.currentLine || 0, quality = config.currentQuality || 'auto') => {\\n const line = config.hlsLines?.[lineIndex];\\n if (!line || !config.stream) return '';\\n \\n const pureStatus = (config.status || '').toLowerCase();\\n let suffix = '_auto';\\n if (pureStatus !== 'public') suffix = '_160p_blurred';\\n return `https:\/\/edge-hls.${line}\/hls\/${config.stream}\/master\/${config.stream}${suffix}.m3u8?pkey=bXorqTB5ZhP5FcpX`;\\n };\\n \\n function getPresetUrl(quality) {\\n const line = config.hlsLines && config.hlsLines[config.currentLine || 0];\\n if (!line || !config.stream) return '';\\n const pureStatus = (config.status || '').toLowerCase();\\n let suffix = quality === 'auto' ? '_auto' : ('_' + quality);\\n if (pureStatus !== 'public') suffix = '_160p_blurred';\\n return `https:\/\/edge-hls.${line}\/hls\/${config.stream}\/master\/${config.stream}${suffix}.m3u8?pkey=bXorqTB5ZhP5FcpX`;\\n }\\n function showFreeze() {\\n try {\\n const v = dp && dp.video;\\n if (!v || !v.videoWidth || v.readyState < 2) return;\\n const cv = document.createElement('canvas');\\n cv.width = v.videoWidth; cv.height = v.videoHeight;\\n cv.getContext('2d').drawImage(v, 0, 0);\\n const el = document.createElement('div');\\n el.className = 'freeze-layer';\\n el.style.backgroundImage = 'url(' + cv.toDataURL('image\/jpeg', 0.7) + ')';\\n elements.playerContainer.appendChild(el);\\n window.__freezeEl = el;\\n setTimeout(function(){ hideFreeze(); }, 9000);\\n } catch (e) {}\\n }\\n function hideFreeze() {\\n try { if (window.__freezeEl) { window.__freezeEl.remove(); window.__freezeEl = null; } } catch (e) {}\\n }\\n function reloadQuality(quality) {\\n config.currentQuality = quality;\\n const url = getPresetUrl(quality);\\n if (!url) return;\\n showFreeze();\\n switchStream(url);\\n }\\n\\n function seamlessQuality(quality, prevQuality) {\\n try {\\n if (!dp || !dp.video || !elements.playerContainer || config.isSwitching) return;\\n const oldVideo = dp.video;\\n const oldHls = hlsInstance;\\n const url = getPresetUrl(quality);\\n if (!url) return;\\n config.isSwitching = true;\\n try { elements.loadingIndicator.style.display = 'block'; } catch (e) {}\\n\\n const nv = document.createElement('video');\\n nv.muted = true;\\n nv.playsInline = true;\\n nv.autoplay = true;\\n nv.preload = 'auto';\\n nv.setAttribute('playsinline', '');\\n nv.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:900;opacity:0;transition:opacity .25s;background:#000;pointer-events:none;';\\n elements.playerContainer.appendChild(nv);\\n\\n let finished = false;\\n const cleanupOld = () => {\\n try { if (oldHls) oldHls.destroy(); } catch (e) {}\\n try { if (oldVideo && oldVideo.parentNode) oldVideo.parentNode.removeChild(oldVideo); } catch (e) {}\\n };\\n const fail = () => {\\n if (finished) return;\\n finished = true;\\n try { nv.remove(); } catch (e) {}\\n config.isSwitching = false;\\n config.currentQuality = prevQuality || 'auto';\\n try {\\n const disp = config.currentQuality === 'auto' ? '默认' : config.currentQuality;\\n elements.qualityBtn.innerHTML = disp + ' <i class=\\\"fas fa-chevron-down\\\"><\/i>';\\n elements.floatingQuality.textContent = disp;\\n updateQualityMenuHighlights();\\n } catch (e) {}\\n try { elements.loadingIndicator.style.display = 'none'; } catch (e) {}\\n try { notify('清晰度切换失败,已保留原清晰度'); } catch (e) {}\\n try { oldVideo.muted = false; } catch (e) {}\\n };\\n\\n const start = () => {\\n try {\\n const nh = new Hls({\\n enableWorker: true,\\n lowLatencyMode: true,\\n backBufferLength: 30,\\n maxBufferLength: 20,\\n liveSyncDurationCount: 3,\\n liveMaxLatencyDurationCount: 10,\\n maxLiveSyncPlaybackRate: 1.5\\n });\\n nh.loadSource(url);\\n nh.attachMedia(nv);\\n nh.on(Hls.Events.MANIFEST_PARSED, () => { try { nv.play(); } catch (e) {} });\\n nh.on(Hls.Events.ERROR, (ev, data) => { if (data && data.fatal) fail(); });\\n\\n const ok = () => {\\n if (finished) return;\\n finished = true;\\n try {\\n const target = nh.liveSyncPosition;\\n if (target != null && Math.abs(nv.currentTime - target) > 0.5) nv.currentTime = target;\\n } catch (e) {}\\n \/\/ 音频无缝交接:保持用户当前静音状态\\n try {\\n const wasMuted = !!isMuted || !!oldVideo.muted;\\n nv.volume = oldVideo.volume;\\n nv.muted = wasMuted;\\n if (!wasMuted) { nv.muted = false; oldVideo.muted = true; }\\n } catch (e) {}\\n nv.style.opacity = '1';\\n try { oldVideo.style.transition = 'none'; oldVideo.style.opacity = '0'; oldVideo.style.visibility = 'hidden'; } catch (e) {}\\n setTimeout(() => {\\n cleanupOld();\\n dp.video = nv;\\n hlsInstance = nh;\\n try { bindVideoState(nv); } catch (e) {}\\n try { syncMuteUI(nv); } catch (e) {}\\n nv.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); return false; });\\n nv.addEventListener('playing', function(){ try { hideFreeze(); } catch (e) {} });\\n config.isSwitching = false;\\n try { elements.loadingIndicator.style.display = 'none'; } catch (e) {}\\n try { showControls(); } catch (e) {}\\n }, 120);\\n };\\n nv.addEventListener('playing', ok, { once: true });\\n setTimeout(() => { if (!finished) { try { nv.play(); } catch (e) {} } }, 1500);\\n setTimeout(() => { if (!finished) fail(); }, 9000);\\n } catch (e) { fail(); }\\n };\\n start();\\n } catch (e) {\\n try { config.isSwitching = false; } catch (x) {}\\n }\\n }\\n\\n function seamlessLine(lineIndex, oldLine) {\\n try {\\n if (!dp || !dp.video || !elements.playerContainer || config.isSwitching) return;\\n const oldVideo = dp.video;\\n const oldHls = hlsInstance;\\n const url = getStreamUrl(lineIndex, config.currentQuality);\\n if (!url) return;\\n config.isSwitching = true;\\n try { elements.loadingIndicator.style.display = 'block'; } catch (e) {}\\n\\n const nv = document.createElement('video');\\n \/\/ 预加载期间静音,交接完成后再恢复用户音量状态\\n nv.muted = true;\\n nv.playsInline = true;\\n nv.autoplay = false;\\n nv.setAttribute('playsinline', '');\\n nv.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:900;opacity:0;background:#000;pointer-events:none;';\\n elements.playerContainer.appendChild(nv);\\n\\n let finished = false;\\n const fail = () => {\\n if (finished) return;\\n finished = true;\\n try { nv.remove(); } catch (e) {}\\n config.isSwitching = false;\\n config.currentLine = oldLine;\\n try {\\n elements.lineBtn.innerHTML = '线路' + (oldLine + 1) + ' <i class=\\\"fas fa-chevron-down\\\"><\/i>';\\n elements.floatingLine.textContent = '线路' + (oldLine + 1);\\n updateLineMenuHighlights();\\n } catch (e) {}\\n try { elements.loadingIndicator.style.display = 'none'; } catch (e) {}\\n try { notify('线路切换失败,已回退到原线路'); } catch (e) {}\\n };\\n const nh = new Hls({\\n enableWorker: true,\\n lowLatencyMode: true,\\n backBufferLength: 30,\\n maxBufferLength: 20,\\n liveSyncDurationCount: 3,\\n liveMaxLatencyDurationCount: 10,\\n maxLiveSyncPlaybackRate: 1.5\\n });\\n nh.loadSource(url);\\n nh.attachMedia(nv);\\n nh.on(Hls.Events.MANIFEST_PARSED, () => {\\n try { applyQuality(nh); } catch (e) {}\\n try { nv.play(); } catch (e) {}\\n });\\n nh.on(Hls.Events.ERROR, (ev, data) => { if (data && data.fatal) fail(); });\\n nv.addEventListener('playing', () => {\\n if (finished) return;\\n finished = true;\\n try { if (oldHls) oldHls.destroy(); } catch (e) {}\\n try { if (oldVideo.parentNode) oldVideo.parentNode.removeChild(oldVideo); } catch (e) {}\\n nv.style.opacity = '1';\\n dp.video = nv;\\n hlsInstance = nh;\\n config.currentLine = lineIndex;\\n try { elements.lineBtn.innerHTML = '线路' + (lineIndex + 1) + ' <i class=\\\"fas fa-chevron-down\\\"><\/i>'; elements.floatingLine.textContent = '线路' + (lineIndex + 1); updateLineMenuHighlights(); } catch (e) {}\\n try { nv.volume = oldVideo.volume; nv.muted = isMuted; if (!isMuted) nv.muted = false; bindVideoState(nv); syncMuteUI(nv); } catch (e) {}\\n config.isSwitching = false;\\n try { elements.loadingIndicator.style.display = 'none'; } catch (e) {}\\n try { showControls(); } catch (e) {}\\n }, { once: true });\\n setTimeout(() => { if (!finished) fail(); }, 9000);\\n } catch (e) {\\n try { config.isSwitching = false; } catch (x) {}\\n }\\n }\\n\\n function applyQuality(h) {\\n const inst = h || hlsInstance;\\n if (!inst || !inst.levels || !inst.levels.length) return;\\n const q = config.currentQuality || 'auto';\\n if (q === 'auto' || q === '160p_blurred') {\\n try { inst.nextLevel = -1; inst.autoLevelCapping = -1; } catch (e) {}\\n return;\\n }\\n const target = parseInt(String(q).replace(\/[^0-9]\/g, ''), 10);\\n if (!target) return;\\n const levels = inst.levels;\\n const cur = (inst.currentLevel >= 0 && levels[inst.currentLevel]) || null;\\n const curCodec = (cur && cur.attrs && cur.attrs.CODECS) || '';\\n let exactName = -1, exactHeight = -1;\\n let sameCodecBest = -1, sameCodecDiff = 1e9;\\n let anyBest = -1, anyDiff = 1e9;\\n for (let i = 0; i < levels.length; i++) {\\n const lv = levels[i] || {};\\n const attrs = lv.attrs || {};\\n let hh = lv.height;\\n if (!hh && attrs.RESOLUTION) hh = parseInt(String(attrs.RESOLUTION).split('x')[1], 10);\\n if (!hh && !attrs.NAME) continue;\\n const nm = attrs.NAME || '';\\n const codec = attrs.CODECS || '';\\n const diff = Math.abs((hh || 0) - target);\\n if (diff < anyDiff) { anyDiff = diff; anyBest = i; }\\n if (hh === target && exactHeight < 0) exactHeight = i;\\n if (nm === q && exactName < 0) exactName = i;\\n if (curCodec && codec && codec !== curCodec) continue;\\n if (diff < sameCodecDiff) { sameCodecDiff = diff; sameCodecBest = i; }\\n }\\n let best = exactName;\\n if (best < 0) best = exactHeight;\\n if (best < 0) best = sameCodecBest;\\n if (best < 0) best = anyBest;\\n if (best >= 0) {\\n window.__qBest = best;\\n try { inst.nextLevel = best; inst.autoLevelCapping = best; } catch (e) {}\\n }\\n }\\n\\n function showControls() {\\n elements.playerContainer.classList.remove('controls-hidden');\\n elements.playerContainer.classList.add('controls-visible');\\n if (hideControlsTimer) {\\n clearTimeout(hideControlsTimer);\\n }\\n hideControlsTimer = setTimeout(() => {\\n elements.playerContainer.classList.remove('controls-visible');\\n elements.playerContainer.classList.add('controls-hidden');\\n }, 3000);\\n }\\n \\n function switchStream(url) {\\n if (!dp || !dp.video || !url) return;\\n \\n config.isSwitching = true;\\n elements.loadingIndicator.style.display = 'block';\\n showControls();\\n \\n const video = dp.video;\\n video.pause();\\n video.src = '';\\n video.load();\\n \\n if (hlsInstance) {\\n try {\\n hlsInstance.stopLoad();\\n setTimeout(() => {\\n hlsInstance.destroy();\\n hlsInstance = null;\\n startNewStream(url, video);\\n }, 50);\\n } catch (e) {\\n hlsInstance.destroy();\\n hlsInstance = null;\\n startNewStream(url, video);\\n }\\n } else {\\n startNewStream(url, video);\\n }\\n }\\n \\n function startNewStream(url, video) {\\n video.src = url;\\n \\n if (Hls.isSupported()) {\\n hlsInstance = new Hls({\\n enableWorker: true,\\n lowLatencyMode: true,\\n backBufferLength: 30,\\n maxBufferLength: 20,\\n liveSyncDurationCount: 3,\\n liveMaxLatencyDurationCount: 10,\\n maxLiveSyncPlaybackRate: 1.5\\n });\\n \\n hlsInstance.loadSource(url);\\n hlsInstance.attachMedia(video);\\n \\n hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {\\n completeSwitch();\\n try { applyQuality(); } catch (e) {}\\n try { video.muted = isMuted; } catch (e) {}\\n try { syncMuteUI(video); } catch (e) {}\\n video.play().catch(function() {});\\n });\\n \\n hlsInstance.on(Hls.Events.ERROR, (event, data) => {\\n if (data.fatal) {\\n switch(data.type) {\\n case Hls.ErrorTypes.NETWORK_ERROR:\\n hlsInstance.startLoad();\\n break;\\n case Hls.ErrorTypes.MEDIA_ERROR:\\n hlsInstance.recoverMediaError();\\n break;\\n default:\\n completeSwitch();\\n break;\\n }\\n }\\n });\\n \\n setTimeout(() => {\\n if (config.isSwitching) {\\n completeSwitch();\\n }\\n }, 5000);\\n \\n } else if (video.canPlayType('application\/vnd.apple.mpegurl')) {\\n video.addEventListener('loadedmetadata', () => {\\n completeSwitch();\\n video.play().catch(e => {});\\n });\\n \\n video.addEventListener('error', () => {\\n completeSwitch();\\n });\\n \\n setTimeout(() => {\\n if (config.isSwitching) {\\n completeSwitch();\\n }\\n }, 5000);\\n } else {\\n completeSwitch();\\n }\\n }\\n \\n function completeSwitch() {\\n config.isSwitching = false;\\n elements.loadingIndicator.style.display = 'none';\\n }\\n \\n function togglePlayback() {\\n try {\\n if (!dp || !dp.video) return;\\n const v = dp.video;\\n \/\/ 直接使用原生 video 暂停\/播放,绕过 DPlayer 内部状态\\n if (v.paused) {\\n try { const p = v.play(); if (p && p.catch) p.catch(function(){}); } catch (e) {}\\n } else {\\n try { v.pause(); } catch (e) {}\\n try { if (!v.paused) v.paused = true; } catch (e) {}\\n }\\n } catch (e) {}\\n try { showControls(); } catch (e) {}\\n }\\n\\n function syncMuteUI(v) {\\n try {\\n if (!v) return;\\n const m = !!v.muted;\\n isMuted = m;\\n const btn = elements.volumeBtn;\\n if (btn) btn.innerHTML = m ? '<i class=\\\"fas fa-volume-mute\\\"><\/i>' : '<i class=\\\"fas fa-volume-up\\\"><\/i>';\\n } catch (e) {}\\n }\\n\\n function notify(t) {\\n try {\\n const box = document.getElementById('chatContainer');\\n if (!box) return;\\n const d = document.createElement('div');\\n d.className = 'message-item system-message';\\n d.textContent = t;\\n box.appendChild(d);\\n box.scrollTop = box.scrollHeight;\\n } catch (e) {}\\n }\\n\\n function bindVideoState(v) {\\n if (!v) return;\\n const upd = function () {\\n try {\\n const pc = elements.playerContainer;\\n if (!pc || !v) return;\\n if (v.paused) pc.classList.add('video-paused'); else pc.classList.remove('video-paused');\\n const btn = document.getElementById('centerPlayBtn');\\n if (btn) btn.innerHTML = v.paused ? '<i class=\\\"fas fa-play\\\"><\/i>' : '<i class=\\\"fas fa-pause\\\"><\/i>';\\n syncMuteUI(v);\\n } catch (e) {}\\n };\\n v.addEventListener('play', upd);\\n v.addEventListener('pause', upd);\\n v.addEventListener('playing', upd);\\n v.addEventListener('volumechange', function(){ syncMuteUI(v); });\\n upd();\\n }\\n\\n function initPlayer() {\\n config = { ...liveData, currentLine: 0, currentQuality: 'auto', isSwitching: false };\\n try { window.__fitAr = null; } catch (e) {}\\n \\n const url = getStreamUrl();\\n if (!url) return;\\n \\n if (dp) {\\n dp.destroy();\\n dp = null;\\n }\\n \\n dp = new DPlayer({\\n container: document.getElementById('dplayer'),\\n live: true,\\n autoplay: true,\\n theme: '#00a1d6',\\n loop: false,\\n lang: 'zh-cn',\\n screenshot: false,\\n hotkey: false,\\n preload: 'auto',\\n volume: 0.7,\\n mutex: true,\\n controls: false,\\n video: {\\n url: url,\\n pic: config.coverImg,\\n type: 'customHls',\\n customType: {\\n customHls: function(video, player) {}\\n }\\n },\\n contextmenu: [],\\n danmaku: false\\n });\\n \\n dp.video.addEventListener('playing', function(){ try { hideFreeze(); } catch (e) {} });\\n try { bindVideoState(dp.video); } catch (e) {}\\n let prefMuted = false;\\n try { const savedMute = localStorage.getItem('vt_muted'); if (savedMute === '0') prefMuted = false; } catch (e) {}\\n dp.video.muted = prefMuted;\\n isMuted = prefMuted;\\n if (elements.volumeBtn) elements.volumeBtn.innerHTML = isMuted ? '<i class=\\\"fas fa-volume-mute\\\"><\/i>' : '<i class=\\\"fas fa-volume-up\\\"><\/i>';\\n try { syncMuteUI(dp.video); } catch (e) {}\\n try { var __pp = dp.video.play(); if (__pp && __pp.catch) __pp.catch(function(){}); } catch (e) {}\\n try {\\n const resumeAudio = function () {\\n try {\\n if (isMuted && dp && dp.video && localStorage.getItem('vt_muted') === '0') {\\n dp.video.muted = false;\\n isMuted = false;\\n if (elements.volumeBtn) elements.volumeBtn.innerHTML = '<i class=\\\"fas fa-volume-up\\\"><\/i>';\\n }\\n } catch (e) {}\\n document.removeEventListener('pointerdown', resumeAudio);\\n document.removeEventListener('touchstart', resumeAudio);\\n };\\n document.addEventListener('pointerdown', resumeAudio, { once: true });\\n document.addEventListener('touchstart', resumeAudio, { once: true });\\n } catch (e) {}\\n\\n const videoElement = dp.video;\\n if (videoElement) {\\n videoElement.style.pointerEvents = 'none';\\n videoElement.addEventListener('click', function(e) {\\n e.preventDefault();\\n e.stopPropagation();\\n return false;\\n });\\n }\\n \\n const dplayerContainer = dp.container;\\n dplayerContainer.style.pointerEvents = 'none';\\n dplayerContainer.addEventListener('click', function(e) {\\n e.preventDefault();\\n e.stopPropagation();\\n return false;\\n });\\n \\n isMuted = dp.video.muted;\\n elements.volumeBtn.innerHTML = isMuted \\n ? '<i class=\\\"fas fa-volume-mute\\\"><\/i>'\\n : '<i class=\\\"fas fa-volume-up\\\"><\/i>';\\n \\n switchStream(url);\\n }\\n\\n function refreshPlayer() {\\n if (config.isSwitching || !config.stream) return;\\n const url = getStreamUrl(config.currentLine, config.currentQuality);\\n switchStream(url);\\n }\\n \\n function switchLine(lineIndex) {\\n if (config.isSwitching || !config.hlsLines || lineIndex >= config.hlsLines.length) return;\\n const oldLine = config.currentLine;\\n config.currentLine = lineIndex;\\n elements.lineBtn.innerHTML = `线路${lineIndex + 1} <i class=\\\"fas fa-chevron-down\\\"><\/i>`;\\n elements.floatingLine.textContent = `线路${lineIndex + 1}`;\\n updateLineMenuHighlights();\\n elements.lineMenu.style.display = 'none';\\n\\n if (dp && dp.video && hlsInstance) {\\n seamlessLine(lineIndex, oldLine);\\n } else {\\n config.currentLine = lineIndex;\\n const url = getStreamUrl(lineIndex, config.currentQuality);\\n switchStream(url);\\n }\\n }\\n\\n function switchQuality(quality) {\\n if (config.isSwitching) return;\\n const prevQuality = config.currentQuality;\\n config.currentQuality = quality;\\n const displayText = quality === 'auto' ? '默认' : quality;\\n elements.qualityBtn.innerHTML = `${displayText} <i class=\\\"fas fa-chevron-down\\\"><\/i>`;\\n elements.floatingQuality.textContent = displayText;\\n updateQualityMenuHighlights();\\n elements.qualityMenu.style.display = 'none';\\n if (quality === 'auto') {\\n if (hlsInstance) {\\n try { hlsInstance.nextLevel = -1; hlsInstance.autoLevelCapping = -1; } catch (e) {}\\n } else {\\n seamlessQuality('auto');\\n }\\n return;\\n }\\n seamlessQuality(quality, prevQuality);\\n }\\n\\n function formatDuration(seconds) {\\n if (!seconds) return '0秒';\\n \\n const days = Math.floor(seconds \/ (24 * 60 * 60));\\n const hours = Math.floor((seconds % (24 * 60 * 60)) \/ (60 * 60));\\n const minutes = Math.floor((seconds % (60 * 60)) \/ 60);\\n const secs = Math.floor(seconds % 60);\\n \\n let result = '';\\n if (days > 0) result += `${days}天`;\\n if (hours > 0) result += `${hours}小时`;\\n if (minutes > 0) result += `${minutes}分钟`;\\n if (secs > 0 || result === '') result += `${secs}秒`;\\n \\n return result;\\n }\\n \\n function updateTimeInfo() {\\n if (!config.status || !config.startTime) return;\\n \\n const pureStatus = config.status.replace(\/^状态:\/, '').toLowerCase();\\n const isPublic = pureStatus === 'public';\\n const startTime = config.startTime ? new Date(config.startTime).getTime() : null;\\n const now = Date.now();\\n \\n if (config.isLive && startTime) {\\n if (isPublic) {\\n elements.floatingDurationLabel.textContent = '直播时长:';\\n elements.floatingTimeLabel.textContent = '直播时间:';\\n \\n const duration = Math.floor((now - startTime) \/ 1000);\\n const durationText = formatDuration(duration);\\n elements.floatingDuration.textContent = durationText;\\n \\n const startDate = new Date(startTime);\\n elements.floatingStatusTime.textContent = startDate.toLocaleString();\\n } else {\\n elements.floatingDurationLabel.textContent = '持续时长:';\\n elements.floatingTimeLabel.textContent = '私密直播:';\\n \\n const duration = Math.floor((now - startTime) \/ 1000);\\n const durationText = formatDuration(duration);\\n elements.floatingDuration.textContent = durationText;\\n \\n const startDate = new Date(startTime);\\n elements.floatingStatusTime.textContent = startDate.toLocaleString();\\n }\\n } else if (startTime) {\\n elements.floatingDurationLabel.textContent = '持续时长:';\\n elements.floatingTimeLabel.textContent = '下播时间:';\\n \\n const duration = Math.floor((now - startTime) \/ 1000);\\n let durationText = formatDuration(duration);\\n elements.floatingDuration.textContent = durationText;\\n \\n const endDate = new Date(startTime);\\n elements.floatingStatusTime.textContent = endDate.toLocaleString();\\n } else {\\n elements.floatingDuration.textContent = '--';\\n elements.floatingStatusTime.textContent = '--';\\n }\\n }\\n \\n function startDurationUpdate() {\\n if (durationInterval) {\\n clearInterval(durationInterval);\\n }\\n \\n updateTimeInfo();\\n durationInterval = setInterval(updateTimeInfo, 1000);\\n }\\n \\n function updateFloatingPanel() {\\n const pureUsername = (config.username || '').split('\/').pop() || config.username || '';\\n elements.floatingHostName.textContent = pureUsername || '未知主播';\\n elements.membersCount.textContent = config.membersCount > 0 ? config.membersCount.toLocaleString() : '--';\\n elements.floatingMembersCount.textContent = config.membersCount > 0 ? config.membersCount.toLocaleString() : '--';\\n \\n if (config.topBestPlace > 0) {\\n const rankText = `TOP ${config.topBestPlace}`;\\n elements.floatingTopBestPlace.textContent = rankText;\\n } else {\\n elements.floatingTopBestPlace.textContent = '未上榜';\\n }\\n \\n if (config.tipMenuCreatedAt) {\\n const createdAt = new Date(config.tipMenuCreatedAt);\\n elements.floatingTipMenuCreatedAt.textContent = createdAt.toLocaleString();\\n } else {\\n elements.floatingTipMenuCreatedAt.textContent = '--';\\n }\\n \\n if (config.topic && String(config.topic).trim() !== '') {\\n elements.floatingTopic.textContent = config.topic;\\n elements.floatingTopicSection.style.display = 'block';\\n } else {\\n elements.floatingTopicSection.style.display = 'none';\\n }\\n\\n if (config.userDescription && config.userDescription.trim() !== '') {\\n elements.floatingUserDescription.textContent = config.userDescription;\\n elements.floatingUserDescriptionSection.style.display = 'block';\\n } else {\\n elements.floatingUserDescriptionSection.style.display = 'none';\\n }\\n \\n if (config.goalDescription && config.goalDescription.trim() !== '') {\\n elements.floatingGoalDescription.textContent = config.goalDescription + (config.goalTarget > 0 ? '(已筹 ' + (config.goalActual || 0) + '\/' + config.goalTarget + (config.goalLeft != null ? ',剩余 ' + config.goalLeft : '') + ')' : '');\\n elements.floatingGoalSection.style.display = 'block';\\n } else {\\n elements.floatingGoalSection.style.display = 'none';\\n }\\n \\n if (config.fanClubDescription && config.fanClubDescription.trim() !== '') {\\n elements.floatingFanClubDescription.textContent = config.fanClubDescription;\\n elements.floatingFanClubSection.style.display = 'block';\\n } else {\\n elements.floatingFanClubSection.style.display = 'none';\\n }\\n \\n if (config.tipMenuPriceList && config.tipMenuPriceList.length > 0) {\\n elements.floatingPriceList.innerHTML = '';\\n config.tipMenuPriceList.forEach((tip) => {\\n const priceItem = document.createElement('div');\\n priceItem.className = 'floating-price-item';\\n \\n const activityDiv = document.createElement('div');\\n activityDiv.className = 'floating-price-activity';\\n activityDiv.textContent = tip.activity || '未命名';\\n \\n const priceDiv = document.createElement('div');\\n priceDiv.className = 'floating-price-value';\\n priceDiv.textContent = `${tip.price || 0} 代币`;\\n \\n priceItem.appendChild(activityDiv);\\n priceItem.appendChild(priceDiv);\\n elements.floatingPriceList.appendChild(priceItem);\\n });\\n } else {\\n elements.floatingPriceList.innerHTML = '<div class=\\\"floating-price-item\\\"><div class=\\\"floating-price-activity\\\">暂无打赏菜单<\/div><\/div>';\\n }\\n \\n startDurationUpdate();\\n }\\n \\n function initUI() {\\n const pureUsername = (config.username || '').split('\/').pop() || config.username || '';\\n elements.hostName.textContent = pureUsername || '未知主播';\\n \\n if (config.topic && config.topic.trim() !== '') {\\n if (elements.topic) elements.topic.textContent = config.topic;\\n if (elements.topicContainer) elements.topicContainer.style.display = 'block';\\n document.title = config.topic;\\n } else {\\n if (elements.topicContainer) elements.topicContainer.style.display = 'none';\\n document.title = pureUsername || '直播间';\\n }\\n \\n if (config.avatarUrl) {\\n const prefix = 'https:\/\/static-cdn.strpst.com';\\n if (config.avatarUrl.startsWith(prefix + prefix)) {\\n config.avatarUrl = config.avatarUrl.replace(prefix + prefix, prefix);\\n }\\n elements.avatarImg.src = config.avatarUrl;\\n } else {\\n elements.avatarImg.src = '主播';\\n }\\n \\n updateFloatingPanel();\\n }\\n \\n function initLineMenu() {\\n elements.lineMenu.innerHTML = '';\\n \\n if (!config.hlsLines || config.hlsLines.length === 0) {\\n const item = document.createElement('div');\\n item.className = 'menu-item';\\n item.textContent = '无线路';\\n item.style.opacity = '0.6';\\n elements.lineMenu.appendChild(item);\\n return;\\n }\\n \\n config.hlsLines.forEach((line, index) => {\\n const item = document.createElement('div');\\n item.className = `menu-item ${index === config.currentLine ? 'active' : ''}`;\\n item.textContent = `线路${index + 1}`;\\n item.dataset.index = index;\\n item.onclick = (e) => {\\n e.stopPropagation();\\n switchLine(index);\\n };\\n elements.lineMenu.appendChild(item);\\n });\\n }\\n \\n function initQualityMenu() {\\n elements.qualityMenu.innerHTML = '';\\n const pureStatus = (config.status || '').replace(\/^状态:\/, '').toLowerCase();\\n \\n let availableQualities = [];\\n availableQualities.push({ value: 'auto', display: '默认' });\\n \\n if (pureStatus !== 'public') {\\n availableQualities = [{ value: '160p_blurred', display: '模糊' }];\\n config.currentQuality = '160p_blurred';\\n } else if (config.pixelatedResolutions && config.pixelatedResolutions.length > 0) {\\n config.pixelatedResolutions.forEach(resolution => {\\n if (resolution !== '160p_blurred') {\\n availableQualities.push({ \\n value: resolution, \\n display: resolution \\n });\\n }\\n });\\n }\\n \\n availableQualities.forEach((quality) => {\\n const isActive = quality.value === config.currentQuality;\\n const item = document.createElement('div');\\n item.className = `menu-item ${isActive ? 'active' : ''}`;\\n item.textContent = quality.display;\\n item.dataset.quality = quality.value;\\n item.onclick = (e) => {\\n e.stopPropagation();\\n switchQuality(quality.value);\\n };\\n elements.qualityMenu.appendChild(item);\\n });\\n \\n const currentQuality = availableQualities.find(q => q.value === config.currentQuality);\\n const displayText = currentQuality ? currentQuality.display : '默认';\\n elements.qualityBtn.innerHTML = `${displayText} <i class=\\\"fas fa-chevron-down\\\"><\/i>`;\\n elements.floatingQuality.textContent = displayText;\\n }\\n \\n function updateLineMenuHighlights() {\\n const items = elements.lineMenu.querySelectorAll('.menu-item');\\n items.forEach((item, index) => {\\n if (index === config.currentLine) {\\n item.classList.add('active');\\n } else {\\n item.classList.remove('active');\\n }\\n });\\n }\\n \\n function updateQualityMenuHighlights() {\\n const items = elements.qualityMenu.querySelectorAll('.menu-item');\\n items.forEach((item) => {\\n const quality = item.dataset.quality;\\n if (quality === config.currentQuality) {\\n item.classList.add('active');\\n } else {\\n item.classList.remove('active');\\n }\\n });\\n }\\n \\n async function handleRotateClick() {\\n try {\\n if (!document.fullscreenElement) {\\n await elements.playerContainer.requestFullscreen();\\n }\\n \\n if (screen.orientation) {\\n const type = screen.orientation.type;\\n if (type.startsWith('portrait')) {\\n await screen.orientation.lock('landscape');\\n elements.rotateBtn.textContent = '横屏';\\n } else {\\n await screen.orientation.lock('portrait');\\n elements.rotateBtn.textContent = '横屏';\\n }\\n }\\n showControls();\\n } catch (err) {}\\n }\\n \\n function bindEvents() {\\n const __centerBtn = document.getElementById('centerPlayBtn');\\n if (__centerBtn) {\\n __centerBtn.addEventListener('click', (e) => {\\n e.preventDefault(); e.stopPropagation();\\n togglePlayback();\\n });\\n __centerBtn.addEventListener('touchend', (e) => {\\n e.preventDefault(); e.stopPropagation();\\n togglePlayback();\\n }, { passive: false });\\n }\\n\\n elements.refreshBtn.addEventListener('click', (e) => {\\n e.stopPropagation();\\n refreshPlayer();\\n showControls();\\n });\\n \\n elements.volumeBtn.addEventListener('click', (e) => {\\n e.stopPropagation();\\n if (dp && dp.video && !config.isSwitching) {\\n isMuted = !isMuted;\\n dp.video.muted = isMuted;\\n try { localStorage.setItem('vt_muted', isMuted ? '1' : '0'); } catch (e) {}\\n elements.volumeBtn.innerHTML = isMuted \\n ? '<i class=\\\"fas fa-volume-mute\\\"><\/i>'\\n : '<i class=\\\"fas fa-volume-up\\\"><\/i>';\\n }\\n showControls();\\n });\\n \\n elements.playerContainer.addEventListener('click', (e) => {\\n if (e.target && e.target.closest && e.target.closest('.custom-controls')) return;\\n \/\/ 点击画面只显示控制条,播放\/暂停由中央按钮负责\\n showControls();\\n });\\n\\n elements.fullscreenBtn.addEventListener('click', (e) => {\\n e.stopPropagation();\\n if (!isFullscreen) {\\n const container = document.querySelector('.player-container');\\n if (container.requestFullscreen) {\\n container.requestFullscreen();\\n } else if (container.webkitRequestFullscreen) {\\n container.webkitRequestFullscreen();\\n }\\n elements.fullscreenBtn.innerHTML = '<i class=\\\"fas fa-compress\\\"><\/i>';\\n } else {\\n if (document.exitFullscreen) {\\n document.exitFullscreen();\\n } else if (document.webkitExitFullscreen) {\\n document.webkitExitFullscreen();\\n }\\n elements.fullscreenBtn.innerHTML = '<i class=\\\"fas fa-expand\\\"><\/i>';\\n }\\n isFullscreen = !isFullscreen;\\n showControls();\\n });\\n \\n elements.lineBtn.addEventListener('click', (e) => {\\n e.stopPropagation();\\n elements.lineMenu.style.display = \\n elements.lineMenu.style.display === 'block' ? 'none' : 'block';\\n elements.qualityMenu.style.display = 'none';\\n updateLineMenuHighlights();\\n showControls();\\n });\\n \\n elements.qualityBtn.addEventListener('click', (e) => {\\n e.stopPropagation();\\n elements.qualityMenu.style.display = \\n elements.qualityMenu.style.display === 'block' ? 'none' : 'block';\\n elements.lineMenu.style.display = 'none';\\n updateQualityMenuHighlights();\\n showControls();\\n });\\n \\n elements.rotateBtn.addEventListener('click', (e) => {\\n e.stopPropagation();\\n handleRotateClick();\\n });\\n \\n elements.playerContainer.addEventListener('touchstart', (e) => {\\n if (e.target.closest('.custom-controls, .selector-menu, .control-btn, .selector-btn, .menu-item, .rotate-btn')) {\\n showControls();\\n return;\\n }\\n \\n if (elements.playerContainer.classList.contains('controls-hidden')) {\\n showControls();\\n } else {\\n elements.playerContainer.classList.remove('controls-visible');\\n elements.playerContainer.classList.add('controls-hidden');\\n if (hideControlsTimer) {\\n clearTimeout(hideControlsTimer);\\n hideControlsTimer = null;\\n }\\n }\\n });\\n \\n document.addEventListener('click', (e) => {\\n if (!e.target.closest('.selector-btn')) {\\n elements.lineMenu.style.display = 'none';\\n elements.qualityMenu.style.display = 'none';\\n }\\n });\\n \\n document.addEventListener('fullscreenchange', handleFullscreenChange);\\n document.addEventListener('webkitfullscreenchange', handleFullscreenChange);\\n \\n elements.floatingBtn.addEventListener('click', (e) => {\\n e.stopPropagation();\\n elements.floatingPanel.style.display = \\n elements.floatingPanel.style.display === 'block' ? 'none' : 'block';\\n });\\n \\n document.addEventListener('click', (e) => {\\n if (!e.target.closest('.floating-button') && !e.target.closest('.floating-panel')) {\\n elements.floatingPanel.style.display = 'none';\\n }\\n });\\n }\\n \\n function handleFullscreenChange() {\\n const isFullscreenNow = !!(document.fullscreenElement || \\n document.webkitFullscreenElement);\\n if (!isFullscreenNow) {\\n elements.fullscreenBtn.innerHTML = '<i class=\\\"fas fa-expand\\\"><\/i>';\\n isFullscreen = false;\\n }\\n }\\n \\n function initializeApp() {\\n if (!liveData || Object.keys(liveData).length === 0) return;\\n \\n addSystemMessage('正在读取直播间信息');\\n \\n config = { ...liveData, currentLine: 0, currentQuality: 'auto', isSwitching: false };\\n \\n initUI();\\n initLineMenu();\\n initQualityMenu();\\n initPlayer();\\n bindEvents();\\n \/\/ 优先保证视频起播,聊天\/历史稍后再初始化\\n (function () {\\n let started = false;\\n const startChat = function () {\\n if (started) return;\\n started = true;\\n try { initChat(); } catch (e) {}\\n };\\n try {\\n const v = dp && dp.video;\\n if (v) v.addEventListener('playing', function () { setTimeout(startChat, 400); }, { once: true });\\n } catch (e) {}\\n setTimeout(startChat, 2500);\\n })();\\n \\n elements.playerContainer.classList.add('controls-visible');\\n showControls();\\n }\\n \\n document.addEventListener('DOMContentLoaded', initializeApp);\\n <\/script>\\n <script>(function(){\\nvar EMOJIS = [[\\\"grinning\\\",\\\"1f600\\\"],[\\\"tongue\\\",\\\"1f61b\\\"],[\\\"thumbsup\\\",\\\"1f44d\\\"],[\\\"lol\\\",\\\"1f602\\\"],[\\\"wink\\\",\\\"1f609\\\"],[\\\"flirting\\\",\\\"1f618\\\"],[\\\"inlove\\\",\\\"1f60d\\\"],[\\\"heart\\\",\\\"2764-fe0f\\\"],[\\\"flirt\\\",\\\"1f60f\\\"],[\\\"ok\\\",\\\"1f44c\\\"],[\\\"kissing\\\",\\\"1f617\\\"],[\\\"airkiss\\\",\\\"1f618\\\"],[\\\"cheekkiss\\\",\\\"1f619\\\"],[\\\"flower\\\",\\\"1f337\\\"],[\\\"behind\\\",\\\"1f351\\\"],[\\\"blowjob\\\",\\\"1f4a6\\\"],[\\\"onbed\\\",\\\"1f6cf-fe0f\\\"],[\\\"ass\\\",\\\"1f351\\\"],[\\\"movingboobs\\\",\\\"1f44c\\\"],[\\\"wanker\\\",\\\"270a\\\"],[\\\"crazyguy\\\",\\\"1f92a\\\"],[\\\"ahh\\\",\\\"1f62e\\\"],[\\\"devil\\\",\\\"1f608\\\"],[\\\"dancing\\\",\\\"1f483\\\"],[\\\"shy\\\",\\\"1f633\\\"],[\\\"fire\\\",\\\"1f525\\\"],[\\\"asking\\\",\\\"1f64f\\\"],[\\\"angry\\\",\\\"1f620\\\"],[\\\"facepalm\\\",\\\"1f926\\\"],[\\\"cry\\\",\\\"1f622\\\"],[\\\"sad\\\",\\\"1f61e\\\"],[\\\"hideeyes\\\",\\\"1f648\\\"],[\\\"king\\\",\\\"1f451\\\"],[\\\"lgbt\\\",\\\"1f3f3-fe0f-200d-1f308\\\"],[\\\"knight\\\",\\\"1f6e1-fe0f\\\"],[\\\"gift\\\",\\\"1f381\\\"],[\\\"please\\\",\\\"1f97a\\\"],[\\\"bye\\\",\\\"1f44b\\\"]];\\nvar panel=document.getElementById('emojiPanel'), grid=document.getElementById('emojiGrid'),\\n btn=document.getElementById('emojiBtn'), closeBtn=document.getElementById('emojiClose'), inp=document.getElementById('ci');\\nif(!panel||!grid||!btn) return;\\nvar base='https:\/\/cdn.jsdelivr.net\/npm\/emoji-datasource-apple@15.1.2\/img\/apple\/64\/';\\nfunction build(){\\n if(grid.children.length) return;\\n EMOJIS.forEach(function(e){\\n var b=document.createElement('button'); b.type='button'; b.className='emoji-item'; b.title=':'+e[0]+':';\\n var img=document.createElement('img'); img.alt=e[0]; img.src=base+e[1]+'.png'; img.loading='lazy';\\n b.appendChild(img);\\n b.addEventListener('click',function(){\\n if(!inp) return;\\n var code=':'+e[0]+':';\\n if(inp.value.length + code.length > 200) return;\\n inp.value += code;\\n });\\n grid.appendChild(b);\\n });\\n}\\nbtn.addEventListener('click',function(e){ e.preventDefault(); e.stopPropagation(); build(); panel.classList.toggle('open'); });\\nif(closeBtn) closeBtn.addEventListener('click',function(){ panel.classList.remove('open'); });\\ndocument.addEventListener('click',function(e){\\n if(!panel.classList.contains('open')) return;\\n if(panel.contains(e.target)||btn.contains(e.target)) return;\\n panel.classList.remove('open');\\n});\\n})();<\/script>\\n<script>(function(){\\nfunction sys(t){try{var box=(document.getElementById('chatContainer')||document.getElementById('chat'));if(!box)return;var d=document.createElement('div');d.className='message-item system-message';d.textContent=t;box.appendChild(d);box.scrollTop=box.scrollHeight;}catch(e){}}\\nvar btn=document.getElementById('favBtn'); if(!btn) return;\\nvar fav=!!(window.liveData&&liveData.isInFavorites);\\nfunction render(){btn.innerHTML='<i class=\\\"fas fa-heart\\\"><\/i>';btn.style.color=fav?'#ff3b5c':'#8a9099';btn.title=fav?'取消收藏':'收藏';}\\nrender();\\nbtn.addEventListener('click',function(e){\\n if(e && e.stopPropagation) e.stopPropagation();\\n if(!window.__myUid){ sys('请先登录后再收藏'); return; }\\n var url='\/api\/front\/users\/'+window.__myUid+'\/favorites\/'+liveData.modelId;\\n var method=fav?'DELETE':'PUT';\\n fetch(url,{method:method,credentials:'include',headers:{'Content-Type':'application\/json','Accept':'application\/json'},body:JSON.stringify({csrfToken:(liveData.csrfToken||''),csrfTimestamp:(liveData.csrfTimestamp||''),csrfNotifyTimestamp:(liveData.csrfNotifyTimestamp||''),uniq:(liveData.uniq||('u'+Date.now()))})})\\n .then(function(r){\\n if(r.status===200){ fav=!fav; render(); sys(fav?'已加入收藏':'已取消收藏'); return; }\\n r.text().then(function(t){\\n var msg='收藏失败('+r.status+')';\\n try{ var j=JSON.parse(t); if(j&&j.error) msg='收藏失败:'+j.error; }catch(e){}\\n if(r.status===401||r.status===403) msg='收藏失败:登录已过期,请重新登录';\\n sys(msg);\\n }).catch(function(){ sys('收藏失败('+r.status+')'); });\\n })\\n .catch(function(err){ sys('收藏异常:'+err); });\\n});\\n})();<\/script>\\n<script>(function(){\\nfunction fit(){try{\\n var pc=document.getElementById('playerContainer');\\n var v=document.querySelector('#dplayer video')||document.querySelector('video');\\n if(!pc||!v)return;\\n var w=pc.clientWidth||window.innerWidth||360;\\n if(!window.__fitAr && v.videoWidth && v.videoHeight) window.__fitAr=v.videoWidth\/v.videoHeight;\\n var ar=window.__fitAr||((v.videoWidth&&v.videoHeight)?(v.videoWidth\/v.videoHeight):(16\/9));\\n var h=Math.round(w\/ar);\\n var max=Math.round((window.innerHeight||640)*0.75);\\n if(h>max)h=max;\\n if(h<120)h=Math.round(w*9\/16);\\n pc.style.aspectRatio='auto';\\n pc.style.height=h+'px';\\n try{ var ac=document.querySelector('.app-container'); if(ac) ac.style.setProperty('padding-top',h+'px','important'); }catch(e){}\\n}catch(e){}}\\nsetInterval(fit,1500);\\nwindow.addEventListener('resize',fit);\\ndocument.addEventListener('loadedmetadata',fit,true);\\ndocument.addEventListener('playing',fit,true);\\nsetTimeout(fit,800);setTimeout(fit,2500);\\n})();<\/script>\\n<script>(function(){try{\\nvar ck=((window.liveData&&liveData.cookieHeader)||'');\\nck.split(';').forEach(function(p){p=p.trim();if(p&&p.indexOf('=')>0){document.cookie=p+'; path=\/';}});\\n}catch(e){}})();<\/script>\\n<script>(function(){\\nfunction sys(t){try{var box=(document.getElementById('chatContainer')||document.getElementById('chat'));var d=document.createElement('div');d.className='m';d.style.color='#7fd1ff';d.textContent=t;box.appendChild(d);box.scrollTop=box.scrollHeight;}catch(e){}}\\nsetTimeout(function(){\\n fetch('\/api\/front\/v3\/config\/initial-dynamic',{credentials:'include'}).then(function(r){return r.text().then(function(t){return {s:r.status,t:t};});}).then(function(x){\\n var user=null;try{var j=JSON.parse(x.t);user=(j.initialDynamic&&j.initialDynamic.user)||null;}catch(e){}\\n window.__myUid=(user&&user.id)||null;window.__myName=(user&&user.username)||'';\\n sys('登录态:'+(user?('已登录 '+user.username):'未登录(接口状态 '+x.s+')'));\\n }).catch(function(e){ sys('登录态检测失败:'+e); });\\n},1200);\\n})();<\/script>\\n<script>(function(){\\nfunction sys(t){try{var box=(document.getElementById('chatContainer')||document.getElementById('chat'));var d=document.createElement('div');d.className='m';d.style.color='#ff9b9b';d.textContent=t;box.appendChild(d);box.scrollTop=box.scrollHeight;}catch(e){}}\\nfunction send(){\\n try{\\n var inp=document.getElementById('ci'); if(!inp) return;\\n var txt=(inp.value||'').trim(); if(!txt) return;\\n var plain=txt.replace(\/:[a-zA-Z]+:\/g,'').replace(\/\\\\s+\/g,'');\\n if(!plain){ sys('站点不允许只发表情,请加点文字'); return; }\\n if(!liveData||!liveData.modelId){ sys('缺少模型ID'); return; }\\n inp.value='';\\n fetch('\/api\/front\/models\/'+liveData.modelId+'\/chat',{method:'POST',credentials:'include',headers:{'Content-Type':'application\/json','Accept':'application\/json'},body:JSON.stringify({text:txt,csrfToken:(liveData.csrfToken||''),csrfTimestamp:(liveData.csrfTimestamp||''),csrfNotifyTimestamp:(liveData.csrfNotifyTimestamp||''),uniq:(liveData.uniq||('u'+Date.now()+Math.random().toString(16).slice(2)))})})\\n .then(function(r){return r.text().then(function(t){return {ok:r.ok,status:r.status,t:t};});})\\n .then(function(x){\\n if(x.ok){ sys('发送成功'); }\\n else { var msg=''; try{var j=JSON.parse(x.t);msg=j.error||(j.data&&JSON.stringify(j.data))||x.t;}catch(e){msg=x.t||x.status;} var hs=String(x.t).trim().slice(0,1)==='<'?'(返回HTML,可能未登录\/风控)':''; sys('发送失败('+x.status+')'+hs+':'+String(msg).slice(0,120)); }\\n })\\n .catch(function(e){ sys('发送异常:'+e); });\\n }catch(e){ sys('发送异常:'+e); }\\n}\\nvar b=document.getElementById('cs'); if(b) b.addEventListener('click',send);\\nvar i=document.getElementById('ci'); if(i) i.addEventListener('keydown',function(e){ if(e.key==='Enter'){ e.preventDefault(); send(); } });\\n})();<\/script>\\n<script>(function(){\\nfunction sys(t){try{var box=(document.getElementById('chatContainer')||document.getElementById('chat'));if(!box)return;var d=document.createElement('div');d.className='m';d.style.color='#ffd479';d.textContent=t;box.appendChild(d);box.scrollTop=box.scrollHeight;}catch(e){}}\\nvar reconnecting=false, attempts=0;\\nsetInterval(function(){\\n try{\\n if(typeof chatWs==='undefined'||!chatWs) return;\\n if(chatWs.readyState===3){\\n if(reconnecting) return; reconnecting=true;\\n attempts++;\\n if(attempts>8){ sys('弹幕重连次数过多,请重新打开播放器'); return; }\\n sys('弹幕断开,正在重连('+attempts+')…');\\n fetch('\/api\/front\/v3\/config\/initial-dynamic',{credentials:'include'})\\n .then(function(r){return r.json();})\\n .then(function(j){\\n var d=(j&&j.initialDynamic)||{}, w=d.websocket||{};\\n if(w.url) liveData.websocketUrl=w.url;\\n if(w.token) liveData.websocketToken=w.token;\\n try{ initChat(); }catch(e){}\\n })\\n .catch(function(){})\\n .then(function(){ setTimeout(function(){ reconnecting=false; }, 4000); });\\n }\\n }catch(e){}\\n},3000);\\n})();<\/script>\\n<\/body><\/html>\";\nvar url=String(chapter.url||'');\nvar t=Number(book.type||0);\nvar web=false;\ntry{var v=JSON.parse(String(source.getVariable()||'{}'));if(String(v.webMode)==='1')web=true;if(String(v.playMode)==='web')web=true;}catch(e){}\nif((t&8)===8&&(t&4)!==4)web=true;\nif(!web)return url;\nvar id='';try{var mt=String(book.bookUrl||'').match(\/models\\\/(\\d+)\/);id=mt?mt[1]:'';}catch(e){}\nif(!id){try{var mu=url.match(\/\\\/models\\\/(\\d+)\\\/\/);id=mu?mu[1]:'';}catch(e){}}\nif(!id)return url;\n var cam={},bc={},cfg={},mem={};\n try{cam=JSON.parse(String(java.ajax('https:\/\/zh.virtualtaboo.live\/api\/front\/v2\/models\/'+id+'\/cam?uniq=0')));}catch(e){}\n try{bc=(JSON.parse(String(java.ajax('https:\/\/zh.virtualtaboo.live\/api\/front\/v2\/broadcasts\/'+id+'?uniq=0'))).item)||{};}catch(e){}\n try{cfg=JSON.parse(String(java.ajax('https:\/\/zh.virtualtaboo.live\/api\/front\/v3\/config\/initial')));}catch(e){}\n try{mem=JSON.parse(String(java.ajax('https:\/\/zh.virtualtaboo.live\/api\/front\/v2\/models\/'+id+'\/members?uniq=0')));}catch(e){}\n var u=(cam.user&&cam.user.user)||{},c=cam.cam||{};\n var init=cfg.initial||{},common=init.common||{},client=init.client||{};\n var hosts=common.hlsStreamHosts||{},order=['C','C1','D','D1','E','E1','F','F1','A','A1','B','B1'],lines=[];\n for(var li=0;li<order.length;li++){var hh=hosts[order[li]];if(hh&&lines.indexOf(hh)<0)lines.push(hh);}\n if(!lines.length){lines=['doppiocdn.media','doppiocdn.net','doppiocdn.org','doppiocdn.live','doppiocdn.com','doppiocdn1.com'];}\n var ws=client.websocket||{};\n var memCount=(mem.guests||0)+(mem.spies||0)+(mem.invisibles||0)+(mem.greens||0)+(mem.golds||0)+(mem.regulars||0);\n var st=bc.status||u.status||'offline';\n var presets=((bc.settings&&bc.settings.presets)||[]).filter(function(p){return p&&String(p).indexOf('blurred')===-1;});\n if(st!=='public'){presets=[];}\n if(!presets.length&&st==='public'){var hh2=(bc.settings&&bc.settings.height)||0;presets=(hh2>=720)?['720p','480p','240p']:['480p','240p'];}\n var tips=[];if(c.tipMenu&&c.tipMenu.settings){tips=c.tipMenu.settings.map(function(x){return {activity:x.activity||'',price:x.price||0};});}\n var data={status:st,isLive:!!bc.isLive,stream:bc.streamName||id,username:u.username||'',topic:c.topic||'',startTime:bc.statusChangedAt||'',tipsCreatedAt:(c.tipMenu&&c.tipMenu.createdAt)||'',membersCount:memCount,hlsLines:lines,pixelatedResolutions:presets,websocketUrl:ws.url||'',websocketToken:ws.token||'',modelId:id,topBestPlace:u.topBestPlace||0,userDescription:u.description||'',goalDescription:(c.goal&&c.goal.description)||'',fanClubDescription:(c.userFanClub&&c.userFanClub.description)||'',tipMenuPriceList:tips,csrfToken:(client.csrfToken||''),csrfTimestamp:(client.csrfTimestamp||''),csrfNotifyTimestamp:(client.csrfNotifyTimestamp||''),uniq:(client.sessionHash||''),isInFavorites:!!(cam.user&&cam.user.isInFavorites),goalActual:(c.goal&&c.goal.spent)||0,goalTarget:(c.goal&&c.goal.goal)||0,goalLeft:(c.goal&&c.goal.left),cookieHeader:String(cookie.getCookie('https:\/\/zh.virtualtaboo.live\/')||''),avatarUrl:(u.avatarUrl||''),cdn:(lines[0]||''),coverImg:(bc.snapshotTimestamp?'https:\/\/img.doppiocdn.com\/thumbs\/'+bc.snapshotTimestamp+'\/'+(bc.streamName||id)+'_webp':(u.avatarUrl||''))};\n try{cookie.setWebCookie('https:\/\/zh.virtualtaboo.live\/',String(cookie.getCookie('https:\/\/zh.virtualtaboo.live\/')||''));}catch(e){}\n try{\n var dj=JSON.stringify(data).replace(\/<\/g,'\\\\u003c');\n var h2=LG_PLAYER_HTML.split('__LIVE_DATA__').join(dj).split('__DATA__').join(dj);\n java.startBrowser('https:\/\/zh.virtualtaboo.live\/'+(data.username||''),'VirtualTaboo 播放器',h2);\n }catch(e){}\n return '已在网页播放器打开\\n'+url;\n})()",
"subContent": "@js:(function(){\nvar id='';try{var m=String(book.bookUrl||'').match(\/models\\\/(\\d+)\/);id=m?m[1]:'';}catch(e){}\nif(!id)return '';\nvar d={};try{d=JSON.parse(String(java.ajax('https:\/\/zh.virtualtaboo.live\/api\/front\/v2\/models\/'+id+'\/chat?limit=100')));}catch(e){}\nvar ms=(d&&d.messages)||[];\nif(!ms.length)return '';\nfunction esc(s){return String(s==null?'':s).replace(\/&\/g,'&').replace(\/<\/g,'<').replace(\/>\/g,'>').replace(\/\"\/g,'"');}\nvar out=['<i>'];\nfor(var i=0;i<ms.length;i++){\n var mm=ms[i];if(!mm||!mm.details)continue;\n var txt='',color=16777215;\n if(mm.type==='text'){txt=mm.details.body||'';}\n else if(mm.type==='tip'){txt='💰 打赏 '+(mm.details.amount||0)+' 代币';color=16744192;}\n else continue;\n var u=(mm.userData&&mm.userData.username)||'';\n var body=u?(u+':'+txt):txt;\n var t=(1+i*0.8).toFixed(1);\n out.push('<d p=\"'+t+',1,25,'+color+',0,0,0,0\">'+esc(body)+'<\/d>');\n}\nout.push('<\/i>');\nreturn out.join('');\n})()"
},
"ruleExplore": {
"bookList": "$.blocks..models[*]||$.models[*]",
"bookUrl": "https:\/\/zh.virtualtaboo.live\/api\/front\/v2\/models\/{{$.id}}\/cam?timezoneOffset=-480&triggerRequest=loadCam&primaryTag=girls&withRecentShow=1&skipTipMenu=1&uniq=ix9yufd20we8bscq",
"coverUrl": "@js:var sn=result.snapshotTimestamp;var st=result.streamName;if(sn&&st){'https:\/\/img.doppiocdn.com\/thumbs\/'+sn+'\/'+st+'_webp'}else if(result.previewUrlThumbBig){result.previewUrlThumbBig}else if(result.previewUrl){result.previewUrl}else if(result.previewUrlThumbSmall){result.previewUrlThumbSmall}else if(result.avatarUrl){result.avatarUrl}else{''}",
"kind": "@js:var s={'public':'免费','groupShow':'付费'}[result.status]||'离线';var c=result.country?result.country.toUpperCase()+' ':'未知';c+' | '+(s)+(result.isLive?(' | '+(result.viewersCount||0)+'在看'):'')",
"name": "@js:\nvar hide=false;\ntry{var v=JSON.parse(String(source.getVariable()||'{}'));hide=(String(v.showPaid)==='0');}catch(e){}\nvar s=\"{{$.status}}\",n=\"{{$.username}}\";\nif(hide && s!=='public'){result='';}else{result=n;}"
},
"ruleSearch": {
"author": "$.username",
"bookList": "$.models[*]",
"bookUrl": "https:\/\/zh.virtualtaboo.live\/api\/front\/v2\/models\/{{$.id}}\/cam?timezoneOffset=-480&triggerRequest=loadCam&primaryTag=girls&withRecentShow=1&skipTipMenu=1&uniq=ix9yufd20we8bscq",
"checkKeyWord": "bao",
"coverUrl": "@js:try{var sn=result.streamName;var ts=result.snapshotTimestamp;if(sn&&ts){'https:\/\/img.doppiocdn.live\/thumbs\/'+ts+'\/'+sn+'_webp'}else if(result.previewUrl){result.previewUrl}else if(result.previewUrlThumbBig){result.previewUrlThumbBig}else if(result.previewUrlThumbSmall){result.previewUrlThumbSmall}else if(result.avatarUrl){result.avatarUrl}else{''}}catch(e){''}",
"intro": "@js:lgSearchIntro(result)",
"kind": "@js:result=(function(r){if(typeof r==='string'){try{r=JSON.parse(r);}catch(e){}}var sm={'public':'免费','groupShow':'付费','private':'私秀','p2p':'1v1'}[r.status]||'离线';var a=[];var c=r.country?String(r.country).trim():'';if(c)a.push(c.toUpperCase());a.push(sm);var v=(r.viewersCount!=null)?r.viewersCount:0;if(v>0)a.push(v+'在看');return a.join(' | ');})(result);",
"name": "@js:var s=\"{{$.status}}\",n=\"{{$.username}}\";var m={'public':'','groupShow':'(付费)','p2p':'(一对一)','private':'(私秀)','off':'(离线)'};var tag=(m[s]!==undefined)?m[s]:(s?'('+s+')':'');result=n+(tag||'');"
},
"ruleToc": {
"chapterList": "@js:(function(){\nvar t=Number(book.type||0);\nvar pm='',web=false;\ntry{var v=JSON.parse(String(source.getVariable()||'{}'));pm=String(v.playMode||'');if(pm==='web')web=true;if(!pm&&String(v.webMode)==='1')web=true;}catch(e){}\nvar text;\nif(web)text=true;else if(pm==='native')text=false;else text=((t&8)===8&&(t&4)!==4);\nbook.type=text?8:4;\nvar d=(typeof result==='string')?JSON.parse(result):result;\nvar it=d.item||{};\nif(!text){return lgBuildToc(result,false,java);}\nvar un=it.username||it.name||book.name||'';\nvar lv=it.isLive?'在线':'离线';\nvar pt={'public':'免费','groupShow':'付费','p2p':'一对一','private':'私秀','exclusivePrivate':'专属私秀','idle':'空闲','off':'离线'}[it.status]||'未知';\nvar np=lv;if(pt&&pt!==lv)np+=' · '+pt;\nreturn [JSON.stringify({n:un+' · '+np,u:'https:\/\/zh.virtualtaboo.live\/'+un,v:0})];\n})()",
"chapterName": "@js:(function(){var e=(typeof result==='string')?JSON.parse(result):result;return e.n||''})()",
"chapterUrl": "@js:(function(){var e=(typeof result==='string')?JSON.parse(result):result;return e.u||''})()",
"isVolume": "@js:(function(){var e=(typeof result==='string')?JSON.parse(result):result;return e.v===1?'1':'0'})()"
},
"searchUrl": "https:\/\/zh.virtualtaboo.live\/api\/front\/v4\/models\/search\/group\/username?query={{key}}&limit=100&offset={{(page-1)*100}}&primaryTag=girls&uniq=",
"variableComment": "showPaid:1=发现页显示付费\/离线主播,0=隐藏;playMode:web=网页播放器,native=内置播放器;均可在发现页顶部选择切换",
"weight": 0
}