Greasy Fork

Greasy Fork is available in English.

我只想好好观影

本脚本的目的是为了最小化观影的门槛。

当前为 2023-02-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        我只想好好观影
// @namespace   liuser.betterworld.love
// @match       https://movie.douban.com/subject/*
// @match       https://m.douban.com/movie/*
// @grant       GM_xmlhttpRequest
// @grant       GM_download
// @grant       unsafeWindow
// @connect     *
// @run-at      document-end
// @require     https://unpkg.com/[email protected]/dist/artplayer.js
// @require     https://unpkg.com/[email protected]/dist/hls.min.js
// @version     1.8
// @author      liuser
// @description 本脚本的目的是为了最小化观影的门槛。
// @license MIT
// ==/UserScript==

//finish for循环检测有资源的链接
//finish 开始搜索时先搜索所有资源的链接,选出返回最快的那个
//如果点击播放5秒内没反应就多点几下


(function () {

    let mode = "debug"
    //调试log
    let log_machine = (function (mode) {
        if (mode == "debug") {
            return function (log) {
                console.log(log)
            }
        } else {
            return function (log) {

            }
        }
    })(mode)

    var art = {} //播放器
    var dp = {} //dplayer
    //样式
    let css = `
    .TalionNav{
    z-index:10;
    }
    .liu-playContainer{
    width:100%;
    height:100%;
    background-color:white;
    position:fixed;
    top:0;
    z-index:11;
  }
  .liu-closePlayer{
  float:right;
  margin-inline:10px;
  }
  .video-selector{
      display:flex;
      flex-wrap:wrap;
    width:100%;
    overflow:scroll;
    margin-top:10px;
  }
  .liu-selector:hover{
      color:#aed0ee;
    background-color:none;
  }
  .liu-selector{
    color:black;
      cursor:pointer;
    padding:3px;
      margin:5px;
    border-radius:2px;
  }
  .liu-sourceButton{
    margin-inline:5px;
  }
  .liu-rapidPlay{
    color: #007722;
  }


  `

    //搜索源
    let testSearchSource = [
        // {"name":"闪电资源","searchUrl":"https://sdzyapi.com/api.php/provide/vod/"},//不太好,格式经常有错
        { "name": "卧龙资源", "searchUrl": "https://collect.wolongzyw.com/api.php/provide/vod/" },
        { "name": "ikun资源", "searchUrl": "https://ikunzyapi.com/api.php/provide/vod/from/ikm3u8/at/json/" },
        // {"name":"天空资源","searchUrl":"https://m3u8.tiankongapi.com/api.php/provide/vod/from/tkm3u8/"},//有防火墙,垃圾
        { "name": "非凡资源", "searchUrl": "http://cj.ffzyapi.com/api.php/provide/vod/" },
        // { "name": "飞速资源", "searchUrl": "https://www.feisuzyapi.com/api.php/provide/vod/" },//经常作妖或者没有资源
        { "name": "红牛资源", "searchUrl": "https://www.hongniuzy2.com/api.php/provide/vod/from/hnm3u8/" },
        { "name": "高清资源", "searchUrl": "https://api.1080zyku.com/inc/apijson.php/" },
        { "name": "光速资源", "searchUrl": "https://api.guangsuapi.com/api.php/provide/vod/from/gsm3u8/" },
        { "name": "量子资源", "searchUrl": "https://cj.lziapi.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://api.yulecj.com/api.php/provide/vod/"},//速度太慢
        // {"name":"无尽资源","searchUrl":"https://api.wujinapi.me/api.php/provide/vod/"},//资源少

    ]


    let device = "pc"
    if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) {
        device = "mobile"
        log_machine(`识别到是手机`)
    }
    let containerClass = ".artplayer-app"//包装器的class
    let artplayerContainer = `
  <div class="liu-playContainer">
    <a class="liu-closePlayer">点击此处关闭播放</a>
    <div class="sourceButtonList"></div>

    <div class="artplayer-app" style="width:100%;height:500px;">
    </div>
    <div>部分影片选集后会出现卡顿,拖动一下进度条即可恢复。</div>

  </div>`//player的contianer

    let videoName = ""
    let videosSelector = `<div class="video-selector"></div>` //剧集选择器的container
    let selector = `<a class="liu-selector" ></a>` //每集的点击按钮
    let playButton = `<a class="liu-rapidPlay">播放</a>`
    let SourceButtonTemplate = `<a class="liu-sourceButton"></a>` //资源选择器






    //创建其他资源的按钮
    async function createSourceButton(name) {
        let tip = htmlToElement(`<span class="liu-tip">正在测速...你先看着,如果此段文字时间过长,那就是出bug了,排最前面的速度最快</span>`)
        let playContainer = document.querySelector(".liu-playContainer")
        playContainer.insertBefore(tip, playContainer.childNodes[0])
        let sortedSource = await sortSource(testSearchSource)
        playContainer.firstChild.remove()
        for (let source of sortedSource) {
            let buttonElement = htmlToElement(SourceButtonTemplate)
            buttonElement.innerText = source.name
            let copy = { ...source }
            buttonElement.onclick = async () => {
                destroyPlayer()
                go(copy)
            }
            playContainer.insertBefore(buttonElement, playContainer.childNodes[0])
        }
    }
    //先创建一个列表,然后再测速
    async function createSourceListFirst(name) {
        //先获得可以搜到资源的列表
        let searchedSource = []
        for (let item of testSearchSource) {
            log_machine(`正在搜索${item.name}`)
            let playList = await search(item.searchUrl, videoName)
            if (playList == 0) continue;
            searchedSource.push({ ...item })
        }
        log_machine(searchedSource[0])
        //先渲染这个列表
        let sourceButtonList = document.querySelector(".sourceButtonList")
        for (let item of searchedSource) {
            let sourceButton = htmlToElement(SourceButtonTemplate)
            log_machine(`给这个按钮命名${item}`)
            sourceButton.innerText = item.name
            sourceButton.onclick = async () => {
                destroyPlayer()
                go({ ...item })
            }
            sourceButtonList.appendChild(sourceButton)

        }
        sourceButtonList.appendChild(htmlToElement(`<span class="liu-tip">...自动排序中,排最前面的速度最快</span>`))
        let sortedSource = await sortSource(searchedSource);
        // 重新渲染列表
        sourceButtonList.innerHTML = ""
        for (let item of sortedSource) {
            let buttonElement = htmlToElement(SourceButtonTemplate)
            buttonElement.innerText = item.name
            buttonElement.onclick = async () => {
                destroyPlayer()
                go({ ...item })
            }
            sourceButtonList.appendChild(buttonElement)
        }

    }





    //添加style样式
    function appendStyle(css) {
        let styleSheet = document.createElement("style")
        styleSheet.innerText = css
        document.head.appendChild(styleSheet)
    }



    //将html转为element
    function htmlToElement(html) {
        var template = document.createElement('template');
        html = html.trim(); // Never return a text node of whitespace as the result
        template.innerHTML = html;
        return template.content.firstChild;
    }
    //修改播放器url
    function changeUrl(url) {
        // dplayer.switchVideo({
        //   url:url,
        // },{})
        art.url = url
        // createPurePlayer(url)
        log_machine(`切换到${url}`)
        // art.url = url
        // art.play()
    }

    //生成剧集
    function createVideoSelector(list) {

        let videosSelectorContainer = htmlToElement(videosSelector);
        let selectorContainer = htmlToElement(selector);
        list.forEach(item => {
            log_machine(`${item.name}:${item.url}`)
            let selectorContainerCopy = selectorContainer.cloneNode()
            selectorContainerCopy.innerText = item.name;
            selectorContainerCopy.onclick = () => {
                log_machine(`正在播放${item.name}:${item.url}`)
                changeUrl(item.url)
            }
            videosSelectorContainer.appendChild(selectorContainerCopy)

        })
        document.querySelector(".liu-playContainer").appendChild(videosSelectorContainer)
    }


    //生成播放器
    function createPlayer(url) {
        let container = htmlToElement(artplayerContainer);
        document.body.appendChild(container)
        //关闭播放器钩子
        let button = document.querySelector(".liu-closePlayer")
        // log_machine(button)
        button.onclick = () => {
            destroyPlayer()
        }
        createPurePlayer(url)

    }


    //生成纯播放器
    function createPurePlayer(url) {
        //播放器
        art = new Artplayer({
            container: containerClass,
            url: url,
            setting: true,
            fullscreen: true,
            airplay: true,
            playbackRate: true,
            autoSize: true,
            // playsInline:false,

            customType: {
                m3u8: function (video, url) {
                    // Attach the Hls instance to the Artplayer instance
                    art.hls = new Hls();
                    art.hls.loadSource(url);
                    art.hls.attachMedia(video);
                    if (!video.src) {
                        video.src = url;
                    }
                },
            },
        });
        // dplayer = new DPlayer({
        //     container: document.querySelector(containerClass),
        //     screenshot: true,
        //     autoplay:true,
        //     airplay:true,
        //     chromecast:true,
        //     video: {
        //         url: url,
        //         type:"hls",
        //     }
        // });
        // art.on('video:loadedmetadata', () => {
        //     art.forward = 10;
        // });
        // art.on('url', (url) => {
        //   console.info('url', url);
        //   art.hls = new Hls();
        //   art.hls.loadSource(url);
        //   art.hls.attachMedia(video);
        // });

    }

    //销毁播放器
    function destroyPlayer() {
        art.destroy();
        document.querySelector(".liu-playContainer").remove();
    }



    //获取豆瓣影片名称
    function getVideoName() {
        if (device == "mobile") {
            videoName = document.querySelector(".sub-title").innerText
            if (window.getSelection().toString() != "") {
                videoName = window.getSelection().toString()
            }
            return videoName
        }
        if (window.getSelection().toString() != "") {
            videoName = window.getSelection().toString()
        }
        if (videoName == "") {
            videoName = document.querySelector("h1>span").innerText
        }
        return videoName
    }


    function getVideoNumbers() {
        let numbers = 0;
        try {
            numbers = document.querySelectorAll(".pl")[7].nextSibling.textContent.slice(1);
        } catch (e) {
            log_machine("获取剧集出现错误,请检查!");
        }
        return numbers;
    }

    function getVideoYear(outYear) {
        let yearEqual = 0;
        try {
            if (device == "mobile") {
                yearEqual = document.querySelector(".sub-original-title").innerText.includes(outYear);
            } else {
                yearEqual = document.querySelector(".year").innerText.includes(outYear)
            }

        } catch (e) {
            log_machine("获取年份失败,请检查!");
        }
        return yearEqual;
    }

    //到电影网站搜索电影
    function search(url, videoName) {
        log_machine(`正在搜索${videoName}`)
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: encodeURI(`${url}?ac=detail&wd=${videoName}`),
                onload: function (r) {
                    try {
                        // log_machine(`搜索结果为${JSON.stringify(r)}`)
                        let response = JSON.parse(r.responseText)
                        resolve(handleResponse(response, videoName));
                    } catch (e) {
                        log_machine("垃圾资源,解析失败了,可能有防火墙")
                        log_machine(e)
                        resolve({ "list": [] })
                    }

                },
                onerror: function (error) {
                    resolve({ "list": [] })
                }
            });
        });
    }

    //处理搜索到的结果:从返回结果中找到对应片子
    function handleResponse(r, searchName) {
        if (r.list.length == 0) {
            log_machine("未搜索到结果")
            return 0
        }
        let video = {};
        let found = false
        for (let item of r.list) {

            log_machine("正在对比剧集年份")
            let yearEqual = getVideoYear(item.vod_year)
            if (yearEqual === 0) return 0
            if (yearEqual) {
                video = { ...item }
                found = true
                break
            }
        }
        if (found == false) {
            log_machine("没有找到匹配剧集的影片,怎么回事哟!")
            return 0
        }

        let videoName = video.vod_name;
        let playList = video.vod_play_url.split("$$$").filter(str => str.includes("m3u8"));
        if (playList.length == 0) {
            log_machine("没有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
    }

    //获取下载的内容
    function gm_download(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: encodeURI(url),
                onload: function (r) {
                    resolve(r.response)
                },
                onerror: function (e) {
                    resolve("html")
                }
            })
        })
    }


    //下载m3u8的内容,返回片段列表
    async function downloadM3u8(url) {
        let domain = url.split("/")[0]
        let baseUrl = url.split("/")[2]
        let downLoadList = []
        log_machine(`正在获取index.m3u8 ${url}`)
        let downloadContent = await gm_download(url)

        if (downloadContent.includes("html")) {
            log_machine(`下载失败,被反爬虫了`)
            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_machine(`测试列表为${downLoadList}`)
        return downLoadList

    }



    //测试下载速度
    async function testSpeed(list) {
        let downloadList = list.slice(0, 5)
        let downloadSize = 0
        let startTime = (new Date()).getTime();


        for (item of downloadList) {
            log_machine("正在下载" + item)
            let r = await makeGetRequest(item)
            downloadSize += r.loaded / 1024
        }

        let endTime = (new Date()).getTime();
        let duration = (endTime - startTime) / 1000
        let speed = downloadSize / duration

        log_machine(`速度为${speed}KB/s`)
        return speed
    }

    //将GM_xmlhttpRequest改造为Promise
    function makeGetRequest(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: encodeURI(url),
                responseType: "arraybuffer",
                onload: function (r) {
                    resolve(r);
                },
                onerror: function (error) {
                    resolve({ "loaded": 0 })
                }
            });
        });
    }

    //生成随机数
    function randomIntFromInterval(min, max) { // min and max included
        return Math.floor(Math.random() * (max - min + 1) + min)
    }

    //创建整个界面
    async function go(SearchSource) {
        // log_machine(`正在搜索${testSearchSource[2].name}`)
        log_machine(`正在搜索${SearchSource.name}`)
        let playList = await search(SearchSource.searchUrl, videoName)
        log_machine(`正在播放${playList[0].name}:${playList[0].url}`)
        createPlayer(playList[0].url)
        createVideoSelector(playList)
        await createSourceListFirst(videoName)
    }


    //测试代码
    // testSpeed(arr)
    // downloadM3u8("https://pps.sd-play.com/20220705/iS7EWI78/index.m3u8")
    // let div = document.createElement("div");
    async function main() {
        appendStyle(css) //添加css
        let rapidPlay = htmlToElement(playButton)
        rapidPlay.onclick = async () => {
            for (let item of testSearchSource) {
                let playList = await search(item.searchUrl, videoName)
                if (playList != 0) {
                    go(item)
                    return
                }
            }
            window.alert("没找到此资源,可能是因为豆瓣标题里夹杂了别的文字,可以选中部分文字后再次点击播放");
        }
        rapidPlay.onmouseover = () => {
            getVideoName()
        }
        if (device == "pc") {
            document.querySelector("h1").appendChild(rapidPlay)
        } else {
            document.querySelector(".sub-original-title").appendChild(rapidPlay)
        }


    }

    //将源根据速度进行排序
    async function sortSource() {
        log_machine("进入排序...")
        let sortedSource = []
        let videoName = getVideoName()
        for (let item of testSearchSource) {
            log_machine(`正在搜索${item.name}`)
            let playList = await search(item.searchUrl, videoName)
            if (playList == 0) continue;
            log_machine(`测速中...正在下载${item.name}`)
            let tsList = await downloadM3u8(playList[0].url)
            let speed = 0
            if (tsList.length == 0) {
                log_machine(`没有找到下载链接,请检查`)
            } else {
                speed = await testSpeed(tsList)
            }

            log_machine(`速度为${speed}`)
            sortedSource.push({ ...item, "speed": speed })
        }
        sortedSource.sort((a, b) => {
            return b.speed - a.speed;//从大到小排序
        })
        log_machine("排序完成...")
        for (let item of sortedSource) {
            log_machine(`${item.name}speed:${item.speed}`)
        }
        return sortedSource
    }

    main()


})()