Greasy Fork

Play-With-MPV

通过 MPV 播放网页上的视频

目前为 2022-09-05 提交的版本。查看 最新版本

// ==UserScript==
// @name                    Play-With-MPV
// @name:zh                 使用 MPV 播放
// @description             通过 MPV 播放网页上的视频
// @namespace               https://github.com/LuckyPuppy514
// @version                 1.5.2
// @commit                  v1.2.1 新增 powershell 脚本升级提醒功能
// @commit                  v1.2.2 修复 youtube 标题带 | 导致错误脚本升级提醒
// @commit                  v1.2.3 修改 imomoe 域名
// @commit                  v1.3.0 新增域名:www.6dm.cc, www.dmla.cc(第一线路:大部分支持,其他线路:小部分支持)
// @commit                  v1.3.0 新增域名:www.dm233.me(线路III:大部分支持,其他线路:大部分不支持)
// @commit                  v1.3.0 代码重构,使用继承方便后续添加网站支持
// @commit                  v1.4.0 b站bug修复:标题带数字,解析出错,修复并优化了获取视频链接的速度
// @commit                  v1.4.0 新增对plex支持(本地:*://*/web/index.html*,远程:https://app.plex.tv/desktop/*)
// @commit                  v1.4.1 修复b站番剧播放目录为列表时,无法获取正确集数的bug
// @commit                  v1.4.2 修复b站番剧播放的bug
// @commit                  v1.4.3 修改cdn为unpkg,某些网络无法访问cdn,导致js加载失败(有问题,请自行修改:unpkg.com => cdn.jsdelivr.net/npm)
// @commit                  v1.4.4 www.dmla.cc 域名变更为:www.dmlaa.com
// @commit                  v1.4.5 ddrk.me 域名变更为:ddys.tv
// @commit                  v1.5.0 代码优化,去除 powershell 脚本,只需添加注册表信息即可
// @commit                  v1.5.1 B站添加 cid 参数,配合 https://github.com/itKelis/MPV-Play-BiliBili-Comments 可实现弹幕功能
// @commit                  v1.5.2 注册表代码升级,支持中文标题
// @homepage                https://github.com/LuckyPuppy514/Play-With-MPV
// @author                  LuckyPuppy514
// @copyright               2022, Grant LuckyPuppy514 (https://github.com/LuckyPuppy514)
// @license                 MIT
// @icon                    https://cdn.jsdelivr.net/gh/LuckyPuppy514/pic-bed/common/mpv.png
// @match                   *://www.youtube.com/*
// @include                 https://www.youtube.com/watch/*
// @include                 https://www.bilibili.com/bangumi/play/*
// @include                 https://www.bilibili.com/video/*
// @connect                 api.bilibili.com
// @include                 https://ddys.tv/*
// @include                 https://www.6dm.cc/play/*
// @include                 http://www.dmlaa.com/play/*
// @include                 https://danmu.yhdmjx.com/*
// @include                 https://www.dm233.me/play/*
// @include                 http://www.dmh8.com/player/*
// @include                 https://www.yhdmp.net/vp/*
// @match                   *://*/web/index.html*
// @include                 https://app.plex.tv/desktop/*
// @run-at                  document-end
// @require                 https://unpkg.com/[email protected]/base64.js
// @require                 https://unpkg.com/[email protected]/dist/jquery.min.js
// @grant                   GM_setValue
// @grant                   GM_getValue
// ==/UserScript==

'use strict';

// playwithmpv.reg version
const REGEDIT_VERSION = "v1.5.2";

// using for dev
function debug(data) {
    // console.log(data);
    // alert(data);
}

// Play With MPV CSS
const PWM_CSS = `
#play-with-mpv-button {
  width: 50px;
  height: 50px;
  border: 0px;
  border-radius: 50%;
  background-size: 50px;
  overflow: hidden;
  background-size: cover;
  background-image: url(https://cdn.jsdelivr.net/gh/LuckyPuppy514/pic-bed/common/mpv.png);
  background-repeat: no-repeat;
  cursor: pointer;
  z-index: 999
}

#play-with-mpv-div {
  position: fixed;
  left: 15px;
  bottom: 15px;
}
`;

