Greasy Fork is available in English.

爱奇艺字幕下载

下载爱奇艺视频的外挂字幕

// Copyright 2022 shadows
//
// Distributed under MIT license.
// See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
// ==UserScript==
// @name         爱奇艺字幕下载
// @namespace    http://tampermonkey.net/shadows
// @version      0.3.4
// @description  下载爱奇艺视频的外挂字幕
// @author       shadows
// @license      MIT License
// @copyright    Copyright (c) 2021 shadows
// @match        https://www.iq.com/play/*
// @include      /^https:\/\/www\.iqiyi\.com\/v_(\w+)\.html.*$/
// @icon         https://www.iqiyipic.com/common/images/logo.ico
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlhttpRequest
// @grant        GM_getValue
// @grant        GM.getValue
// @grant        GM_setValue
// @grant        GM.setValue
// @grant        GM_deleteValue
// @grant        GM.deleteValue
// @grant        GM_addValueChangeListener
// @grant        GM_registerMenuCommand
// @grant        GM.registerMenuCommand
// @require      https://greasyfork.org/scripts/371339-gm-webextpref/code/GM_webextPref.js?version=961539
// ==/UserScript==
/* jshint esversion: 6 */
'use strict';
const pref = GM_webextPref({
  default: {
    iqiyi_filetypes: ["ass"],
    iq_filetypes: ["srt"],
  },
  body: [
    {
      key: "iqiyi_filetypes",
      label: "(大陆站)下载的文件格式:(按住crtl多选)",
      type: "select",
      multiple: true,
      options: {
        xml: "xml",
        ass: "ass(脚本根据xml生成)",
      }
    },
    {
      key: "iq_filetypes",
      label: "(国际站)下载的文件格式:(按住crtl多选)",
      type: "select",
      multiple: true,
      options: {
        srt: "srt",
        webvtt: "webvtt",
        xml: "xml",
        ass: "ass(脚本根据xml生成)",
      }
    },
  ],
});
pref.ready();
const extensionDict = {
    srt: "srt",
    webvtt: "vtt",
    xml: "xml",
    ass: "ass",
};

async function main() {
    const url = new URL(window.location.href);
    const site = websiteRules[url.host];
    if (sessionStorage.getItem("download_subtitles") == "true") {
        if (await site.hasSubtitles()) {
            console.log("自动下载字幕");
            await site.downloadSubtitles(false);
            console.log("自动下载字幕已完成");
        }
        await site.gotoNext();
    } else {
        if (await site.hasSubtitles()) {
            await site.addDownloadButton();
        }
    }
}

window.addEventListener('load', async function () {
    console.log("load");
    await main();
});
window.addEventListener('popstate', async function () {
    console.log("popstate");
    await main();
});

var _wr = function(type) {
   var orig = history[type];
   return function() {
       var rv = orig.apply(this, arguments);
       var e = new Event(type);
       e.arguments = arguments;
       window.dispatchEvent(e);
       return rv;
   };
};
history.pushState = _wr('pushState');
history.replaceState = _wr('replaceState');
window.addEventListener('pushState', async function () {
    console.log("pushState");
    await main();
});
window.addEventListener('replaceState', async function () {
    console.log("replaceState");
    await main();
});

const websiteRules = {};

