// ==UserScript== // @name agefans Enhance // @namespace https://github.com/IronKinoko/agefans-enhance // @icon https://www.agemys.com/favicon.ico // @version 1.23.8 // @description 增强agefans播放功能,实现自动换集、无缝换集、画中画、历史记录、断点续播、弹幕等功能 // @author IronKinoko // @include https://www.age.tv/* // @include https://www.agefans.* // @include https://www.agemys.* // @include *://*.yhdm.so/* // @include *://*.yinghuacd.com/* // @include https://www.yhdmp.cc/vp/* // @include http://www.imomoe.live/player/* // @include http://www.88dmw.com/* // @include https://new-ani.me/* // @include https://bangumi.online/* // @include https://danmu.4dm.cc/m3u8.php* // @include http*://www.ntyou.* // @include https://www.dm233.* // @include https://www.olevod.com* // @run-at document-body // @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/plyr@3.6.4/dist/plyr.min.js // @require https://cdn.jsdelivr.net/npm/hls.js@1.0.9/dist/hls.min.js // @require https://cdn.jsdelivr.net/npm/@ironkinoko/danmaku@1.2.6/dist/danmaku.umd.min.js // @resource plyrCSS https://cdn.jsdelivr.net/npm/plyr@3.6.4/dist/plyr.min.css // @grant GM_getResourceText // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect api.acplay.net // @connect chinacloudsites.cn // @license MIT // @downloadURL none // ==/UserScript== /** * 权限声明: * 1. GM_xmlhttpRequest * 脚本会请求有限的网络权限。仅用于访问弹幕查询功能需要链接到的 api.acplay.net 与 chinacloudsites.cn 第三方域名 * 你可以从 脚本编辑/设置/XHR安全 中管理网络权限 * * 2. GM_getResourceText, GM_addStyle * 获取播放器样式文件,用于播放器样式渲染 * * 3. GM_getValue, GM_setValue * 脚本会使用本地存储功能,用于在不同页面间保存“播放器配置”与“agefans 历史浏览记录”。 * * 4. @include * 脚本还匹配了 agefans 以外的一些链接,用于提供相同视频资源搜索功能 */ (function() { let plyrCSS = GM_getResourceText('plyrCSS') GM_addStyle(plyrCSS) })(); (function (Hls, Plyr, Danmaku) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var Hls__default = /*#__PURE__*/_interopDefaultLegacy(Hls); var Plyr__default = /*#__PURE__*/_interopDefaultLegacy(Plyr); var Danmaku__default = /*#__PURE__*/_interopDefaultLegacy(Danmaku); var e=[],t=[];function n(n,r){if(n&&"undefined"!=typeof document){var a,s=!0===r.prepend?"prepend":"append",d=!0===r.singleTag,i="string"==typeof r.container?document.querySelector(r.container):document.getElementsByTagName("head")[0];if(d){var u=e.indexOf(i);-1===u&&(u=e.push(i)-1,t[u]={}),a=t[u]&&t[u][s]?t[u][s]:t[u][s]=c();}else a=c();65279===n.charCodeAt(0)&&(n=n.substring(1)),a.styleSheet?a.styleSheet.cssText+=n:a.appendChild(document.createTextNode(n));}function c(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),r.attributes)for(var t=Object.keys(r.attributes),n=0;n typeof test === 'string' ? target.includes(test) || test === '*' : test.test(target); } class Runtime { constructor() { this.list = []; } register(item) { this.list.push(item); } async getSearchActions() { const isInIframe = parent !== self; const searchs = this.list .map((o) => o.search) .filter(Boolean) .filter((o) => !(isInIframe && o.disabledInIframe)); const register = this.getActiveRegister(); const info = await this.getCurrentVideoNameAndEpisode(); if (!(info === null || info === void 0 ? void 0 : info.name)) return []; let name = info.name; return searchs .filter((search) => search !== register.search && search.search) .map((search) => ({ name: search.name, search: () => { const url = search.search(encodeURIComponent(name)); if (!url) return; if (isInIframe) parent.postMessage({ key: 'openLink', url }, '*'); else window.open(url); }, })); } async getCurrentVideoNameAndEpisode() { var _a, _b, _c; const register = this.getActiveRegister(); if (!((_a = register.search) === null || _a === void 0 ? void 0 : _a.getSearchName)) return; let rawName = await register.search.getSearchName(); let episode = (await ((_c = (_b = register.search).getEpisode) === null || _c === void 0 ? void 0 : _c.call(_b))) || ''; if (!rawName) return; let name = rawName .replace(/第.季/, '') .replace(/[<>《》''‘’""“”\[\]]/g, '') .trim(); episode = episode.replace(/[第集]/g, '').replace(/^0+/, ''); return { name, rawName, episode }; } getActiveRegister() { const registers = this.list.filter(({ domains }) => domains.some(createTest(location.origin))); if (registers.length !== 1) throw new Error('激活的域名应该就一个'); return registers[0]; } getActiveOpts() { const register = this.getActiveRegister(); return register.opts.filter(({ test }) => { const testArr = Array.isArray(test) ? test : [test]; return testArr.some(createTest(location.pathname + location.search)); }); } run() { let setupList = []; let runList = []; const opts = this.getActiveOpts(); opts.forEach(({ run, runInIframe, setup }) => { if (runInIframe || parent === self) { setup && setupList.push(setup); runList.push(run); } }); setupList.forEach((setup) => setup()); window.addEventListener('DOMContentLoaded', () => { runList.forEach((run) => run()); }); } } const runtime = new Runtime(); var css$d = ".agefans-wrapper .loginout a {\n cursor: pointer;\n text-decoration: underline;\n}\n.agefans-wrapper .loginout a + a {\n margin-left: 8px;\n}\n.agefans-wrapper .nav_button {\n cursor: pointer;\n}\n.agefans-wrapper .res_links {\n word-break: break-all;\n word-wrap: break-word;\n}\n\n@media (max-width: 480px) {\n .nav_button:nth-child(n+6) {\n display: inline-block;\n }\n\n #nav {\n position: relative;\n overflow-x: auto;\n white-space: nowrap;\n height: 91px;\n }\n #nav::-webkit-scrollbar {\n display: none;\n }\n #nav .nav_button {\n white-space: nowrap;\n }\n\n #top_search_from {\n width: calc(100% - 16px);\n float: left;\n margin-top: 10px;\n position: sticky;\n left: 8px;\n margin: 8px;\n }\n\n #new_tip1 {\n margin-top: 10px !important;\n }\n}"; n(css$d,{}); var css$c = ".agefans-wrapper .page-preview-trigger .page-preview {\n position: fixed;\n pointer-events: none;\n background-color: rgb(32, 32, 32);\n border: 1px solid rgb(64, 64, 65);\n z-index: 1000;\n display: flex;\n border-radius: 4px;\n overflow: hidden;\n}\n.agefans-wrapper .page-preview-trigger .page-preview-center {\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.agefans-wrapper .page-preview-trigger .page-preview .baseblock2 {\n border: none;\n border-left: 1px solid #404041;\n border-radius: 0;\n}\n.agefans-wrapper .page-preview-trigger .blocktitle.detail_title1 {\n color: #e0e0e0;\n border-bottom: 1px solid #404041;\n}\n.agefans-wrapper .page-preview-trigger .detail_imform_tag {\n display: inline-block;\n color: #808081;\n min-width: 5em;\n}\n.agefans-wrapper .page-preview-trigger .detail_imform_value {\n color: #e0e0e0;\n}\n.agefans-wrapper .page-preview-trigger .detail_imform_show_full {\n display: none;\n}\n.agefans-wrapper .page-preview-trigger .detail_imform_kv {\n min-width: 200px;\n max-width: 256px;\n display: inline-block;\n margin: 3px 0px;\n word-break: break-all;\n word-wrap: break-word;\n}\n.agefans-wrapper .page-preview-trigger .detail_imform_desc_pre {\n font-size: 15px;\n}\n.agefans-wrapper .page-preview-trigger .detail_imform_desc_pre * {\n color: #e0e0e0;\n}\n.agefans-wrapper .page-preview-trigger .detail_imform_name {\n margin: 0px;\n color: #d0e0f0;\n font-size: 1.2em;\n font-weight: bold;\n display: inline-block;\n}"; n(css$c,{}); function createStorage$1(storage) { function getItem(key, defaultValue) { try { const value = storage.getItem(key); if (value) return JSON.parse(value); return defaultValue; } catch (error) { return defaultValue; } } return { getItem, setItem(key, value) { storage.setItem(key, JSON.stringify(value)); }, removeItem: storage.removeItem.bind(storage), clear: storage.clear.bind(storage), }; } const session = createStorage$1(window.sessionStorage); const local = createStorage$1(window.localStorage); const gm = { getItem: GM_getValue, setItem: GM_setValue, }; function parseToURL(url, count = 0) { if (count > 4) throw new Error('url解析失败 ' + url); try { url = new URL(url); } catch (error) { url = decodeURIComponent(url); url = parseToURL(url, ++count); } return url.toString(); } function ageBlock(params) { const { title, content } = params; return `
${title}
${content}
`; } function set(name, value, _in_days = 1) { var Days = _in_days; var exp = new Date(); exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000); document.cookie = name + '=' + escape(String(value)) + ';expires=' + exp.toUTCString() + ';path=/'; } function get(name) { let reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); let arr = document.cookie.match(reg); if (arr) { return decodeURIComponent(arr[2]); } else { return null; } } const Cookie = { get, set, remove: function (name) { set(name, '', 0); }, }; /** * agefans 安全机制: * 1. 从服务端获取cookie `t1` `k1` * 2. 本地根据规则生成cookie `t2` `k2` * 3. 获取链接时候生成cookie `fa_t` `fa_c` * * t1 t2 fa_t 均为时间,相差太多就报错超时 * k1 k2 类似密钥 * fa_c 不重要 */ /** * 获取视频链接的请求地址 */ function getPlayUrl(_url) { const _rand = Math.random(); var _getplay_url = _url.replace(/.*\/play\/(\d+?)\?playid=(\d+)_(\d+).*/, '/_getplay?aid=$1&playindex=$2&epindex=$3') + '&r=' + _rand; /** * fa_t 取当前时间 * fa_c 1-9之间随便取 固定1就行 */ Cookie.set('fa_t', Date.now(), 1); Cookie.set('fa_c', 1, 1); return _getplay_url; } /** * 因为agefans的安全策略,需要刷新下cookie才能正常访问 * * 这个方法实现了 t1 k1 t2 k2 全部刷新 */ function updateCookie(href) { href = href ? location.origin + href : location.href; return new Promise((resolve, reject) => { var _a, _b, _c; const doneFn = () => { resolve(); dom.remove(); }; // DOMContentLoaded is faster than load const dom = document.createElement('iframe'); dom.style.display = 'none'; dom.src = href; document.body.append(dom); (_a = dom.contentWindow) === null || _a === void 0 ? void 0 : _a.addEventListener('DOMContentLoaded', doneFn); (_b = dom.contentWindow) === null || _b === void 0 ? void 0 : _b.addEventListener('load', doneFn); (_c = dom.contentWindow) === null || _c === void 0 ? void 0 : _c.addEventListener('error', reject); }); } const LOCAL_PLAY_URL_KEY = 'play-url-key'; function insertBTSites() { const title = $('#detailname a').text(); const encodedTitle = encodeURIComponent(title); const sites = [ { title: '蜜柑计划', url: `https://mikanani.me/Home/Search?searchstr=${encodedTitle}`, }, ]; $(ageBlock({ title: '种子资源:', content: sites .map((site) => `${site.title}`) .join(''), })).insertAfter('.baseblock:contains(网盘资源)'); } const loadingIcon = ` `; function getLocal(href) { const map = session.getItem(LOCAL_PLAY_URL_KEY, {}); if (href) { const item = map[href]; if (!item) return null; return item.url; } return map; } function saveLocal(href, url) { const map = getLocal(); map[href] = { url }; session.setItem(LOCAL_PLAY_URL_KEY, map); } function removeLocal(href) { const map = getLocal(); delete map[href]; session.setItem(LOCAL_PLAY_URL_KEY, map); } class AGEfansError extends Error { constructor(message) { super(message); this.name = 'AGEfans Enhance Exception'; } } async function getVurl(href) { const res = await fetch(getPlayUrl(href), { referrerPolicy: 'strict-origin-when-cross-origin', }); const text = await res.text(); if (text.includes('ipchk')) { throw new AGEfansError(`你被限流了,请5分钟后重试(${text})`); } if (text.includes('timeout')) { throw new AGEfansError(`Cookie过期,请刷新页面重试(${text})`); } function __qpic_chkvurl_converting(_in_vurl) { const vurl = decodeURIComponent(_in_vurl); const match_resl = vurl.match(/^http.+\.f20\.mp4\?ptype=http\?w5=0&h5=0&state=1$/); return !!match_resl; } const _json_obj = JSON.parse(text); const _purl = _json_obj['purl']; const _vurl = _json_obj['vurl']; const _playid = _json_obj['playid']; if (__qpic_chkvurl_converting(_vurl)) { throw new AGEfansError('视频转码中,请稍后再试'); } if (_playid === 'QLIVE') { throw new AGEfansError('脚本不支持QLIVE模式,请使用关闭脚本使用原生播放'); } let _url = _purl + _vurl; let url = new URL(_url, location.origin); const vurl = url.searchParams.get('url'); return parseToURL(vurl); } async function getVurlWithLocal(href) { let vurl = getLocal(href); if (vurl) { return vurl; } await updateCookie(href); vurl = await getVurl(href); saveLocal(href, vurl); return vurl; } function initGetAllVideoURL() { insertBTSites(); } var css$b = ".k-modal {\n position: fixed;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n text-align: left;\n animation: fadeIn 0.3s ease forwards;\n color: rgba(0, 0, 0, 0.85);\n}\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.k-modal * {\n color: inherit;\n}\n.k-modal-mask {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n background: rgba(0, 0, 0, 0.45);\n cursor: pointer;\n}\n.k-modal-wrap {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n overflow: auto;\n text-align: center;\n user-select: none;\n}\n.k-modal-wrap::before {\n content: \"\";\n display: inline-block;\n width: 0;\n height: 100%;\n vertical-align: middle;\n}\n.k-modal-container {\n margin: 20px 0;\n display: inline-block;\n vertical-align: middle;\n text-align: left;\n position: relative;\n width: 520px;\n min-height: 100px;\n background: white;\n border-radius: 2px;\n user-select: text;\n}\n.k-modal-header {\n font-size: 16px;\n padding: 16px;\n border-bottom: 1px solid #f1f1f1;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.k-modal-close {\n cursor: pointer;\n height: 55px;\n width: 55px;\n position: absolute;\n right: 0;\n top: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n user-select: none;\n}\n.k-modal-close * {\n color: rgba(0, 0, 0, 0.45);\n transition: color 0.15s ease;\n}\n.k-modal-close:hover * {\n color: rgba(0, 0, 0, 0.85);\n}\n.k-modal-body {\n padding: 16px;\n font-size: 14px;\n}\n.k-modal-footer {\n padding: 10px 16px;\n font-size: 14px;\n border-top: 1px solid #f1f1f1;\n display: flex;\n justify-content: flex-end;\n}\n.k-modal-btn {\n user-select: none;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 32px;\n line-height: 32px;\n border-radius: 2px;\n border: 1px solid #1890ff;\n background: #1890ff;\n color: white;\n min-width: 64px;\n cursor: pointer;\n padding: 0 8px;\n}"; n(css$b,{}); function modal(opts) { const { title, content, onClose, onOk, okText = '确 定' } = opts; const store = { width: document.body.style.width, overflow: document.body.style.overflow, }; const ID = Math.random().toString(16).slice(2); $(` `).appendTo('body'); // init css $('body').css({ width: `calc(100% - ${window.innerWidth - document.body.clientWidth}px)`, overflow: 'hidden', }); $(`#${ID} .k-modal-title`).append(title); $(`#${ID} .k-modal-body`).append(content); $(`#${ID} .k-modal-close`).on('click', () => { handleClose(); }); $(`#${ID} .k-modal-container`).on('click', (e) => { e.stopPropagation(); }); $(`#${ID} .k-modal-wrap`).on('click', () => { handleClose(); }); function reset() { $(`#${ID}`).remove(); $('body').css(store); window.removeEventListener('keydown', fn, { capture: true }); } function handleClose() { onClose === null || onClose === void 0 ? void 0 : onClose(); reset(); } function handleOk() { onOk === null || onOk === void 0 ? void 0 : onOk(); reset(); } function fn(e) { if (['Escape', '?', '?'].includes(e.key)) { handleClose(); } e.stopPropagation(); } window.addEventListener('keydown', fn, { capture: true }); if (onOk) { $(`#${ID} .k-modal-container`).append(` `); $(`#${ID} .k-modal-ok`).on('click', () => { handleOk(); }); } } var css$a = ".k-alert {\n margin-bottom: 16px;\n box-sizing: border-box;\n color: rgba(0, 0, 0, 0.8509803922);\n font-size: 14px;\n font-variant: tabular-nums;\n line-height: 1.5715;\n list-style: none;\n font-feature-settings: \"tnum\";\n position: relative;\n display: flex;\n align-items: center;\n padding: 8px 15px;\n word-wrap: break-word;\n border-radius: 2px;\n}\n.k-alert-icon {\n margin-right: 8px;\n display: inline-block;\n color: inherit;\n font-style: normal;\n line-height: 0;\n text-align: center;\n text-transform: none;\n vertical-align: -0.125em;\n text-rendering: optimizeLegibility;\n -webkit-font-smoothing: antialiased;\n color: #1890ff;\n}\n.k-alert-content {\n flex: 1;\n min-width: 0;\n}\n.k-alert-info {\n background-color: #e6f7ff;\n border: 1px solid #91d5ff;\n}"; n(css$a,{}); function alert(html) { return `
${html}
`; } var css$9 = ".agefans-setting-item {\n cursor: pointer;\n width: 100%;\n white-space: nowrap;\n display: flex;\n align-items: center;\n line-height: 1;\n}\n.agefans-setting-item + .agefans-setting-item {\n margin-top: 12px;\n}\n.agefans-setting-item input {\n margin-right: 4px;\n}"; n(css$9,{}); const LOCAL_SETTING_KEY = 'agefans-setting'; const defaultSetting = { usePreview: true, }; function ensureDefaultSetting() { let setting = gm.getItem(LOCAL_SETTING_KEY, defaultSetting); setting = Object.assign({}, defaultSetting, setting); gm.setItem(LOCAL_SETTING_KEY, setting); } function setSetting(key, value) { const setting = gm.getItem(LOCAL_SETTING_KEY); setting[key] = value; gm.setItem(LOCAL_SETTING_KEY, setting); } function getSetting(key) { const setting = gm.getItem(LOCAL_SETTING_KEY); if (key) return setting[key]; return setting; } function showSetting() { const setting = getSetting(); const $usePreview = $(``); $usePreview .find('input') .prop('checked', setting.usePreview) .on('change', (e) => { setSetting('usePreview', e.target.checked); }); const $stopUseKPlayer = $(``); $stopUseKPlayer .find('input') .prop('checked', session.getItem('stop-use')) .on('change', (e) => { session.setItem('stop-use', e.target.checked); }); modal({ title: '脚本设置', okText: '刷新页面', onOk: () => location.reload(), content: $('
').append(alert('这些配置需要刷新页面才能生效'), $usePreview, $stopUseKPlayer), }); } function settingModule() { ensureDefaultSetting(); $('设置').on('click', showSetting).insertBefore('.loginout a'); } function pagePreview(trigger, previewURL) { if (!getSetting('usePreview')) return; const $popover = $(``).appendTo($(trigger)); function caclPosition(e) { const safeArea = 16; const offset = 20; const width = $popover.width() || 0; const height = $popover.height() || 0; const { innerWidth, innerHeight } = window; const { clientX, clientY } = e; const maxLeft = innerWidth - width - safeArea * 2; const maxTop = innerHeight - height - safeArea; const left = Math.min(clientX + offset, maxLeft); const top = // 当指针与 popover 重叠时,切换位置 clientX + offset > maxLeft && clientY + offset > maxTop ? clientY - offset - height : Math.min(clientY + offset, maxTop); $popover.css({ left, top }); } let isLoaded = false; let timeId; $(trigger) .addClass('page-preview-trigger') .on('mouseenter', (e) => { $popover.show(); caclPosition(e); if (isLoaded) return; clearTimeout(timeId); timeId = window.setTimeout(async () => { if (isLoaded) return; isLoaded = true; let { img, info } = session.getItem(previewURL, { img: '', info: '' }); if (!info) { const $root = $(await fetch(previewURL).then((r) => r.text())); img = $root .find('#container > div.div_left > div:nth-child(1) > div > img') .css({ display: 'block', width: 256, height: 356, }) .prop('outerHTML'); const $info = $root .find('#container > div.div_left > div:nth-child(2) > div > div') .width(256); $info .find('.blocktitle.detail_title1') .text($root.find('.detail_imform_name').text()); info = $info.prop('outerHTML'); session.setItem(previewURL, { img, info }); } $popover.empty().append(img, info); caclPosition(e); }, 100); }) .on('mousemove', (e) => { caclPosition(e); }) .on('mouseleave', () => { clearTimeout(timeId); $popover.hide(); }); } function renderHistroyStyle() { $('