const PWM_DIV_ID = "play-with-mpv-div";
const PWM_BUTTON_ID = "play-with-mpv-button";

const STYLE_VISIABLE = "display: block";
const STYLE_INVISIABLE = "display: none";

// add play with mpv div
function addPlayWithMPVDiv() {
    let pwmCss = document.createElement("style");
    pwmCss.innerHTML = PWM_CSS.trim();
    document.head.appendChild(pwmCss);

    let pwmButton = document.createElement("button");
    pwmButton.id = PWM_BUTTON_ID;
    pwmButton.style = STYLE_INVISIABLE;
    // add event listener
    pwmButton.onclick = function () {
        debug("pwm button click");
        handler.playCurrentVideoWithMPV();
        handler.pauseCurrentVideo();
    }

    let pwmDiv = document.createElement("div");
    pwmDiv.id = PWM_DIV_ID;
    pwmDiv.appendChild(pwmButton);
    document.body.appendChild(pwmDiv);
    debug("add div success");
}

function setVisiable() {
    debug("set visiable: " + currentVideoUrl);
    if (checkVideoUrl(currentVideoUrl)) {
        document.getElementById(PWM_BUTTON_ID).style = STYLE_VISIABLE;
    }
}
function setInvisiable() {
    document.getElementById(PWM_BUTTON_ID).style = STYLE_INVISIABLE;
}

// support domain
const YOUTUBE = "www.youtube.com";
const BILIBILI = "www.bilibili.com";
const DDRK = "ddys.tv";
const DM6CC = "www.6dm.cc";
const DMLACC = "www.dmlaa.com";
const YHDMJX = "danmu.yhdmjx.com";
const DM233 = "www.dm233.me";
const DMH8 = "www.dmh8.com";
const YHDMP = "www.yhdmp.net";
const PLEX_LOCAL = "/web/index.html";
const PLEX = "app.plex.tv";

// api
const BILIBILI_API = 'https://api.bilibili.com'

// playwithmpv protocol
const PWM_PROTOCOL = "PlayWithMPV://";

// video url need play
var currentVideoUrl;
// currentPage info
var currentUrl;
var currentDomain;

var handler;
var ddrkPlayStatus;
var bilibiliCid;

class Handler {
    getProtocolLink() {
        // 添加注册表提示
        let version = GM_getValue("REGEDIT_VERSION");
        if (!version || version != REGEDIT_VERSION) {
            let message =
                  "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥\n"
                + "🔥 \n"
                + "🔥 请自行添加最新版注册表信息,否则可能无法正常运行\n"
                + "🔥 \n"
                + "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥\n"
                + "🔥 \n"
                + "🔥 确定:已添加,不再提示此消息\n"
                + "🔥 \n"
                + "🔥 取消:查看如何添加最新版注册表信息\n"
                + "🔥 \n"
                + "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥";
            var result = confirm(message);
            if (result == true) {
                GM_setValue("REGEDIT_VERSION", REGEDIT_VERSION);
            } else {
                window.open("https://github.com/LuckyPuppy514/Play-With-MPV#4-%E6%B7%BB%E5%8A%A0%E6%B3%A8%E5%86%8C%E8%A1%A8%E4%BF%A1%E6%81%AF");
                return;
            }
        }

        let protocolLink = PWM_PROTOCOL
            + '"' + currentVideoUrl + '" '
            + '--force-media-title="' + document.title + '" ';
        if (currentDomain.indexOf("bilibili") != -1) {
            protocolLink = protocolLink + '--http-header-fields=referer:"' + currentUrl + ',user-agent:' + navigator.userAgent + '" --script-opts="cid=' + bilibiliCid + '" ';
        }
        debug(protocolLink);
        return protocolLink;
    }

    getCurrentVideoUrl() { }
    playCurrentVideoWithMPV() { }
    pauseCurrentVideo() { }