websiteRules["www.iqiyi.com"] = {
    hasSubtitles: async function () {
        // check has video pleyer
        console.log("check has video player");
        if (document.querySelector("[data-player-hook]") == null) return false;
        for (let i = 0; i < 250; ++i) {
            await sleep(500);
            if (playerObject._player.package.engine == undefined) continue;
            this.playerData = playerObject._player.package.engine;
            if (this.playerData?.movieinfo?.current == undefined) continue;
            // check has subtitles
            if (this.playerData.movieinfo.current?.subtitlesUrlMap == undefined) break;
            if (document.querySelectorAll("[data-player-hook=subtitles_language_list] .iqp-set-zimu").length == 0) continue;
            this.subtitlesUrlMap = this.playerData.movieinfo.current.subtitlesUrlMap;
            return true;
        }
        console.log("check has video player:Failed!");
        return false;
    },
    addDownloadButton: async function () {
        if (document.querySelector(".download-sub")!==null) return;
        console.log("addDownloadButton");
        let parentElement = document.createElement("div");
        let downloadButton = creatButton("下载当前字幕");
        downloadButton.id = "download-subtitles";
        parentElement.append(downloadButton);
        document.addEventListener('click', async(event) => {
            if (event.target.id == "download-subtitles") {
                event.stopPropagation();
                await this.downloadSubtitles(true);
                return;
            }
        }, true);

        if (Object.keys(this.subtitlesUrlMap).length > 1) {
            let downloadAllButton = creatButton("下载所有字幕");
            downloadAllButton.id = "download-all-subtitles";
            parentElement.append(downloadAllButton);
            document.addEventListener('click', async(event) => {
                if (event.target.id == "download-all-subtitles") {
                    event.stopPropagation();
                    await this.downloadSubtitles(false);
                    return;
                }
            }, true);
        }

/*         let downloadListAllButton = creatButton("下载所有视频的字幕");
        downloadListAllButton.id = "download-list-all-subtitles"
        document.addEventListener('click', async(event) => {
            if (event.target.id == "download-list-all-subtitles") {
                event.stopPropagation();
                await this.downloadSubtitles(false);
                sessionStorage.setItem("download_subtitles","true")
                await this.gotoNext();
            }
        }, true);
        parentElement.append(downloadListAllButton);*/

        parentElement.append(createSettingButton());
        document.querySelector("div[data-block-v2='80521_function']").after(parentElement);

    },
    getSubtitles: function (filetypes, onlySeleted = true) {
        console.log("getSubtitles");
        let subtitles = [];
        const videoTitle = Array.from(document.querySelectorAll(".iqp-top-item.iqp-top-title iqpspan")).reduce((title, current) => title + current.textContent.replace(/^\s*/, ''), "");

        const languages = document.querySelectorAll("[data-player-hook=subtitles_language_list] .iqp-set-zimu");
        for (let i = 0; i < languages.length; ++i) {
            if (onlySeleted && !languages[i].classList.contains("selected")) continue;
            for (let filetype of filetypes) {
                let name = `${videoTitle}_${languages[i].textContent}.${extensionDict[filetype]}`;
                let url = "https:" + this.subtitlesUrlMap[i+1];
                subtitles.push({ name, url, filetype: filetype });
            }
            if (onlySeleted) break;
        }
        return subtitles;
    },
    downloadSubtitles: async function (onlySeleted = true) {
        const filetypes = pref.get("iqiyi_filetypes");
        const subtitles = this.getSubtitles(filetypes, onlySeleted);
        for (let item of subtitles) {
            if (item.filetype !== "ass") {
                await download(item.url, item.name);
            } else {
                let xmlString = await xhr({
                    method: "GET",
                    url: item.url,
                }).then(resp => resp.responseText)
                let content = xml2ass(xmlString,"test");
                saveBlob(content,item.name);
            }
        }
    },
    gotoNext: async function () {
        const nextEpisode = document.querySelector(".qy-episode-txt li.selected+li a");
        if (nextEpisode) {
            nextEpisode.click();
        } else {
            const nextTab = document.querySelector(".tab-cont .selected+div+div>.bar-link");
            if (nextTab){
                nextTab.click();
                await sleep(500);
                document.querySelector(".qy-episode-txt li a").click()
            } else {
                // clear the download_subtitles option if no more video
                sessionStorage.removeItem("download_subtitles");
            }
        }
    },
};

websiteRules["www.iq.com"] = {
    hasSubtitles: async function () {
        for (let i = 0; i < 100; ++i) {
            await sleep(200);
            if (playerObject?._player.package.engine == undefined) continue;
            this.playerData = playerObject._player.package.engine;
            if (this.playerData?.movieinfo.tvid == undefined) continue;
            this.tvid = this.playerData.movieinfo.tvid;
            if (this.playerData.episode.EpisodeStore[this.tvid].movieInfo?.originalData.data == undefined) continue;
            this.data = this.playerData.episode.EpisodeStore[this.tvid].movieInfo.originalData.data;
            // check has subtitles
            if (this.data.program?.stl == undefined) continue;
            if (this.data.program.stl?.length == 0) continue;
            this.stl = this.data.program.stl;
            if (document.querySelector(".left-section")==null) continue;
            return true;
        }
        return false;
    },
    addDownloadButton: async function () {
        if(document.querySelector(".download-sub")!==null) return;
        console.log("addDownloadButton");
        for (let i = 0; i < 100; ++i) {
            if (document.querySelector(".left-section")==null) { await sleep(50);continue; } else { break }
        }
        let parentElement = document.querySelector(".left-section");
        let downloadButton = creatButton("下载当前字幕");
        downloadButton.addEventListener("click", () => {
            this.downloadSubtitles(true);
        });
        parentElement.append(downloadButton);

        if (this.stl.length > 1) {
            let downloadAllButton = creatButton("下载所有字幕");
            downloadAllButton.addEventListener("click", () => { this.downloadSubtitles(false); });
            parentElement.append(downloadAllButton);
        }
        let downloadListAllButton = creatButton("下载所有视频的字幕");
        downloadListAllButton.addEventListener("click", async () => {
            await this.downloadSubtitles(false);
            sessionStorage.setItem("download_subtitles","true")
            await this.gotoNext();
        });
        parentElement.append(downloadListAllButton);

        parentElement.append(createSettingButton());
    },
    getSubtitles: function (filetypes,onlySeleted = true) {
        const prefix = this.data.dstl;
        let subtitles = [];
        const videoTitle = document.querySelector('#pageMetaTitle').previousElementSibling.textContent;

        for (let item of this.stl) {
            if (onlySeleted && !item._selected) continue;
            for (let filetype of filetypes) {
                let name = `${videoTitle}_${item._name}.${extensionDict[filetype]}`;
                let url = (filetype == "ass") ? prefix + item.xml : prefix + item[filetype] ;
                subtitles.push({ name, url, filetype: filetype});
            }
            if (onlySeleted) break;
        }
        return subtitles;
    },
    downloadSubtitles: async function (onlySeleted = true) {
        const filetypes = pref.get("iq_filetypes");
        const subtitles = this.getSubtitles(filetypes, onlySeleted);
        for (let item of subtitles) {
            if (item.filetype !== "ass") {
                await download(item.url, item.name);
            } else {
                let xmlString = await xhr({
                    method: "GET",
                    url: item.url,
                }).then(resp => resp.responseText)
                let content = xml2ass(xmlString,"test");
                saveBlob(content,item.name);
            }
        }
    },
    gotoNext: async function () {
        const nextEpisode = document.querySelector(".intl-episodes-list li.selected~li a");
        if (nextEpisode) {
            let nextUrl = new URL(nextEpisode.href).toString();
            if (nextUrl) {
                window.location.assign(nextUrl);
            }
        } else {
            // clear the download_subtitles option if no more video
            sessionStorage.removeItem("download_subtitles");
        }
    },
};

