Greasy Fork

agefans Enhance

增强agefans播放功能,实现自动换集、画中画、历史记录、断点续播等功能

目前为 2021-05-12 提交的版本。查看 最新版本

// ==UserScript==
// @name         agefans Enhance
// @namespace    https://github.com/IronKinoko/agefans-enhance
// @version      1.2.0
// @description  增强agefans播放功能,实现自动换集、画中画、历史记录、断点续播等功能
// @author       IronKinoko
// @match        https://www.agefans.net/*
// @match        https://www.agefans.net/play/*
// @match        https://www.agefans.net/detail/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  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();
  }

  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() {
      const id = location.pathname.match(/\/play\/(\d*)/)?.[1];
      if (!id) return

      const hisItem = {};
      hisItem.id = id;
      hisItem.title = $('#detailname a').text();
      hisItem.href = location.href;
      hisItem.section = $('li a[style*="color: rgb(238, 0, 0);"]').text();
      hisItem.time = 0;
      hisItem.logo = $('#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() {
    $('#history')
      .html('')
      .append(() => {
        /** @type {any[]} */
        const histories = his.getAll();
        let html = '';
        histories.forEach((o) => {
          html += `<a class="history-item" href="${o.href}">
        <img
          referrerpolicy="no-referrer"
          src="${o.logo}"
          alt="${o.title}"
          title="${o.title}"
        />
        <div class="desc">
          <div class="title">${o.title}</div>
          <div class="position">${o.section} ${parseTime(o.time)}</div>
        </div>
      </a>
      `;
        });
        return `<div class="history-list">${
        html || '<center>暂无数据</center>'
      }</div>`
      });
  }

  function renderHistoryPage() {
    const currentDom = $('.nav_button_current');
    $(
      '<style>.nav_button{cursor: pointer;}#history{background:#202020;border:4px solid #303030;}.history-list{padding:16px;display:flex;flex-wrap:wrap;}.history-item{width:115px;display:inline-block;margin:4px}.history-item img{width: 100%;border-radius:2px}.history-item .desc .title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:14px;margin:4px 0}.history-item .desc .position{font-size:14px}</style>'
    ).appendTo('head');
    $('<div id="history"></div>').insertBefore('#footer').hide();

    $(`<a class="nav_button">历史</a>`)
      .appendTo('#nav')
      .on('click', (e) => {
        if ($('#history').is(':visible')) {
          $('#container').show();
          $('#history').hide();
          changeActive(currentDom);
        } else {
          renderHistoryList();
          $('#container').hide();
          $('#history').show();
          changeActive($(e.currentTarget));
        }
      });

    $('.nav_button_current')
      .on('click', (e) => {
        $('#container').show();
        $('#history').hide();
        changeActive(e.currentTarget);
      })
      .removeAttr('href');
  }

  function changeActive(dom) {
    $('.nav_button_current').removeClass('nav_button_current');
    $(dom).addClass('nav_button_current');
  }

  function historyModule() {
    renderHistoryPage();
    renderHistoryList();
  }

  function copyToClipboard(element) {
    var $temp = $("<textarea>");
    $("body").append($temp);
    $temp.val($(element).text()).trigger('select');
    document.execCommand("copy");
    $temp.remove();
  }

  function checkCSS() {
    if ($('#k-modal-css').length === 0) {
      $(`
    <style id="k-modal-css">
      .k-modal {
        position: fixed;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .k-modal * {
        color: rgba(0, 0, 0, 0.85);
      }
      .k-modal .k-modal-mask {
        position: absolute;
        z-index: 100;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.45);
      }
      .k-modal .k-modal-container {
        position: absolute;
        z-index: 101;
        width: 520px;
        min-height: 100px;
        background: white;
        border-radius: 2px;
      }
      .k-modal .k-modal-header {
        font-size: 16px;
        padding: 16px;
        border-bottom: 1px solid #f1f1f1;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      .k-modal .k-modal-close,
      .k-modal .k-modal-mask {
        cursor: pointer;
      }
      .k-modal .k-modal-body,
      .k-modal .k-modal-footer {
        padding: 16px;
        font-size: 14px;
      }
      .k-modal .k-modal-footer {
        border-top: 1px solid #f1f1f1;
        display: flex;
        justify-content: flex-end;
      }
      .k-modal .k-modal-btn {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 32px;
        border-radius: 2px;
        border: 1px solid #2af;
        background: #2af;
        color: white;
        min-width: 64px;
        cursor: pointer;
      }
    </style>
    `).appendTo('head');
    }
  }
  function modal({ title, content, onClose, onOk }) {
    checkCSS();

    const ID = Math.random().toString(16).slice(2);
    $(`
<div class="k-modal" role="dialog" id="${ID}">
  <div class="k-modal-mask"></div>
  <div class="k-modal-container">
    <div class="k-modal-header">
      <div class="k-modal-title"></div>
      <a class="k-modal-close">X</a>
    </div>
    <div class="k-modal-body">
    </div>
  </div>
</div>`).appendTo('body');

    $(`#${ID} .k-modal-title`).append(title);
    $(`#${ID} .k-modal-body`).append(content);
    $(`#${ID} .k-modal-close`).on('click', () => {
      $(`#${ID}`).remove();
      onClose && onClose();
    });
    $(`#${ID} .k-modal-mask`).on('click', () => {
      $(`#${ID}`).remove();
      onClose && onClose();
    });

    if (onOk) {
      $(`#${ID} .k-modal-container`).append(`
      <div class="k-modal-footer">
        <button class="k-modal-btn k-modal-ok">确 定</button>
      </div>
    `);
      $(`#${ID} .k-modal-ok`).on('click', () => {
        onOk();
        $(`#${ID}`).remove();
      });
    }
  }

  /**
   * @typedef {{title:string,href:string}} ATag
   *
   */
  function __setCookie(name, value, _in_days) {
    var Days = _in_days;
    var exp = new Date();
    exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
    document.cookie =
      name + '=' + escape(value) + ';expires=' + exp.toGMTString() + ';path=/';
  }
  function __getCookie(name) {
    var arr,
      reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)');
    if ((arr = document.cookie.match(reg))) {
      return unescape(arr[2])
    } else {
      return null
    }
  }
  function getCookie2(name) {
    return __getCookie(name)
  }

  function FEI2(in_epi) {
    //
    var hf_epi = Number(in_epi);
    const time_curr = new Date().getTime();

    var fa_t = Number(getCookie2('fa_t'));
    if (!fa_t) {
      fa_t = time_curr;
    }

    var fa_c = Number(getCookie2('fa_c'));
    if (!fa_c) {
      fa_c = 0;
    }

    //
    if (time_curr - fa_t > 6000) {
      fa_t = 0;
      fa_c = 0;
    }

    //
    fa_c += 1;
    fa_t = time_curr;

    //
    if (fa_c > 10) {
      fa_t = 0;
      fa_c = 0;
      //
      if (hf_epi > 1) {
        hf_epi = time_curr % hf_epi;
        if (!hf_epi) {
          hf_epi = 1;
        }
      }
    }

    __setCookie('fa_t', fa_t, 1);
    __setCookie('fa_c', fa_c, 1);

    return hf_epi
  }
  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;
    var re_resl = _getplay_url.match(/[&?]+epindex=(\d+)/);
    const hf_epi = '' + FEI2(re_resl[1]);
    const t_epindex_ = 'epindex=';
    _getplay_url = _getplay_url.replace(
      t_epindex_ + re_resl[1],
      t_epindex_ + hf_epi
    );
    return _getplay_url
  }

  function insertBtn() {
    $(`
  <div class="baseblock">
    <div class="blockcontent">
      <div id="wangpan-div" class="baseblock2">
        <div class="blocktitle">获取全部视频链接:</div>
        <div class="blockcontent">
          <a id="open-modal" class="res_links_a" style="cursor:pointer">获取全部视频链接</a>
          <span>|</span>
          <a id="all-select" class="res_links_a" style="cursor:pointer">复制内容</a>
          <div id="url-list" style="width:100%; max-height:400px; overflow:auto;"></div>
        </div>
      </div>
    </div>
  </div>
`).insertAfter($('.baseblock:contains(网盘资源)'));
    $('#all-select').on('click', function () {
      copyToClipboard($('#url-list'));
      $(this).text('已复制');
      setTimeout(() => {
        $(this).text('复制内容');
      }, 1000);
    });
    $('#open-modal').on('click', function () {
      modal({
        title: '选择需要的链接',
        content: insertModalForm(),
        onOk: () => {
          let list = [];
          $('#modal-form input').each(function (_, el) {
            if (el.checked) {
              list.push({
                title: $(this).data('title'),
                href: $(this).attr('name'),
              });
            }
          });
          insertResult(list);
        },
      });
    });
  }

  /**
   * @return {ATag[]}
   */
  function getAllVideoUrlList() {
    const $aTagList = $('.movurl:visible li a');
    const aTags = [];
    $aTagList.each(function (index, aTag) {
      aTags.push({
        title: aTag.textContent,
        href: aTag.getAttribute('href'),
      });
    });

    return aTags
  }

  function insertModalForm() {
    const list = getAllVideoUrlList();

    let $dom = $(`
  <div id="modal-form">
    <ul>
      ${list
        .map(
          (aTag) => `
        <li>
          <label><input type="checkbox" name="${aTag.href}" data-title="${aTag.title}" checked />${aTag.title}</label>
        </li>`
        )
        .join('')}
    </ul>
  </div>
  `);

    return $dom
  }

  function genUrlItem(title, content = '加载中...') {
    return `<div>
  <div style="white-space: nowrap;">[${title}]</div>
  <div class="url" data-status='0' style="word-break:break-all; word-wrap:break-word;">${content}</div>
</div>`
  }
  /**
   * @param {ATag[]} list
   */
  function insertResult(list) {
    const $parent = $('#url-list');
    $parent.empty();
    list.forEach((item) => {
      let $dom = $(genUrlItem(item.title)).appendTo($parent);

      let $msg = $dom.find('.url');
      function _getUrl() {
        fetch(getPlayUrl(item.href))
          .then((res) => res.json())
          .then((res) => {
            const url = decodeURIComponent(res.vurl);
            saveLocal(item.href, item.title, url);
            $msg.text(url);
            $msg.data('status', '1');
          })
          .catch((error) => {
            console.error(error);
            $msg.empty();
            $msg.data('status', '2');
            $(`<a style="cursor:pointer">加载出错,重试</a>`)
              .appendTo($msg)
              .on('click', () => {
                _getUrl();
              });
          });
      }
      _getUrl();
    });
  }

  const PLAY_URL_KEY = 'play-url-key';
  function saveLocal(href, title, url) {
    const map = JSON.parse(window.localStorage.getItem(PLAY_URL_KEY) || '{}');
    map[href] = { title, url };
    window.localStorage.setItem(PLAY_URL_KEY, JSON.stringify(map));
  }
  function getLocal() {
    return JSON.parse(window.localStorage.getItem(PLAY_URL_KEY) || '{}')
  }
  function insertLocal() {
    const map = getLocal();
    const list = getAllVideoUrlList();
    const $parent = $('#url-list');
    $(
      list
        .map((item) => {
          if (map[item.href]) {
            return genUrlItem(item.title, map[item.href].url)
          } else {
            return ''
          }
        })
        .join('')
    ).appendTo($parent);
  }
  function initGetAllVideoURL() {
    insertBtn();
    insertLocal();
  }

  function replacePlayer() {
    const dom = document.getElementById('age_playfram');

    dom.setAttribute('allow', 'autoplay');
    const prefix = 'https://ironkinoko.github.io/agefans-enhance/?url=';

    const fn = () => {
      let url = new URL(dom.src);

      if (url.hostname.includes('agefans')) {
        let videoURL = url.searchParams.get('url');
        if (videoURL) {
          dom.src = prefix + encodeURIComponent(videoURL);
          showCurrentLink(videoURL);
        }
      }
      // 移除版权规避提示
      if ($(dom).css('display') === 'none') {
        $(dom).show();
      }
    };

    const mutationOb = new MutationObserver(fn);
    mutationOb.observe(dom, { attributes: true });
    fn();
  }

  function showCurrentLink(url) {
    $(`
  <div class="baseblock">
    <div class="blockcontent">
      <div id="wangpan-div" class="baseblock2">
        <div class="blocktitle">本集链接:</div>
        <div class="blockcontent">
          <span class="res_links">
            ${decodeURIComponent(url)}
          </span>
          <br>
        </div>
      </div>
    </div>
  </div>
`).insertBefore($('.baseblock:contains(网盘资源)'));
  }

  function gotoNextPart() {
    const dom = document.querySelector("li a[style*='color: rgb(238, 0, 0);']")
      .parentElement.nextElementSibling;

    if (dom) {
      dom.children[0].click();
    }
  }

  function toggleFullScreen() {
    let dom = document.querySelector('.fullscn');
    dom.click();
  }

  function notifyChildToggleFullScreen(isFull) {
    const dom = document.getElementById('age_playfram');
    dom.contentWindow.postMessage({ code: 666, isFull }, '*');
  }

  function initPlayPageStyle() {
    let dom = document.querySelector('.fullscn');
    dom.onclick = () => {
      if (document.body.style.overflow === 'hidden') {
        document.body.style.overflow = '';
        notifyChildToggleFullScreen(false);
      } else {
        document.body.style.overflow = 'hidden';
        notifyChildToggleFullScreen(true);
      }
    };
    dom.style.opacity = 0;

    let ageframediv = document.getElementById('ageframediv');
    let { width } = ageframediv.getBoundingClientRect();
    ageframediv.style.height = (width / 16) * 9 + 'px';
  }

  function prerenderNextPartHTML() {
    const dom = document.querySelector("li a[style*='color: rgb(238, 0, 0);']")
      .parentElement.nextElementSibling;
    if (dom) {
      const link = document.createElement('link');
      link.rel = 'prerender';
      link.href = dom.children[0].href;
      document.head.appendChild(link);
    }
  }

  function updateTime(time = 0) {
    const id = location.pathname.match(/\/play\/(\d*)/)?.[1];
    if (!id) return

    his.setTime(id, Math.floor(time));
  }

  function notifyChildJumpToHistoryPosition() {
    const id = location.pathname.match(/\/play\/(\d*)/)?.[1];
    if (!id) return

    if (his.get(id)?.time && his.get(id)?.time > 3) {
      const dom = document.getElementById('age_playfram');
      dom.contentWindow.postMessage({ code: 999, time: his.get(id).time }, '*');
    }
  }

  function addListener() {
    window.addEventListener('message', (e) => {
      if (e.data?.code === 233) {
        gotoNextPart();
      }

      if (e.data?.code === 200) {
        notifyChildJumpToHistoryPosition();
      }

      if (e.data?.code === 666) {
        toggleFullScreen();
      }

      if (e.data?.code === 999) {
        updateTime(e.data.time);
      }
    });
  }

  function removeCpraid() {
    $('#cpraid').remove();
  }

  function playModule() {
    addListener();
    his.logHistory();
    initPlayPageStyle();
    replacePlayer();
    prerenderNextPartHTML();
    removeCpraid();
    initGetAllVideoURL();
  }

  if (parent === self) {
    historyModule();

    // log page to history
    if (location.pathname.startsWith('/play')) {
      playModule();
    }

    // in detail pages show view history
    if (location.pathname.startsWith('/detail')) {
      detailModule();
    }
  }

}());