    addPlayWithMPVButton() {
        addPlayWithMPVDiv();
    }
    addTimer() {
        // first try to get video url (wait page load)
        setTimeout(refreshCurrentVideoUrl, 600);
        // try to refresh video url (avoid get video url fail)
        setInterval(refreshCurrentVideoUrl, 3000);
        // page change listener
        setInterval(pageChangeListener, 1000);

        function refreshCurrentVideoUrl() {
            debug("refresh current video url: " + currentVideoUrl);
            debug("current url: " + currentUrl);
            if (!checkVideoUrl(currentVideoUrl)) {
                handler.getCurrentVideoUrl();
            }
        }
        function pageChangeListener() {
            let newCurrentUrl = window.location.href;
            if (currentUrl != newCurrentUrl) {
                setInvisiable();
                init();
            }
        }
    }
}

class YoutubeHandler extends Handler {
    getCurrentVideoUrl() {
        currentVideoUrl = currentUrl;
        setVisiable();
    }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    pauseCurrentVideo() {
        document.getElementsByTagName("video")[0].pause();
    }
}

class BilibiliHandler extends Handler {
    getCurrentVideoUrl() {
        // video
        let bvIndex = currentUrl.indexOf('/video/BV');
        if (bvIndex != -1) {
            let bvid = currentUrl.substring(bvIndex + 9, bvIndex + 19);
            debug("bvid: " + bvid);
            this.getBilibiliVideoUrlByBvid(bvid);
            return;
        }

        // bangumi
        // get bilibili video epid
        let aElement = document.getElementsByClassName('ep-item cursor visited')[0];
        if (!aElement) {
            aElement = document.getElementsByClassName('ep-item cursor')[0];
        }
        let epid = aElement.getElementsByTagName('a')[0].href;
        epid = epid.substring(epid.indexOf('/ep') + 3);
        epid = epid.substring(0, epid.indexOf('/'));
        debug('epid: ' + epid);

        let eno = document.getElementsByClassName("ep-list-progress")[0];
        if (!eno) {
            return;
        }
        eno = eno.innerHTML;
        eno = eno.substring(0, eno.indexOf('/'));
        debug('eno: ' + eno);
        this.getBilibiliVideoUrlByEpid(epid, eno);
    }

    playCurrentVideoWithMPV() {
        if (currentUrl.indexOf(BILIBILI + "/video") != -1) {
            window.open(this.getProtocolLink(), "_blank");
        } else {
            window.open(this.getProtocolLink(), "_self");
        }
    }

    pauseCurrentVideo() {
        document.getElementsByTagName("video")[0].pause();
    }

    getBilibiliVideoUrlByBvid(bvid) {
        $.ajax({
            type: "GET",
            url: BILIBILI_API + "/x/web-interface/view?bvid=" + bvid,
            xhrFields: {
                // add cookie (CORS ignore cookie)
                withCredentials: true
            },
            success: function (res) {
                debug("get acid and cid by bvid result: ");
                debug(res);
                let avid = res.data.aid;
                let cid = res.data.cid;
                let index = currentUrl.indexOf("?p=");
                if (index != -1 && res.data.pages.length > 1) {
                    let p = currentUrl.substring(index + 3);
                    let endIndex = p.indexOf("&");
                    if (endIndex != -1) {
                        p = p.substring(0, endIndex);
                    }
                    cid = res.data.pages[p - 1].cid;
                }

                debug("avid: " + avid);
                debug("cid: " + cid);
                bilibiliCid = cid;

                let queryBilibiliVideoUrl = "/x/player/playurl?"
                    + "qn=120&otype=json&fourk=1&fnver=0&fnval=0"
                    + "&avid=" + avid
                    + "&cid=" + cid;
                $.ajax({
                    type: "GET",
                    url: BILIBILI_API + queryBilibiliVideoUrl,
                    xhrFields: {
                        // add cookie (CORS ignore cookie)
                        withCredentials: true
                    },
                    success: function (res) {
                        debug("get video url by bvid result: ");
                        debug(res);
                        currentVideoUrl = res.data.durl[0].url;
                        setVisiable();
                    }
                })
            }
        })
    }
    getBilibiliVideoUrlByEpid(epid, eno) {
        if (!epid || !eno) {
            return;
        }

        $.ajax({
            type: "GET",
            url: BILIBILI_API + "/pgc/view/web/season?ep_id=" + epid,
            xhrFields: {
                // add cookie (CORS ignore cookie)
                withCredentials: true
            },
            success: function (res) {
                debug("get acid and cid by epid result: ");
                debug(res);
                var episodes = res.result.episodes;
                if (eno.indexOf('PV') != -1 || eno.indexOf('OP') != -1 || eno.indexOf('ED') != -1) {
                    return;
                }

                // get avid and cid
                var episode = episodes[eno - 1];
                var avid = episode.aid;
                var cid = episode.cid;
                debug("avid: " + avid);
                debug("cid: " + cid);
                bilibiliCid = cid;

                let queryBilibiliVideoUrl = "/pgc/player/web/playurl?"
                    + "qn=120&otype=json&fourk=1&fnver=0&fnval=0"
                    + "&avid=" + avid
                    + "&cid=" + cid;
                $.ajax({
                    type: "GET",
                    url: BILIBILI_API + queryBilibiliVideoUrl,
                    xhrFields: {
                        // add cookie (CORS ignore cookie)
                        withCredentials: true
                    },
                    success: function (res) {
                        debug("get video url by epid result: ");
                        debug(res);
                        currentVideoUrl = res.result.durl[0].url;
                        setVisiable();
                    }
                });
            }
        })
    }
}

