Greasy Fork

Greasy Fork is available in English.

我只想好好观影

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

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

您需要先安装一款用户脚本管理器扩展,例如 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.10
// @author      liuser
// @description 本脚本的目的是为了最小化观影的门槛。
// @license MIT
// ==/UserScript==

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


//添加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;
}

(function () {

    let mode = "production"
    let videoName = ""
    var art = {} //播放器
    var seriesNum = 0
    let device = "pc"
    if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) {
        device = "mobile"
        log_machine(`识别到是手机`)
    }
    //样式
    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;
  }
  .liu-light{
    background-color:#7bed9f;
  }


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

        }
    }
})(mode)

    //搜索源
    let searchSource = [
        // {"name":"闪电资源","searchUrl":"https://sdzyapi.com/api.php/provide/vod/"},//不太好,格式经常有错
        //{ "name": "卧龙资源", "searchUrl": "https://collect.wolongzyw.com/api.php/provide/vod/" }, 非常恶心的广告
        { "name": "光速资源", "searchUrl": "https://api.guangsuapi.com/api.php/provide/vod/from/gsm3u8/" },
        { "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://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://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/" },

      //https://caiji.kczyapi.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/"},//资源少

    ]

    //播放按钮
    //使用类来规范代码
    class playButtonv3 {
        constructor() {
            this.element = htmlToElement(`<a class="liu-rapidPlay">一键播放</a>`)
            this.element.onclick = async () => {

                for (let item of searchSource) {
                    let playList = await search(item.searchUrl, getVideoNamev2())
                    if (playList != 0) {
                        let ui = new UI(playList)
                        ui.init()
                        break
                    }
                }
            }
        }

        mount() {
            if (device == "pc") {
                document.querySelector("h1").appendChild(this.element)
            } else {
                document.querySelector(".sub-original-title").appendChild(this.element)
            }


        }

    }

    //影视源选择按钮
    class SourceButton {
        constructor(item) {
            this.element = htmlToElement(`<a class="liu-sourceButton">${item.name}</a>`)
            this.element.onclick = ()=>{
                art.url = item.playList[seriesNum].url
            }
        }
        //sources 是[{name:"..资源",playList:[{name:"第一集",url:""}]}]

    }

    //资源列表的container
    class SourceListContainer{
        constructor(sources){
            this.element = document.querySelector(".sourceButtonList")
            this.sources = sources

        }

        //渲染资源列表
        async renderList() {
            let videoName = getVideoNamev2()
            let filteredList = await this.filter(videoName)
            let sortedList = await this.sort(filteredList)
            for(let item of sortedList){
                let button = new SourceButton(item)
                this.element.appendChild(
                    button.element
                )
            }
        }

        //搜索后对列表进行过滤
        async filter(name){
            let reslist = []
            for (let item of this.sources) {

                let playList = await search(item.searchUrl,name)
                if(playList==0)continue
                reslist.push({name:item.name,playList:playList})
            }
            return reslist
        }

        //对列表添加速度
        async sort(sources){
            let sortedSource = []
            for(let item of sources){
                let tsList = await downloadM3u8(item.playList[0].url)
                let speed = 0
                if (tsList.length == 0) {
                    log_machine(`没有找到下载链接,请检查`)
                } else {
                    speed = await testSpeed(tsList)
                }
                sortedSource.push({...item,"speed":speed})
            }
            sortedSource.sort((a, b) => {
                return b.speed - a.speed;//从大到小排序
            })
            log_machine("排序完成...")
            log_machine(sortedSource)

            return sortedSource
        }

    }


    //剧集选择器
    class seriesButton {
        constructor(name, url,index) {
            this.element = htmlToElement(`<a class="liu-sourceButton">${name}</a>`)
            this.element.onclick = () => {
                seriesNum = index
                art.url = url
                document.querySelector(".show-series").innerText= `正在播放第${index+1}集`
            }
        }
    }

    //选择器的container
    class seriesContainer{
        constructor(playList){
            this.element = htmlToElement(`<div class="video-selector"></div>`)
            this.playList = playList
        }
        init(){
            for(let [index,item] of this.playList.entries()){
                let button = new seriesButton(item.name,item.url,index)
                this.element.appendChild(button.element)
            }
            document.querySelector(".liu-playContainer").appendChild(this.element)
        }
    }

    class UI {
        constructor(playList) {
            this.element = htmlToElement(`
                <div class="liu-playContainer">
                    <a class="liu-closePlayer">关闭界面</a>
                    <div class="sourceButtonList"></div>
                    <div class="artplayer-app" style="width:100%;height:500px;"></div>
                    <div class="show-series"></div>
                    <div class="prompt">部分影片选集后会出现卡顿,拖动一下进度条即可恢复。</div>
                    <div>最上方为影视来源列表,自动测速,最快的资源排在第一个</div>
                </div>
            `)
            this.playList = playList
        }
        async init() {
            document.body.appendChild(this.element)
            let button = document.querySelector(".liu-closePlayer")
            //
            button.onclick = () => {
                this.element.remove()
            }
            //第n集开始播放
            log_machine(this.playList[seriesNum].url)
            initArt(this.playList[seriesNum].url)
            let series_container = new seriesContainer(this.playList)
            series_container.init()
            let sources_container = new SourceListContainer(searchSource)
            sources_container.renderList()

        }
    }








    function initArt(url){
        art = new Artplayer({
            container: ".artplayer-app",
            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;
                    }
                },
            },
        });
    }



    //获取豆瓣影片名称
    //v2:新的获取方式,看起来简洁多了
    function getVideoNamev2() {
        videoName = document.title.slice(0, -5)
        return videoName
    }

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

                },
                onerror: function (error) {
                    resolve(0)
                }
            });
        });
    }

    //处理搜索到的结果:从返回结果中找到对应片子
    function handleResponse(r) {
        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 })
                }
            });
        });
    }


    //将源根据速度进行排序
    async function sortSource() {
        log_machine("进入排序...")
        let sortedSource = []
        let videoName = getVideoNamev2()
        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
    }


    async function main() {
        appendStyle(css) //添加css
        let playbuttonv3 = new playButtonv3()
        playbuttonv3.mount()
    }


    main()


})()