下载爱奇艺视频的外挂字幕
// 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?.subtitl###rlMap == undefined) break; if (document.querySelectorAll("[data-player-hook=subtitles_language_list] .iqp-set-zimu").length == 0) continue; this.subtitl###rlMap = this.playerData.movieinfo.current.subtitl###rlMap; 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.subtitl###rlMap).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.subtitl###rlMap[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 r###lt = [strings[0]]; keys.forEach((key, i) => { const value = Number.isInteger(key) ? values[key] : dict[key]; r###lt.push(value, strings[i + 1]); }); return r###lt.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; }