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