您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download subtitle guides from Hulu.jp in VTT format
// ==UserScript== // @name Hulu.jp Subtitle Downloader // @namespace https://hulu.jp // @version 2.0.0 // @description Download subtitle guides from Hulu.jp in VTT format // @author Ronny // @match https://*.hulu.jp/watch/* // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @license MIT // ==/UserScript== (function () { ("use strict"); // Functions const createButton = (text, marginInlineStart = "0") => { const btn = document.createElement("button"); btn.innerHTML = text; btn.style.border = "none"; btn.style.borderRadius = "4px"; btn.style.backgroundColor = "#889188"; btn.style.color = "#fefffe"; btn.style.padding = "1em 1.5em"; btn.style.lineHeight = "1"; btn.style.fontSize = "0.8em"; btn.style.marginInlineStart = marginInlineStart; btn.addEventListener("mouseover", () => (btn.style.opacity = "0.8")); btn.addEventListener("mouseout", () => (btn.style.opacity = "1")); return btn; }; const appendButton = (button, parent) => { if (!parent.contains(button)) { parent.appendChild(button); } }; const button = createButton("字幕ガイドをダウンロード"); const batchButton = createButton( "最終話までの字幕ガイドをダウンロード", "1em" ); let subSrc = ""; const downloadSubtitle = (subSrc, fileName) => { if (!subSrc) { console.error("No subtitle track found."); return; } fetch(subSrc) .then((response) => response.text()) .then((data) => { const blob = new Blob([data], { type: "text/vtt" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); }) .catch((error) => { console.error("Failed to download subtitle:", error); }); }; button.onclick = () => { const pageTitle = document.title; const fileName = pageTitle.split(" | ")[0].trim() + ".vtt"; downloadSubtitle(subSrc, fileName); }; const startBatchDownload = () => { if ( window.confirm( `下载过程中请在播放器上滑动鼠标以激活下一集按钮。 While downloading, please hover over the player to activate the next episode button. ダウンロード中は、プレーヤーにマウスを重ねて次のエピソードボタンをアクティブにしてください。` ) ) { const subSrcList = []; // For the current episode pushSubtitleSrc(subSrcList, subSrc); // Fetch next episode fetchNextEpisode(subSrcList); } }; batchButton.onclick = startBatchDownload; const fetchNextEpisode = (subSrcList) => { const nextButton = document.querySelector('[aria-label="次の動画"]'); if (nextButton && !nextButton.classList.contains("disabled")) { setTimeout(() => { pushSubtitleSrc(subSrcList, subSrc); fetchNextEpisode(subSrcList); }, 5000); nextButton.click(); } else { setTimeout(() => { downloadAllSubtitles(subSrcList); }, 5000); } }; const pushSubtitleSrc = (subSrcList, subSrc) => { if (subSrc) { const pageTitle = document.title; const fileName = pageTitle.split(" | ")[0].trim() + ".vtt"; subSrcList.push({ src: subSrc, name: fileName }); console.log("Added subtitle track for", fileName); } }; const downloadAllSubtitles = (subSrcList) => { if (subSrcList.length === 0) { console.error("No subtitle tracks found."); return; } const zip = new JSZip(); let count = 0; subSrcList.forEach(({ src, name }) => { fetch(src) .then((response) => response.text()) .then((data) => { zip.file(`${name}`, data); count++; if (count === subSrcList.length) { zip.generateAsync({ type: "blob" }).then((content) => { const pageTitle = document.title; const batchName = pageTitle.split("第")[0].trim(); const url = URL.createObjectURL(content); const a = document.createElement("a"); a.href = url; a.download = `${batchName}.zip`; a.click(); URL.revokeObjectURL(url); }); } }) .catch((error) => { console.error("Failed to download subtitle:", error); }); }); }; // Override XMLHttpRequest open method const handleXhrLoad = (response) => { try { const targetTrack = response.tracks.find( (track) => track.label === "字幕ガイド" ); if (targetTrack) { const src = targetTrack.src; console.log("Found subtitle track, src:", src); subSrc = src; // Create buttons const titlePanel = document.getElementsByClassName("watch-info-title")[0]; appendButton(button, titlePanel); appendButton(batchButton, titlePanel); } else { console.log("No subtitle track found."); } } catch (e) { console.error("Failed to parse JSON response:", e); } }; const originalXhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this.addEventListener("load", function () { if (url.startsWith("https://playback.prod.hjholdings.tv/session/open")) { handleXhrLoad(this.response); } }); originalXhrOpen.apply(this, [method, url, ...rest]); }; })();