// ==UserScript==
// @name agefans Enhance
// @namespace https://github.com/IronKinoko/agefans-enhance
// @version 1.10.0
// @description 增强agefans播放功能,实现自动换集、无缝换集、画中画、历史记录、断点续播、显示视频源、获取当前页面全部视频等功能
// @author IronKinoko
// @include https://www.agefans.net/*
// @include https://www.agefans.cc/*
// @include https://www.agefans.vip/*
// @include http://www.yhdm.so/v/*
// @resource plyrCSS https://cdn.jsdelivr.net/npm/plyr@3.6.4/dist/plyr.min.css
// @require https://cdn.jsdelivr.net/npm/jquery@1.12.4/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
// @grant GM_getResourceText
// @grant GM_addStyle
// @license MIT
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
try {
let plyrCSS = GM_getResourceText('plyrCSS')
GM_addStyle(plyrCSS)
} catch(e) { /* empty */ }
})();
(function ($, Plyr, Hls) {
'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var $__default = /*#__PURE__*/_interopDefaultLegacy($);
var Plyr__default = /*#__PURE__*/_interopDefaultLegacy(Plyr);
var Hls__default = /*#__PURE__*/_interopDefaultLegacy(Hls);
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 < t.length; n++) e.setAttribute(t[n], r.attributes[t[n]]);
var a = "prepend" === s ? "afterbegin" : "beforeend";
return i.insertAdjacentElement(a, e), e;
}
}
var css$6 = ".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(css$6,{});
function renderHistroyStyle() {
// add a tag visited style
let styleDom = document.createElement('style');
styleDom.innerHTML = `.movurl li a:visited { color: red; }`;
document.head.appendChild(styleDom);
}
function detailModule() {
renderHistroyStyle();
}
var css$5 = ".agefans-wrapper #history {\n background: #202020;\n border: 4px solid #303030;\n}\n.agefans-wrapper #history .history-list {\n padding: 16px;\n display: flex;\n flex-wrap: wrap;\n}\n.agefans-wrapper #history .history-item {\n width: 115px;\n display: inline-block;\n margin: 4px;\n}\n.agefans-wrapper #history .history-item img {\n width: 100%;\n border-radius: 2px;\n}\n.agefans-wrapper #history .history-item .desc .title {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n font-size: 14px;\n margin: 4px 0;\n}\n.agefans-wrapper #history .history-item .desc .position {\n font-size: 14px;\n}";
n(css$5,{});
class History {
constructor() {
this.cacheKey = 'v-his';
}
get his() {
return JSON.parse(localStorage.getItem(this.cacheKey) || '[]');
}
set his(value) {
if (Array.isArray(value)) {
localStorage.setItem(this.cacheKey, JSON.stringify(value.slice(0, 100)));
}
}
getAll() {
return this.his;
}
get(id) {
return this.his.find(o => o.id === id);
}
setTime(id, time = 0) {
const his = this.his;
his.find(o => o.id === id).time = time;
this.his = his;
}
log(item) {
const his = this.his;
his.unshift(item);
this.his = his;
}
refresh(id, data) {
const his = this.his;
const index = his.findIndex(o => o.id === id);
const item = his.splice(index, 1)[0];
his.unshift(data || item);
this.his = his;
}
has(id) {
return Boolean(this.his.find(o => o.id === id));
}
logHistory() {
var _location$pathname$ma;
const id = (_location$pathname$ma = location.pathname.match(/\/play\/(\d*)/)) === null || _location$pathname$ma === void 0 ? void 0 : _location$pathname$ma[1];
if (!id) return;
const hisItem = {};
hisItem.id = id;
hisItem.title = $__default['default']('#detailname a').text();
hisItem.href = location.href;
hisItem.section = $__default['default']('li a[style*="color: rgb(238, 0, 0);"]').text();
hisItem.time = 0;
hisItem.logo = $__default['default']('#play_poster_img').attr('src');
if (this.has(id)) {
const oldItem = this.get(id);
if (oldItem.href !== hisItem.href) {
this.refresh(id, hisItem);
} else {
this.refresh(id);
}
} else {
this.log(hisItem);
}
}
}
const his = new History();
function parseTime(time = 0) {
return `${Math.floor(time / 60).toString().padStart(2, '0')}:${(time % 60).toString().padStart(2, '0')}`;
}
function renderHistoryList() {
$__default['default']('#history').html('').append(() => {
/** @type {any[]} */
const histories = his.getAll();
let html = '';
histories.forEach(o => {
html += `
${o.title}
${o.section} ${parseTime(o.time)}
`;
});
return `
');
this.$message.appendTo($__default['default'](selector));
}
info(text, duration = 1500) {
this.$message.empty();
return new Promise(resolve => {
$__default['default'](`
`).append(text).hide().appendTo(this.$message).fadeIn(150).delay(duration).fadeOut(150, function () {
$__default['default'](this).remove();
resolve();
});
});
}
destroy() {
this.$message.empty();
}
}
const SHIFT_KEY = '~!@#$%^&*()_+{}|:"<>?' + '~!@#¥%…&*()——+「」|:“《》?';
/**
* @param {string[]} keys
* @param {(e:KeyboardEvent,key:string)=>void} cb
*/
function keybind(keys, cb) {
const ua = navigator.userAgent;
if (!ua.includes('Mac OS')) {
keys = keys.filter(key => !key.includes('meta'));
}
$__default['default'](window).on('keydown', e => {
let keyArr = [];
e.ctrlKey && keyArr.push('ctrl');
e.metaKey && keyArr.push('meta');
e.shiftKey && !SHIFT_KEY.includes(e.key) && keyArr.push('shift');
e.altKey && keyArr.push('alt');
if (!['Control', 'Meta', 'Shift', 'Alt'].includes(e.key)) {
keyArr.push(e.key);
}
keyArr = [...new Set(keyArr)];
const key = keyArr.join('+');
if (keys.includes(key)) {
cb(e, key);
}
});
}
const speedList = [0.5, 0.75, 1, 1.25, 1.5, 2, 4];
const MediaErrorMessage = {
1: '你中止了媒体播放',
2: '一个网络错误导致媒体下载中途失败',
3: '由于损坏问题或媒体使用了你的浏览器不支持的功能,媒体播放被中止了',
4: '媒体无法被加载,要么是因为服务器或网络故障,要么是因为格式不被支持',
5: '该媒体是加密的,我们没有解密的钥匙'
};
class KPlayer {
/**
* Creates an instance of KPlayer.
* @param {stromg} selector
* @param {Plyr.Options} opts
*/
constructor(selector, opts) {
const $wrapper = $__default['default']('
').replaceAll(selector);
const $loading = $__default['default'](loadingHTML);
const $error = $__default['default'](errorHTML);
const $video = $__default['default']('
');
const $progress = $__default['default'](progressHTML);
const $header = $__default['default']('');
$wrapper.append($video);
this.plyr = new Plyr__default['default']('#k-player', {
autoplay: true,
keyboard: {
global: true
},
controls: [// 'play-large', // The large play button in the center
'play', // Play/pause playback
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'duration', // The full duration of the media
'mute', // Toggle mute
'volume', // Volume control
'settings', // Settings menu
'pip', // Picture-in-picture (currently Safari only)
'fullscreen' // Toggle fullscreen
],
seekTime: 5,
speed: {
options: speedList
},
i18n: {
restart: '重播',
rewind: '快退 {seektime}s',
play: '播放(空格键)',
pause: '暂停(空格键)',
fastForward: '快进 {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} / {duration}',
played: '已播放',
buffered: '已缓冲',
currentTime: '当前时间',
duration: '片长',
volume: '音量',
mute: '静音(M)',
unmute: '取消静音(M)',
enableCaptions: '显示字幕',
disableCaptions: '隐藏字幕',
download: '下载',
enterFullscreen: '进入全屏(F)',
exitFullscreen: '退出全屏(F)',
frameTitle: '标题名称: {title}',
captions: '字幕',
settings: '设置',
pip: '画中画',
menuBack: '返回上级',
speed: '倍速',
normal: '1.0x',
quality: '分辨率',
loop: '循环',
start: '开始',
end: '结束',
all: '全部',
reset: '重置',
disabled: '禁用',
enabled: '启用',
advertisement: '广告',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD'
}
},
tooltips: {
controls: true,
seek: true
},
...opts
});
this.$wrapper = $wrapper;
this.$loading = $loading;
this.$error = $error;
this.$video = $video;
this.$progress = $progress;
this.$header = $header;
this.$videoWrapper = $wrapper.find('.plyr');
this.$videoWrapper.append($loading).append($error).append($progress).append($header);
this.message = new Message(this.$videoWrapper);
this.eventMap = {};
this.isWideScreen = false;
this.wideScreenBodyStyles = {};
this.statusSessionKey = 'k-player-status';
this._injectQuestion();
this._injectNext();
this._injectSreen();
this._initEvent();
/** @private */
this.isHoverControls = false;
/** @private */
this.hideCursorDebounced = debounce(() => {
const dom = document.querySelector('.plyr');
dom.classList.add('plyr--hide-cursor');
}, 1000);
/** @private */
this.hideControlsDebounced = debounce(() => {
const dom = document.querySelector('.plyr');
if (!this.isHoverControls) dom.classList.add('plyr--hide-controls');
}, 1000);
const status = window.sessionStorage.getItem(this.statusSessionKey);
if (status) {
window.sessionStorage.removeItem(this.statusSessionKey);
this._toggleFullscreen(JSON.parse(status));
}
}
/** @private */
_initEvent() {
this.on('loadstart', () => {
this.$loading.show();
this.hideError();
});
this.on('canplay', () => {
this.$loading.hide();
this.plyr.play();
});
this.on('error', () => {
const code = this.plyr.media.error.code;
this.$loading.hide();
this.showError(MediaErrorMessage[code] || this.src);
if (code === 3) {
const countKey = 'skip-error-retry-count' + window.location.search;
let skipErrorRetryCount = parseInt(window.sessionStorage.getItem(countKey) || '0');
if (skipErrorRetryCount < 3) {
skipErrorRetryCount++;
const duration = 2 * skipErrorRetryCount;
this.message.info(`视频源出现问题,第${skipErrorRetryCount}次尝试跳过${duration}s错误片段`, 4000).then(() => {
this.trigger('skiperror', 2 * skipErrorRetryCount);
});
window.sessionStorage.setItem(countKey, skipErrorRetryCount.toString());
} else {
this.message.info(`视频源出现问题,多次尝试失败,请手动跳过错误片段`, 4000).then(() => {
this.trigger('skiperror', 0);
});
window.sessionStorage.removeItem(countKey);
}
} else {
const $dom = $__default['default']('
视频播放失败,点击此处暂时关闭脚本功能,使用原生播放器观看
').css('cursor', 'pointer');
$dom.on('click', () => {
this.message.destroy();
window.sessionStorage.setItem('stop-use', '1');
window.location.reload();
});
this.message.info($dom, 10000);
}
});
this.on('pause', () => {
this.hideControlsDebounced();
});
this.on('enterfullscreen', () => {
this.$videoWrapper.addClass('k-player-fullscreen');
});
this.on('exitfullscreen', () => {
this.$videoWrapper.removeClass('k-player-fullscreen');
});
this.on('timeupdate', () => {
this.$progress.find('.k-player-progress-current').css('width', this.currentTime / this.plyr.duration * 100 + '%');
this.$progress.find('.k-player-progress-buffer').css('width', this.plyr.buffered * 100 + '%');
});
keybind([// 进退 30s
'shift+ArrowLeft', 'shift+ArrowRight', // 进退 60s
'alt+ArrowLeft', 'alt+ArrowRight', // 进退 90s
'ctrl+ArrowLeft', 'ctrl+ArrowRight', 'meta+ArrowLeft', 'meta+ArrowRight', // 下一集
'n', ']', '】', 'PageDown', // 上一集
'p', '[', '【', 'PageUp', // 切换网页全屏
'w', // 关闭网页全屏
'Escape', // 播放速度
'z', 'x', 'c'], (e, key) => {
switch (key) {
case 'ctrl+ArrowLeft':
case 'meta+ArrowLeft':
case 'shift+ArrowLeft':
case 'alt+ArrowLeft':
case 'ctrl+ArrowRight':
case 'meta+ArrowRight':
case 'shift+ArrowRight':
case 'alt+ArrowRight':
{
e.stopPropagation();
e.preventDefault();
const time = {
'ctrl+ArrowLeft': 90,
'meta+ArrowLeft': 90,
'shift+ArrowLeft': 30,
'alt+ArrowLeft': 60,
'ctrl+ArrowRight': 90,
'meta+ArrowRight': 90,
'shift+ArrowRight': 30,
'alt+ArrowRight': 60
}[key];
if (e.key === 'ArrowLeft') {
this.currentTime = Math.max(0, this.currentTime - time);
this.message.info(`步退${time}s`);
} else {
this.currentTime = Math.min(this.currentTime + time, this.plyr.duration);
this.message.info(`步进${time}s`);
}
break;
}
case 'n':
case ']':
case '】':
case 'PageDown':
e.preventDefault();
this.trigger('next');
break;
case 'p':
case '[':
case '【':
case 'PageUp':
e.preventDefault();
this.trigger('prev');
break;
case 'w':
if (this.plyr.fullscreen.active) break;
this._toggleFullscreen();
break;
case 'Escape':
if (this.plyr.fullscreen.active || !this.isWideScreen) break;
this._toggleFullscreen(false);
break;
case 'z':
this.plyr.speed = 1;
this.message.info(`视频速度:${1}`);
break;
case 'x':
case 'c':
{
let idx = speedList.indexOf(this.plyr.speed);
const newIdx = key === 'x' ? Math.max(0, idx - 1) : Math.min(speedList.length - 1, idx + 1);
if (newIdx === idx) break;
const speed = speedList[newIdx];
this.message.info(`视频速度:${speed}`);
this.plyr.speed = speed;
break;
}
}
});
document.querySelectorAll('.plyr__controls .plyr__control').forEach(dom => {
dom.addEventListener('click', e => {
e.currentTarget.blur();
});
});
const playerEl = document.querySelector('.plyr');
playerEl.addEventListener('mousemove', () => {
playerEl.classList.remove('plyr--hide-cursor');
this.hideCursorDebounced();
if (this.plyr.paused) {
this.hideControlsDebounced();
}
});
const controlsEl = document.querySelector('.plyr__controls');
controlsEl.addEventListener('mouseenter', () => {
this.isHoverControls = true;
});
controlsEl.addEventListener('mouseleave', () => {
this.isHoverControls = false;
});
}
/** @typedef {'prev'|'next'|'enterwidescreen'|'exitwidescreen'|'skiperror'} CustomEventMap */
/**
* @param {CustomEventMap | keyof Plyr.PlyrEventMap} event
* @param {function} callback
* @private
*/
on(event, callback) {
if (['prev', 'next', 'enterwidescreen', 'exitwidescreen', 'skiperror'].includes(event)) {
if (!this.eventMap[event]) this.eventMap[event] = [];
this.eventMap[event].push(callback);
} else {
this.plyr.on(event, callback);
}
}
/**
* @param {CustomEventMap} event
* @param {*} [params]
*/
trigger(event, params) {
const fnList = this.eventMap[event] || [];
fnList.forEach(fn => {
fn(this, params);
});
}
/** @private */
_injectQuestion() {
$__default['default'](`
`).appendTo(this.$header).on('click', () => {
showInfo();
});
}
/** @private */
_injectNext() {
$__default['default']($__default['default']('#plyr__next').html()).insertBefore('.plyr__controls__item.plyr__progress__container').on('click', () => {
this.trigger('next');
});
}
/** @private */
_injectSreen() {
$__default['default']($__default['default']('#plyr__widescreen').html()).insertBefore('[data-plyr="fullscreen"]').on('click', () => {
this._toggleFullscreen();
});
}
/** @private */
_toggleFullscreen(bool = !this.isWideScreen) {
if (this.isWideScreen === bool) return;
this.isWideScreen = bool;
window.sessionStorage.setItem(this.statusSessionKey, JSON.stringify(this.isWideScreen));
if (this.isWideScreen) {
this.wideScreenBodyStyles = $__default['default']('body').css(['overflow']);
$__default['default']('body').css('overflow', 'hidden');
this.$wrapper.addClass('k-player-widescreen');
$__default['default']('.plyr__widescreen').addClass('plyr__control--pressed');
} else {
$__default['default']('body').css(this.wideScreenBodyStyles);
this.$wrapper.removeClass('k-player-widescreen');
$__default['default']('.plyr__widescreen').removeClass('plyr__control--pressed');
}
this.trigger(this.isWideScreen ? 'enterwidescreen' : 'exitwidescreen');
}
/**
* video src
* @param {string} src
*/
set src(src) {
if (src.includes('.m3u8')) {
if (!Hls__default['default'].isSupported()) throw new Error('不支持播放 hls 文件');
const hls = new Hls__default['default']();
hls.loadSource(src);
hls.attachMedia(this.$video[0]);
} else {
this.$video.attr('src', src);
}
}
get src() {
return this.$video.attr('src');
}
set currentTime(value) {
this.plyr.currentTime = value;
}
get currentTime() {
return this.plyr.currentTime;
}
showError(text) {
this.$error.show().find('.error-info').text(text);
}
hideError() {
this.$error.hide();
}
}
function addReferrerMeta() {
if ($__default['default']('meta[name=referrer]').length === 0) {
$__default['default']('head').append('
');
} else {
const $meta = $__default['default']('meta[name=referrer]');
$meta.attr('content', 'same-origin');
}
}
function showInfo() {
const video = $__default['default']('#k-player')[0];
const githubIssueURL = genIssueURL({
title: '🐛[Bug]',
body: issueBody(video === null || video === void 0 ? void 0 : video.src)
});
modal({
title: '脚本信息',
content: scriptInfo(video, githubIssueURL)
});
}
keybind(['?', '?'], e => {
if (!document.fullscreenElement) {
e.stopPropagation();
e.preventDefault();
showInfo();
}
});
/**
* @param {string} url
* @param {number} [count=0]
* @return {string}
*/
function parseToURL(url, count = 0) {
if (count > 4) throw new Error('url解析失败');
try {
url = new URL(url);
} catch (error) {
url = decodeURIComponent(url);
url = parseToURL(url, ++count);
}
return url.toString();
}
function copyToClipboard(element) {
var $temp = $__default['default']('