Greasy Fork is available in English.

观影self

不更不答复.请勿反馈. 代码归原作者.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.

// ==UserScript==
// @name        观影self
// @namespace   liuser.betterworld.love
// @match       https://movie.douban.com/subject/*
// @match       https://m.douban.com/movie/*
// @grant       GM_addStyle
// @grant       GM_xmlhttpRequest
// @connect     *
// @run-at      document-end
// @require     https://cdn.jsdelivr.net/npm/xy-ui@1.10.7/+esm
// @require     https://cdn.staticfile.org/artplayer/4.6.2/artplayer.min.js
// @require     https://unpkg.com/hls.js@1.2.9/dist/hls.min.js
// @version     2.4.1
// @author      liuser, collaborated with ray
// @description  不更不答复.请勿反馈. 代码归原作者.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.
// @license MIT
// ==/UserScript==
// ==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
// @antifeature tracking
// @connect     *
// @run-at      document-end
// @require     https://cdn.staticfile.org/artplayer/4.6.2/artplayer.min.js
// @require     https://cdn.staticfile.org/hls.js/1.4.3/hls.min.js
// @version     2.12
// @author      liuser, collaborated with ray
// @description 想看就看
// @license MIT
// ==/UserScript==




(function () {
    const _debug = 0;
    let art = {}; //播放器
    let seriesNum = 0;
    let sourceSelected = false;
    const { query: $, queryAll: $$, isMobile } = Artplayer.utils;
    const tip = (message) => alert(message);




    //获取豆瓣影片名称
    const videoName = isMobile ? $(".sub-title").innerText : document.title.slice(0, -5);

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

    //搜索源
    const searchSource = [

        //自己的放前面
        { "name": "天堂官采", "searchUrl": "http://vipmv.cc/api.php/provide/vod/" },
        { "name": "M3U8官采", "searchUrl": "https://www.zycaiji.net:7788/api.php/provide/vod/" },
         //以下原版
         { "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 pushSource = ()=>{
      let sourceAdded = GM_getValue("sourceAdded","")

      sourceAdded.split(",").forEach((item)=>{
        if(item==="")return
        name_url = item.split("|")
        searchSource.push({
          "name":name_url[0],
          "searchUrl":name_url[1]
        })
      })
    }


    const SourceTop = ()=>{
      let sourceAdded = prompt("请输入自定义源,名称与链接用|隔开,每项用英文逗号隔开")
      GM_setValue("sourceAdded",sourceAdded)
      pushSource()
      }




    GM_registerMenuCommand("自定义源",SourceTop)




    //处理搜索到的结果:从返回结果中找到对应片子
    function handleResponse(r) {
        if (!r || r.list.length == 0) {
            log("未搜索到结果");
            return 0
        }
        let video, found = false;
        for (let item of r.list) {
            log("正在对比剧集年份和演员");
            log(item)
            let yearEqual = getVideoYear(item.vod_year);
            let actorContain = videoActor(item.vod_actor.split(",")[0])

            if (yearEqual === true|| actorContain=== true){
              video = item;
              found = true;
              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) }
        });

        return playList
    }

    //到电影网站搜索电影
    const search = (url) => new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: "GET",
            url: encodeURI(`${url}?ac=detail&wd=${videoName}`),
            timeout: 3000,
            responseType: 'json',
            onload(r) {
                try {
                    resolve(handleResponse(r.response, videoName));
                } catch (e) {
                    log("垃圾资源,解析失败了,可能有防火墙");
                    log(e);
                    reject()
                }
            },
            onerror: reject,
            ontimeout: reject
        });
    });

    //播放按钮
    class PlayBtn {
        constructor() {
            const e = htmlToElement(`<button class="liu-btn play-btn">一键播放</button>`);
            $(isMobile ? ".sub-original-title" : "h1").appendChild(e);
            const render = async (item) => {
                const playList = await search(item.searchUrl);
                if (playList == 0) return;
                if (e.loading) {
                    e.loading = false;
                    new UI(playList);
                }

                //渲染资源列表
                const btn = new SourceButton({ name: item.name, playList }).element;
                if(!sourceSelected){
                  btn.classList.add("selected")
                  sourceSelected = true
                }



                $(".sourceButtonList").appendChild(btn);

            };
            e.onclick = function () {
                e.loading = true;
                //tip("正在搜索");
                searchSource.forEach(render);
                setTimeout(() => {
                    if (e.loading == true) {
                        e.loading = false;
                        tip("未搜索到资源")
                    } else {
                        speedTest()
                    }
                }, 3500);
            };
        }
    }




    class UI {
        constructor(playList) {
            document.body.appendChild(htmlToElement(
                `<div class="liu-playContainer">
				<button class="liu-closePlayer liu-btn">X</button>

				<div class="playSpace" >
					<div class="artplayer-app"></div>
          <div class="series">
            <div class="seletor-title">选集</div>
            <div class="series-contianer"></div>
          </div>
				</div>
        <div class="sourceButtonList"></div>
				<div class="mannul">
          <div class="show-series" style="color:#a3a3a3"></div>
          <a class="love-support liu-btn" href="http://babelgo.cn:5230/m/1" target="_blank" style="color:#4aa150">☕赏作者喝一杯咖啡?</a>
          <a class="love-support liu-btn" href="https://t.me/wzxhhgy" target="_blank" style="color:#4aa150">电报群</a>
          <a class="love-support liu-btn" 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>`
            )).querySelector(".liu-closePlayer").onclick = function () {
                this.parentNode.remove();
                document.body.style.overflow = 'auto';
            };
            document.body.style.overflow = 'hidden';
            //第n集开始播放
            log(playList[seriesNum].url);
            initArt(playList[seriesNum].url);
            new SeriesContainer(playList);
        }
    }

    //初始化播放器
    function initArt(url) {
        art = new Artplayer({
            container: ".artplayer-app",
            url:url,
            pip: true,
            fullscreen: true,
            fullscreenWeb: true,
            screenshot: true,
            hotkey: true,
            airplay: true,
            playbackRate: true,
            controls: [{
                name: "resolution",
                html: "分辨率",
                position: "right"
            }],
            customType: {
                m3u8(video, url) {
                    // Attach the Hls instance to the Artplayer instance
                    if (art.hls) art.hls.destroy();
                    art.hls = new Hls();
                    art.hls.loadSource(url);
                    art.hls.attachMedia(video);
                    if (!video.src) {//兼容safari
                        video.src = url;
                    }
                },
            }
        });
        art.once('destroy', () => art.hls.destroy());
        art.on("video:loadedmetadata", () => {
            art.controls.resolution.innerText = art.video.videoHeight + "P";
        });
        log(art)
    }


      //影视源选择按钮
    class SourceButton {
        constructor(item) {
            this.element = htmlToElement(`<button class="source-selector liu-btn" >${item.name}</button>`);
            this.element.onclick = () => {
                $(".selected")?$(".selected").classList.remove("selected"):null;
                this.element.classList.add("selected")
                switchUrl(item.playList[seriesNum].url);
                new SeriesContainer(item.playList);

            };
            this.element._playList = item.playList
            this.element._sourceName = item.name
        }
        //sources 是[{name:"..资源",playList:[{name:"第一集",url:""}]}]
    }

    //剧集选择器
    class SeriesButton {
        constructor(pNode, name, url, index) {
            let selector = htmlToElement(
                `<button class="series-selector liu-btn" style="color:#a3a3a3" >${name.slice(0,4)}</button>`
            )
            pNode.appendChild(selector).onclick = () => {
                seriesNum = index;
                switchUrl(url);
                $(".playing")?$(".playing").classList.remove("playing"):null;
                // $(".show-series").innerText = `正在播放第${index + 1}集`;
                selector.classList.add("playing")
                speedTest()
            };
        }
    }

    //剧集选择器的container
    class SeriesContainer {
        constructor(playList) {
            //const e = htmlToElement(`<div class="series-select-space" style="display:flex;flex-wrap:wrap;overflow:scroll;align-content: start;"></div>`);
          const e = $(".series-contianer")
          e.innerHTML = ""
          for (let [index, item] of playList.entries()) {
                new SeriesButton(e, item.name, item.url, index);
          }
          seriesNum==0?$(".series-selector").classList.add("playing"):null;
        }
    }



    function switchUrl(url) {//兼容safari
        art.switchUrl(url)
        if (art.video.src != url) {
            art.video.src = url;
        }
    }

    //获取电影的年份
    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(downloadContent)
            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

    }


    //对资源进行测速
    function speedTest() {
        // tip("脚本自动测试源的速度,随后请自行切换源进行尝试")
        let sourceButtons = $$(".source-selector")
        //log(sourceButtons)
        sourceButtons.forEach(async (e) => {
            let url = e._playList[seriesNum].url
            let tsList = await downloadM3u8(url)
            let downloadList = []
            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

            log(`速度为${speed}mb/s`)

            e.innerText = e._sourceName + " " + speed.toFixed(2) + "mb/s"
            let state = speed > 1 ? "fast" : "slow"
            e.classList.add(`speed-${state}`)

        })
    }


    //将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 })
                }
            });
        });
    }

//按钮样式
GM_addStyle(
`
.liu-btn{
  cursor:pointer;
  font-size:1rem;
  padding: 0.6rem 1.2rem;
  border: 1px solid transparent;
}

.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.2rem 0.5rem;
  margin:0.5rem 0.875rem;
  border-radius:4px;
}

.series-selector{
  background-color: #141414;
  border-radius:3px;
  color: #99a2aa;
  width:3.5rem;
  height:3.5rem;
  font-size:0.75rem;
  line-height:3.5rem;
  padding:0;
}

.playing{
  border:1px solid #4caf50;
}

.selected{
  border:1px solid #4caf50;
}


.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{
  margin-top:1rem;
  background-color:#141414;
  margin-right:1rem;
}
.love-support:hover{
  background-color:#1f1f1f;
}


`

);


//剧集选择器布局
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
}

.TalionNav{
	z-index:10;
}
.speed-slow{
	color:#9e9e9e;
}
.speed-fast{
	color:#4aa150;
}



.mannul{
  margin:1rem;
  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;
}



.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;
}
`
    );
    new PlayBtn();
    addScript();
})();