我只想好好观影

为了想看就看

Versión del día 1/9/2023. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name        我只想好好观影
// @namespace   liuser.betterworld.love
// @match       https://movie.douban.com/subject/*
// @match       https://m.douban.com/movie/*
// @grant       GM_addStyle
// @grant       GM_xmlhttpRequest
// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @connect     *
// @run-at      document-end
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/artplayer.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/artplayer-plugin-control.js
// @require     https://cdn.staticfile.org/hls.js/1.4.3/hls.min.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js
// @version     3.4
// @author      liuser, collaborated with ray
// @description 为了想看就看
// @license MIT
// ==/UserScript==

//按钮样式


  GM_addStyle(
    `
    .liu-btn{
      cursor:pointer;
      font-size:1rem;
      padding: 0.6rem 1.2rem;
      border: 1px solid transparent;
      border-radius: 3px;
    }
  
    .play-btn {
      border-radius: 8px;
      cursor: pointer;
      font-weight: bolder;
      background-color:#e8f5e9;
    }
    .play-btn:hover {
      background-color:#c8e6c9;
    }
    .play-btn:active{
      background-color: #81c784;
    }
  
    .source-selector{
      background-color: #141414;
      color: #99a2aa;
      padding:0.6rem 0.8rem;
      margin:0.5rem 0.875rem;
      border-radius:4px;
    }
    .source-selector:hover{
      background-color: #1f1f1f;
    }
  
    .series-selector{
      background-color: #141414;
      border-radius:3px;
      color: #99a2aa;
      width:3.5rem;
      height:3.5rem;
      font-size:0.9rem;
      padding:0;
    }
    .series-selector:hover{
      background-color: #153a1d;
    }
  
    .playing{
      border:2px solid #007011;
    }
  
    .selected{
      border:2px solid #007011;
    }
  
  
    .liu-closePlayer{
      border-radius:3px;
      background-color: #141414;
        float:right;
      color: #99a2aa;
      width:2rem;
      height:2rem;
      line-height:2rem;
      padding:0;
      margin:0.5rem 1rem;
    }
    .liu-closePlayer:hover{
      background-color:#1f1f1f;
      color:white;
    }
  
    .love-support{
      background-color:tranparent;
      margin-right:1rem;
    }
    a:hover{
      font-weight:bold;
      background:none;
    }
  
  
    `
  );
  
  //剧集选择器布局
  GM_addStyle(
    `
    .series-contianer{
      display:grid;
      grid-template-columns: repeat(5,1fr);
      grid-column-gap:0.5rem;
      grid-row-gap:0.5rem;
      margin-top:1rem;
    }
    @media screen and (max-width: 1025px) {
    .series-contianer{
      display:grid;
      grid-template-columns: repeat(5,1fr);
      grid-column-gap:0.5rem;
      grid-row-gap:0.5rem;
      margin-top:1rem;
    }
  
    }
  
  
    `
  );
  
  //布局
  GM_addStyle(
    `
  
    :root{
      font-size:16px;
      font-family: BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif !important;
    }
    :root::-webkit-scrollbar {
      display: none;
    }
  
    .TalionNav{
        z-index:10;
    }
    .speed-slow{
        color:#9e9e9e;
    }
    .speed-fast{
        color:#4aa150;
    }
  
  
  
    .mannul{
      margin-top:2rem;
      margin-bottom:3rem;
      font-size:1rem;
      display:flex;
      flex-wrap:wrap;
    }
  
  
  
    .liu-playContainer{
        width:100%;
        height:100%;
        background-color:#1c2022;
        position:fixed;
        top:0;
        z-index:11;
        overflow:auto;
    }
    .liu-playContainer::-webkit-scrollbar {
      display: none;
    }
  
  
  
    .video-selector{
        display:flex;
        flex-wrap:wrap;
        margin-top:1rem;
    }
  
    .liu-selector:hover{
        color:#aed0ee;
        background-color:none;
    }
  
    .liu-selector{
        color:black;
        cursor:pointer;
        padding:3px;
        margin:5px;
        border-radius:2px;
    }
  
    .liu-rapidPlay{
        color: #007722;
    }
  
    .liu-light{
        background-color:#7bed9f;
    }
  
    .artplayer-app{
      height:600px;
    }
  
  
    .playSpace{
        display: grid;
    /* 	height:400px; */
      margin:1rem;
        grid-template-columns: 2fr 1fr;
        grid-row-gap:0px;
        grid-column-gap:1rem;
      margin-top:2rem;
      clear: both;
    }
  
  
  
    @media screen and (max-width: 1025px) {
        .playSpace{
            display: grid;
    /* 		height:600px; */
            grid-template-rows: 1fr 0.5fr;
            grid-template-columns:1fr;
            grid-row-gap:10px;
            grid-column-gap:0px;
        }
    }
  
  
    .seletor-title{
      height:3rem;
      line-height:3rem;
      background-color: #141414;
      color:#fafafa;
      font-size:1.25rem;
      padding: 0 1rem;
    }
    `
  );
  
  // 上传源地址
  const sourceupload = () => {
    let sourceAdded = prompt(
      "请输入自定义源,名称与链接用|隔开,每一项用英文逗号隔开"
    );
    GM_setValue("sourceAdded", sourceAdded);
  };
  // 加入额外的源
  GM_registerMenuCommand("自定义源", sourceupload);
  
  (function () {
    const _debug = 0;
    const searchSource = [
      {
        name: "红牛资源",
        searchUrl:
          "https://www.hongniuzy2.com/api.php/provide/vod/from/hnm3u8/",
      },
      {
        name: "非凡资源",
        searchUrl: "http://cj.ffzyapi.com/api.php/provide/vod/",
      },
      {
        name: "量子资源",
        searchUrl: "https://cj.lziapi.com/api.php/provide/vod/",
      },
      {
        name: "ikun资源",
        searchUrl:
          "https://ikunzyapi.com/api.php/provide/vod/from/ikm3u8/at/json/",
      },
      {
        name: "光速资源",
        searchUrl:
          "https://api.guangsuapi.com/api.php/provide/vod/from/gsm3u8/",
      },
      {
        name: "高清资源",
        searchUrl: "https://api.1080zyku.com/inc/apijson.php/",
      },
      {
        name: "188资源",
        searchUrl: "https://www.188zy.org/api.php/provide/vod/",
      },
      {
        name: "天空资源",
        searchUrl:
          "https://m3u8.tiankongapi.com/api.php/provide/vod/from/tkm3u8/",
      }, //有防火墙,垃圾
      {
        name: "闪电资源",
        searchUrl: "https://sdzyapi.com/api.php/provide/vod/",
      }, //不太好,格式经常有错
      // { "name": "飞速资源", "searchUrl": "https://www.feisuzyapi.com/api.php/provide/vod/" },//经常作妖或者没有资源
      // { "name": "卧龙资源", "searchUrl": "https://collect.wolongzyw.com/api.php/provide/vod/" }, 非常恶心的广告
      // { "name": "8090资源", "searchUrl": "https://api.yparse.com/api/json/m3u8/" },垃圾 可能有墙
      // { "name": "百度云资源", "searchUrl": "https://api.apibdzy.com/api.php/provide/vod/" },
      // { "name": "酷点资源", "searchUrl": "https://kudian10.com/api.php/provide/vod/" },
      // { "name": "淘片资源", "searchUrl": "https://taopianapi.com/home/cjapi/as/mc10/vod/json/" },
      // { "name": "ck资源", "searchUrl": "https://ckzy.me/api.php/provide/vod/" },
      // { "name": "快播资源", "searchUrl": "https://caiji.kczyapi.com/api.php/provide/vod/" },
      // { "name": "海外看资源", "searchUrl": "http://api.haiwaikan.com/v1/vod/" }, // 说是屏蔽了所有中国的IP,所以如果你有外国的ip可能比较好
      // { "name": "68资源", "searchUrl": "https://caiji.68zyapi.com/api.php/provide/vod/" },
      // {"name":"鱼乐资源","searchUrl":"https://api.yulecj.com/api.php/provide/vod/"},//速度太慢
      // {"name":"无尽资源","searchUrl":"https://api.wujinapi.me/api.php/provide/vod/"},//资源少
    ];
    const { query: $, queryAll: $$, isMobile } = Artplayer.utils;
    const tip = (message) => alert(message);
  
    //app的整体结构,作为vue的渲染模板
    const appTemplate = `
    <div  id="app">
      <div class="liu-playContainer" v-show="ok">
        <button class="liu-closePlayer liu-btn" @click="closePlayer">
          <svg class="icon" width="50%" height="50%" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4012"><path d="M587.19 506.246l397.116-397.263a52.029 52.029 0 0 0 0-73.143l-2.194-2.194a51.98 51.98 0 0 0-73.143 0l-397.068 397.8-397.068-397.8a51.98 51.98 0 0 0-73.143 0l-2.146 2.194a51.054 51.054 0 0 0 0 73.143l397.069 397.263L39.544 903.461a52.029 52.029 0 0 0 0 73.142l2.146 2.195a51.98 51.98 0 0 0 73.143 0L511.9 581.583l397.068 397.215a51.98 51.98 0 0 0 73.143 0l2.194-2.146a52.029 52.029 0 0 0 0-73.143L587.19 506.246z" p-id="4013" data-spm-anchor-id="a313x.search_index.0.i1.10e63a81F8aVUU" class="selected" fill="#ffffff"></path></svg>
        </button>
  
        <!-- 播放模块 -->
        <div class="playSpace">
          <!-- 视频容器 -->
          <div class="artplayer-app"></div>
          <!-- 选集模块 -->
          <div class="series">
            <div class="seletor-title">选集</div>
            <div class="series-contianer">
              <button
                class="series-selector liu-btn"
                :class="{'playing':index==playingIndex}"
                style="color: #a3a3a3"
                v-for="(item,index) in playingList"
                :key="index"
                @click="playListSelect(index)"
              >
                {{item.name.slice(0,4)}}
                <!-- 固定宽度不够长 -->
              </button>
            </div>
          </div>
        </div>
  
        <!-- 源选择模块 -->
        <div class="sourceButtonList">
          <button
            class="source-selector liu-btn"
            v-for="(item,index) in sources"
            :key="index"
            :class="{'selected':index==selectedSource,'speed-fast':item.speed>1}"
            @click="sourceSelect(index)"
          >
            {{item.name}} {{item.speed}} m/s
          </button>
        </div>
        <!-- 一些说明 -->
        <div class="mannul">
          <a
            class="love-support"
            href="https://memos.babelgo.cn/m/1"
            target="_blank"
            style="color: #4aa150"
            >☕赏作者喝一杯咖啡?
  
            </a>
          <a
            class="love-support"
            href="https://t.me/wzxhhgy"
            target="_blank"
            style="color: #4aa150"
            >电报群</a
          >
          <a
            class="love-support"
            href="https://greasyfork.org/zh-CN/scripts/459540-%E6%88%91%E5%8F%AA%E6%83%B3%E5%A5%BD%E5%A5%BD%E8%A7%82%E5%BD%B1/feedback"
            target="_blank"
            style="color: #4aa150"
            >👉反馈</a
          >
        </div>
      </div>
    </div>
  
      `;
  
    //构建vue的实例
    function initVue() {
      const e = htmlToElement(appTemplate);
      document.body.appendChild(e);
      vueInstance = new Vue({
        el: "#app",
        data: {
          //artplayer实例
          art: {},
          //标明是否搜到影片
          ok: false,
          //搜索源
          searchSource: searchSource,
          //标记正在下载的资源,好debug
          sourceTesting: "",

          //所有搜索到的资源 [{name:"..资源",playList:[{name:"第一集",url:""}]}]
          sources: [],
          //标记选择的源
          selectedSource: 0,
          //正在播放的选集总表
          playingList: [],
          //正在播放哪一集
          playingIndex: 0,
          //咖啡地址
          coffeeUrl: "https://memos.babelgo.cn/m/1",
          //telegram地址
          telegramUrl: "https://t.me/wzxhhgy",
          //greasyfork反馈地址
          feedbackUrl:
            "https://greasyfork.org/zh-CN/scripts/459540-%E6%88%91%E5%8F%AA%E6%83%B3%E5%A5%BD%E5%A5%BD%E8%A7%82%E5%BD%B1/feedback",
        },
        methods: {
          //merge自定义源
          mergeSource() {
            let sourceAdded = GM_getValue("sourceAdded", "");
            sourceAdded.split(",").forEach((item) => {
              if (item === "") return;
              name_url = item.split("|");
              this.searchSource.push({
                name: name_url[0],
                searchUrl: name_url[1],
              });
            });
          },
          //测试速度
          async testSpeed() {
            //this.sources //所有搜索到的资源 [{name:"..资源",playList:[{name:"第一集",url:""}]}]
            await Promise.all(
              this.sources.map(async (source, index) => {
                let name = source.name;
                let url = source.playList[this.playingIndex].url;
                let tsList = await downloadM3u8(url);
                //如果没有tsList,就直接返回
                if (tsList.length==0){
                  log("测速失败")
                  this.sources[index].speed=0
                  this.$forceUpdate();
                  return
                }
                let downLoadList = [];
                //随机选择8个切片下载
                for (let i = 0; i < 8; i++) {
                  downLoadList.push(
                    tsList[Math.floor(Math.random() * tsList.length)]
                  );
                }
                let downloadSize = 0;
                let startTime = Date.now();

                for (item of downLoadList) {
                  log("正在下载" + item);
                  let r = await getBuffer(item);
                  downloadSize += r.byteLength / 1024 / 1024;
                }
                let endTime = Date.now();
                let duration = (endTime - startTime) / 1000;
                let speed =
                  downloadSize / duration ? downloadSize / duration : 0;
                this.sources[index].speed = Number(speed.toFixed(2));
                // 因为index会冲突所以暂时不用
                // this.sources.sort((a, b) => {
                //   return b.speed - a.speed;
                // });
                this.$forceUpdate();
                // log(this.sources[index]);
                log(`${name}的速度为${speed}mb/s`);
              })
            );
          },
          //选择源的按钮行为
          sourceSelect(index) {
            let ct = this.art.currentTime;
            this.selectedSource = index;
            this.playingList = this.sources[index].playList;
            this.switchUrl(this.playingList[this.playingIndex].url);
            this.art.once("video:canplay", () => {
              this.art.seek = ct;
            });
            this.testSpeed().then(() => {
              log("测速完成!");
            });
          },
          //选择剧集的按钮行为
          playListSelect(index) {
            this.playingIndex = index;
            //打印playlist
            log(this.playingList);
            this.switchUrl(this.playingList[this.playingIndex].url);
          },
          // 初始化Art播放器
          initArt(url) {
            this.art = new Artplayer({
              container: ".artplayer-app",
              url: url,
              pip: true,
              fullscreen: true,
              fullscreenWeb: true,
              autoMini: true,
              screenshot: true,
              hotkey: true,
              airplay: true,
              playbackRate: true,
              setting: true,
              miniProgressBar: true,
              theme: "#00981a",
              moreVideoAttr: {
                crossOrigin: "anonymous",
              },
              controls: [
                {
                  name: "resolution",
                  html: "分辨率",
                  position: "right",
                },
              ],
              customType: {
                m3u8(video, url, art) {
                  if (Hls.isSupported()) {
                    if (art.hls) art.hls.destroy();
                    const hls = new Hls();
                    hls.loadSource(url);
                    hls.attachMedia(video);
                    art.hls = hls;
                    art.on("destroy", () => hls.destroy());
                  } else if (
                    video.canPlayType("application/vnd.apple.mpegurl")
                  ) {
                    video.src = url;
                  } else {
                    art.notice.show = "Unsupported playback format: m3u8";
                  }
                },
              },
              plugins: [artplayerPluginControl()],
            });
            this.art.once("destroy", () => this.art.hls.destroy());
            this.art.on("video:loadedmetadata", () => {
              this.art.controls.resolution.innerText =
                this.art.video.videoHeight + "P";
            });
            log("初始化art实例完成,art:");
            log(this.art);
          },
          //切换播放器的播放url
          switchUrl(url) {
            this.art.switchUrl(url);
            //兼容safari
            if (this.art.video.src != url) {
              this.art.video.src = url;
            }
          },
          //关闭页面
          closePlayer() {
            $("#app").remove();
          },
          
          search(url) {
            return new Promise((resolve, reject) => {
              let splitVideoName = ""
              if (videoName.length>=3){
                splitVideoName = videoName.slice(0,3)
              }else{
                splitVideoName = videoName;
              }
              
              GM_xmlhttpRequest({
                method: "GET",
                url: encodeURI(`${url}?ac=detail&wd=${splitVideoName}`),
                timeout: 3000,
                responseType: "json",
                onload(r) {
                  try {
                    resolve(handleResponse(r.response));
                  } catch (e) {
                    log(e);
                    reject(
                      `${this.sourceTesting}垃圾资源,解析失败了,可能有防火墙`
                    );
                  }
                },
                onerror: reject,
                ontimeout: reject,
              });
            });
          },
          //处理搜索到的结果:从返回结果中找到对应片子
        },
        async created() {
          //初始化时开始搜索所有资源, 初始化sources 数组
          //merge 用户添加的地址
          this.mergeSource();
          // this.searchSource.forEach();
          await Promise.all(
            this.searchSource.map(async (item) => {
              try {
                //标记当前正在处理的资源
                this.sourceTesting = item.name;
                // search里自带解析函数,所以只要通过就说明搜索到了
                let playList = await this.search(item.searchUrl);
                //返回结构 [{name,url,speed}];
                if (playList != 0) {
                  this.sources.push({ name: item.name, playList });
                  if (this.ok == false) {
                    this.ok = true;
                    this.$forceUpdate();
                    this.playingList = playList;
                    this.initArt(this.playingList[0].url);
                    
                  }
                } else {
                  log(`${item.name} 没搜到`);
                  return;
                }
              } catch (e) {
                log(e);
                log(`${item.name}搜索出现错误`);
              }
            })
          );
          // 结束以后ok还为false说明没搜索
          if (this.ok === false) {
            tip("未搜索到资源,可能是豆瓣的电影名称和资源站的名称不一致,请反馈电影名称。");
            window.open(
              "https://greasyfork.org/zh-CN/scripts/459540-%E6%88%91%E5%8F%AA%E6%83%B3%E5%A5%BD%E5%A5%BD%E8%A7%82%E5%BD%B1/feedback"
            );
          } else {
            //开始测速
            await this.testSpeed();
            log("测速完成");
          }
        },
      });
    }
  
    //--------------------------全局方法
  
    //获取豆瓣影片名称
    const videoName = isMobile
      ? $(".sub-title").innerText
      : document.title.slice(0, -5).replace(" ", "");
  
    // debug方法
    const log = (function () {
      if (_debug) return console.log.bind(console);
      return function () {};
    })();
  
    function htmlToElement(html) {
      //将html转为element
      const template = document.createElement("template");
      template.innerHTML = html.trim();
      return template.content.firstChild;
    }
  
    //添加百度统计脚本统计使用情况
    function addScript() {
      let statistic = document.createElement("script");
      statistic.setAttribute(
        "src",
        "https://hm.baidu.com/hm.js?f02301d8266631b0285c3e325c9a574b"
      );
      document.head.appendChild(statistic);
    }
  
    //速度为0不一定是无法播放,可能是源的防火墙阻止了测速,也可以试试。
  function handleResponse(r) {
    log("正在处理搜索的结果");
    if (!r || r.list.length == 0) {
      log("未搜索到结果");
      return 0;
    }
    let video,
      found = false;
    for (let item of r.list) {
      // 对比名称、发行年、演员,只要有一个一样就算成功
      let nameEqual = item.vod_name == videoName;
      let yearEqual = getVideoYear(item.vod_year);
      let actorContain = videoActor(item.vod_actor.split(",")[0]);

      if (yearEqual === true || actorContain === true || nameEqual === true) {
        video = item;
        found = true;
        log(`已找到资源`);
        break;
      }
    }
    if (found == false) {
      log(`没找到资源`);
      return 0;
    }

    let playList = video.vod_play_url
      .split("$$$")
      .filter((str) => str.includes("m3u8"));
    if (playList.length == 0) {
      log("没有m3u8资源, 无法测速, 无法播放");
      return 0;
    }
    playList = playList[0].split("#");
    playList = playList.map((str) => {
      let index = str.indexOf("$");
      return {
        name: str.slice(0, index),
        url: str.slice(index + 1),
        speed: -1,
      };
    });
    return playList;
  }
    

  
    //播放按钮
    class PlayBtn {
      constructor() {
        const e = htmlToElement(
          `<button class="liu-btn play-btn">一键播放</button>`
        );
        $(isMobile ? ".sub-original-title" : "h1").appendChild(e);
  
        e.onclick = function () {
          initVue();
        };
      }
    }
  
    //获取电影的年份
    function getVideoYear(outYear) {
      const e = $(isMobile ? ".sub-original-title" : ".year");
      if (!e) {
        log("获取年份失败,请检查!");
        return 0;
      }
      return e.innerText.includes(outYear);
    }
  
    //对比电影演员
    function videoActor(outActor) {
      const e = $(isMobile ? ".bd" : ".actor");
      if (!e) {
        log("获取演员失败,请检查!");
        return 0;
      }
      //log(`${outActor}:匹配结果${e.innerText.includes(outActor)}`)
      return e.innerText.includes(outActor);
    }
  
    //下载
    const get = (url) => {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: encodeURI(url),
          timeout: 10000,
          onload: function (r) {
            resolve(r.response);
          },
          onerror: function (e) {
            resolve("html");
          },
          ontimeout: function (o) {
            resolve("html");
          },
        });
      });
    };
  
    //下载m3u8的内容,返回片段列表
    async function downloadM3u8(url) {
      let domain = url.split("/")[0];
      let baseUrl = url.split("/")[2];
      let downLoadList = [];
      log(`正在获取index.m3u8 ${url}`);
      let downloadContent = await get(url);
  
      if (downloadContent.includes("html")) {
        log(`下载失败,被反爬虫了`);
        return [];
      }
  
      if (downloadContent.includes("index.m3u8")) {
        //如果是m3u8地址
        let lines = downloadContent.split("\n");
        for (let item of lines) {
          if (/^[#\s]/.test(item)) continue; //跳过注释和空白行
          if (/^\//.test(item)) {
            downLoadList = await downloadM3u8(domain + "//" + baseUrl + item);
          } else if (/^(http)/.test(item)) {
            downLoadList = await downloadM3u8(item);
          } else {
            downLoadList = await downloadM3u8(url.replace("index.m3u8", item));
          }
        }
      } else {
        //如果是ts地址
        let lines = downloadContent.split("\n");
        for (let item of lines) {
          if (/^[#\s]/.test(item)) continue; //跳过注释和空白行
          if (/^(http)/.test(item)) {
            //如果是http直链
            downLoadList.push(item);
          } else if (/^\//.test(item)) {
            //如果是绝对链接
            downLoadList.push(domain + "//" + baseUrl + item);
          } else {
            downLoadList.push(url.replace("index.m3u8", item));
          }
        }
      }
      // log(`测试列表为${downLoadList}`)
      return downLoadList;
    }
  
  
    //将GM_xmlhttpRequest改造为Promise
    function getBuffer(url) {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "GET",
          timeout: 3000,
          url: encodeURI(url),
          responseType: "arraybuffer",
          onload: function (r) {
            resolve(r.response);
          },
          onerror: function (error) {
            log("速度太慢了或无法测速");
            resolve({ byteLength: 0 });
          },
          ontimeout: function (out) {
            log("速度太慢了或无法测速");
            resolve({ byteLength: 0 });
          },
        });
      });
    }
  
    new PlayBtn();
    //添加统计代码
    //   addScript();
  })();