const buttonCSS = `display: inline-block;
    background: linear-gradient(135deg, #6e8efb, #a777e3);
    color: white;
    padding: 3px 3px;
    margin: 8px 8px;
    text-align: center;
    border-radius: 3px;
    border-width: 0px;`;

function createSettingButton(){
    let settingButton = document.createElement("button");
    settingButton.innerText = "设置";
    settingButton.style.cssText = buttonCSS;
    settingButton.addEventListener("click", () => {
        pref.openDialog();
    });
    return settingButton;
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const xmlHttpRequest = (typeof(GM_xmlhttpRequest) === 'undefined') ? GM.xmlHttpRequest : GM_xmlhttpRequest;
const xhr = option => new Promise((resolve, reject) => {
    xmlHttpRequest({
        ...option,
        onerror: reject,
        onload: (response) => {
            if (response.status >= 200 && response.status < 300) {
                resolve(response);
            } else {
                reject(response);
            }
        },
    });
});

async function download(url,name) {
    await xhr({
        method: "GET",
        url: url,
        responseType: "blob"
    }).then(resp => saveBlob(resp.response,name));
}

function saveBlob(content,name) {
    const fileUrl = window.URL.createObjectURL(content);
    const anchorElement = document.createElement('a');
    anchorElement.href = fileUrl;
    anchorElement.download = name;
    anchorElement.style.display = 'none';
    document.body.appendChild(anchorElement);
    anchorElement.click();
    anchorElement.remove();
    window.URL.revokeObjectURL(fileUrl);
}

function creatButton(text) {
    let button = document.createElement("button");
    button.innerText = text;
    button.style.cssText = buttonCSS;
    button.className = "download-sub";
    return button;
}

function template(strings, ...keys) {
  return (...values) => {
    const dict = values[values.length - 1] || {};
    const result = [strings[0]];
    keys.forEach((key, i) => {
      const value = Number.isInteger(key) ? values[key] : dict[key];
      result.push(value, strings[i + 1]);
    });
    return result.join("");
  };
}
function xml2ass(xmlString, title='') {

  function encodeTime(input){
  let time = new Date(input),
      ms = time.getMilliseconds(),
      second = time.getSeconds(),
      minute = time.getMinutes(),
      hour = time.getUTCHours();

    ms = (ms/10).toFixed(0);
    if (minute<10) minute = '0'+minute;
    if (second<10) second = '0'+second;
    if (ms<10) ms = '0'+ms;
    return `${hour}:${minute}:${second}.${ms}`;
  }

  let assContent = `[Script Info]
Title: ${title}
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
PlayResX: 1920
PlayResY: 1080
YCbCr Matrix: TV.709

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Segoe UI,76,&H00FFFFFF,&H000000FF,&H00000000,&H32000000,-1,0,0,0,100,100,1.2,0,1,3.3,0.5,2,10,10,20,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
`;
  const subTemplate = template`Dialogue: 0,${0},${1},Default,,0,0,${3},,${2}`
  const nodes = new DOMParser().parseFromString(xmlString, "application/xml").documentElement.getElementsByTagName('dia');
  for (let node of nodes) {
    const startTime = node.querySelector("st").textContent;
    const endTime = node.querySelector("et").textContent;
    const text = node.querySelector("sub").textContent;
    let margin_v = node.querySelector("position")?.getAttribute("vertical-margin");
    if (isNaN(parseFloat(margin_v))) {
        margin_v = 0;
    } else {
        margin_v = ( 1 - parseFloat(margin_v) / 100)*1080;
    }
    let line = subTemplate(encodeTime(parseInt(startTime)),encodeTime(parseInt(endTime)),text.replace('\n','\\n'),Math.floor(margin_v));
    assContent = assContent + line + '\n';
  }
  const blob = new Blob([assContent], { type: "text/plain;charset=utf-8" , endings: 'native'});
  return blob;
}