class DdrkHandler extends Handler {
    getCurrentVideoUrl() {
        // click play to load video element
        if (ddrkPlayStatus == 0) {
            // alert("start play");
            var playButton = document.getElementsByClassName('vjs-big-play-button')[0];
            if (!playButton) {
                debug("ddrk get play button fail");
                return;
            }
            playButton.click();
            ddrkPlayStatus = 1;
        }

        currentVideoUrl = document.getElementById('vjsp_html5_api').src;
        setVisiable();
    }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    pauseCurrentVideo() {
        document.getElementsByTagName("video")[0].pause();
    }
}

class Dm6ccHandler extends Handler {
    constructor() {
        super();
        window.addEventListener('message', function (event) {
            currentVideoUrl = event.data;
            setVisiable();
            window.removeEventListener("message", () => { });
        }, false);
    }
    getCurrentVideoUrl() { }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    pauseCurrentVideo() {
        document.getElementsByTagName("iframe")[2].contentWindow.postMessage("pause", "https://" + YHDMJX);
    }
}

class DmlaccHandler extends Handler {
    constructor() {
        super();
        window.addEventListener('message', function (event) {
            currentVideoUrl = event.data;
            setVisiable();
            window.removeEventListener("message", () => { });
        }, false);
    }
    getCurrentVideoUrl() { }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    pauseCurrentVideo() {
        document.getElementsByTagName("iframe")[2].contentWindow.postMessage("pause", "https://" + YHDMJX);
    }
}

class YhdmjxHandler extends Handler {
    constructor() {
        super();
        // listen to pause
        window.addEventListener("message", function (event) {
            if (event.data == "pause") {
                document.getElementsByTagName('video')[0].pause();
            }
        }, false);
    }
    getCurrentVideoUrl() {
        // send current video url to parent iframe
        currentVideoUrl = document.getElementsByTagName('video')[0].src;
        if (checkVideoUrl(currentVideoUrl)) {
            window.parent.postMessage(currentVideoUrl, "*");
        }
    }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    pauseCurrentVideo() { }
}

class Dm233Handler extends Handler {
    constructor() {
        super();
        this.videoElement = null;
    }
    getCurrentVideoUrl() {
        let iframe = document.getElementById('id_main_playiframe');
        this.videoElement = iframe.contentWindow.document.getElementsByTagName("video")[0];

        let videoUrl = this.videoElement.src;
        if (videoUrl.startsWith("blob:")) {
            videoUrl = iframe.src;
            let startIndex = videoUrl.indexOf('url=http') + 4;
            let endIndex = videoUrl.indexOf('&getplay_url=');
            videoUrl = decodeURIComponent(videoUrl.substring(startIndex, endIndex));
        }

        currentVideoUrl = videoUrl;
        setVisiable();
    }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    pauseCurrentVideo() {
        this.videoElement.pause();
    }
}

class Dmh8Handler extends Handler {
    constructor() {
        super();
        window.addEventListener('message', function (event) {
            currentVideoUrl = event.data;
            setVisiable();
            window.removeEventListener("message", () => { });
        }, false);
    }
    getCurrentVideoUrl() {
        let iframe = document.getElementsByTagName('iframe')[2];
        let videoUrl = iframe.src;
        let startIndex = videoUrl.indexOf('url=http') + 4;
        let endIndex = videoUrl.indexOf('m3u8') + 4;
        currentVideoUrl = decodeURIComponent(videoUrl.substring(startIndex, endIndex));
        setVisiable();
    }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    pauseCurrentVideo() { }
}

class YhdmpHandler extends Handler {
    constructor() {
        super();
        this.videoElement = null;
    }
    getCurrentVideoUrl() {
        let iframe = document.getElementById('yh_playfram');
        this.videoElement = iframe.contentWindow.document.getElementsByTagName("video")[0];

        let videoUrl = iframe.src;
        let startIndex = videoUrl.indexOf('url=http') + 4;
        let endIndex = videoUrl.indexOf('&getplay_url=');
        currentVideoUrl = decodeURIComponent(videoUrl.substring(startIndex, endIndex));
        setVisiable();
    }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    pauseCurrentVideo() {
        this.videoElement.pause();
    }
}

class PlexHandler extends Handler {
    constructor() {
        super();
    }
    getCurrentVideoUrl() {
        let as = document.getElementsByTagName('a');
        let pwmButton = document.getElementById(PWM_BUTTON_ID);
        if (pwmButton) {
            return;
        }
        for (let a of as) {
            if (a) {
                if (a.target == "downloadFileFrame") {
                    pwmButton = document.createElement("button");
                    pwmButton.id = PWM_BUTTON_ID;
                    // add event listener
                    pwmButton.onclick = function () {
                        debug("pwm button click");
                        handler.playCurrentVideoWithMPV();
                    }
                    pwmButton.innerText = "Play With MPV";
                    pwmButton.className = a.className;
                    a.before(pwmButton);
                    currentVideoUrl = a.href;
                    break;
                }
            }
        }
    }
    playCurrentVideoWithMPV() {
        window.open(this.getProtocolLink(), "_self");
    }
    addPlayWithMPVButton() {
    }
    addTimer() {
        setInterval(this.getCurrentVideoUrl, 500);
    }
}

// check video url valid or not
function checkVideoUrl(videoUrl) {
    if (videoUrl == null || videoUrl == undefined || !videoUrl.startsWith("http")) {
        return false;
    }
    if (YOUTUBE == currentDomain && currentUrl.indexOf("/watch") == -1) {
        debug("not in youtube/watch: " + currentUrl);
        return false;
    }
    // yun.66dm.net return m3u8 as .jpg, mpv play fail
    if (videoUrl.indexOf("yun.66dm.net") != -1) {
        debug("yun.66dm.net: " + videoUrl);
        return false;
    }
    return true;
}

// init
function init() {
    debug("init ......");
    currentUrl = window.location.href;
    currentDomain = window.location.host;
    currentVideoUrl = "";
    ddrkPlayStatus = 0;

    debug("start create handler");
    switch (currentDomain) {
        case YOUTUBE:
            handler = new YoutubeHandler();
            break;
        case BILIBILI:
            handler = new BilibiliHandler();
            break;
        case DDRK:
            handler = new DdrkHandler();
            break;
        case DM6CC:
            handler = new Dm6ccHandler();
            break;
        case DMLACC:
            handler = new DmlaccHandler();
            break;
        case YHDMJX:
            handler = new YhdmjxHandler();
            break;
        case DM233:
            handler = new Dm233Handler();
            break;
        case DMH8:
            handler = new Dmh8Handler();
            break;
        case YHDMP:
            handler = new YhdmpHandler();
            break;
        default:
            if (currentUrl.indexOf(PLEX_LOCAL) != -1 || currentDomain == PLEX) {
                handler = new PlexHandler();
            }
    }

    debug("start add button");
    handler.addPlayWithMPVButton();
    debug("start add timer");
    handler.addTimer();
}


debug("Play With MPV");
init();