// ==UserScript== // @name YouTubeで自動翻訳字幕(日本語)を常にオン // @description B:再度試みる N,Enter:次の動画 P,]:前の動画 A:今の位置から再生するURLをコピー(Shift+A:タブのURLにも反映+プレイリストパラメータを維持) [:全画面化 // @version 0.5.21 // @run-at document-idle // @match *://www.youtube.com/* // @match *://www.nicovideo.jp/watch/* // @match *://live.nicovideo.jp/watch/* // @grant GM.setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @namespace https://greasyfork.org/users/181558 // @downloadURL none // ==/UserScript== (function() { const WAIT_PAGEOPEN_OFFSET = 0; // ミリ秒 不安定なら大きくする const ENABLE_EVENJAPANESE = 1; // 1なら日本語タイトルの動画でも実行 const ENABLE_EVENEMBED = 1; // 1なら埋め込み動画でも実行 const DISABLE_AUTO_GENERATED_JAPANESE = 1; // 1なら自動生成の日本語しかない時は字幕をオフ const ENABLE_WHEN_MUTED = 1; // 1なら音声がミュートされている時は字幕をオン const DO_NOT_SELECT_LANGUAGE = 0; // 1なら日本語を選択しない(字幕をオンにするだけ) const WORDS_TO_FORCE_ENABLE = /$^/; // 動画タイトルに含むと字幕をオンにする正規表現 対象なしは/$^/ const WORDS_TO_FORCE_DISABLE = /$^/; // 動画タイトルに含むと字幕をオフにする正規表現 対象なしは/$^/ const KEY_A_VARIATIONS = 2; // 1-2:aキーで作るurlの種類 1:標準のみ 2:短縮形url const NICOLIVE_EXPAND_COMMENT_LIST = 1; // 1:ニコ生でコメント欄の横幅を広げるスライダーを設置 const BUGGY_EXPRIMENTAL_FORCE_ENGLISH_WITH_DUALMARK = 0; // 1:デュアル字幕併用時に第1字幕に英語を選択(メニューがバグる) const WAIT_EACHACTION = 300; // ミリ秒 不安定なら大きくする const verbose = 0; // 開発用 const VERBOSE_NOTIFY = 0; // 開発用 1:vn() const CHECK_ROAJ = 0; // 開発用 1:日本語にしなかった理由をalert var checkROAJAlready = 0; var ds = 0 var mllID; var kaisuuA = 0; var per = (performance.now()) / 3; verb("performance.now:" + performance.now(), "wait_base:" + per + "ms"); var wait_pageOpen = (per > 1500 ? 1500 : per < 400 ? 400 : per) + WAIT_PAGEOPEN_OFFSET; vn("wait_pageOpen : " + Math.floor(wait_pageOpen) + " ms"); String.prototype.match0 = function(re) { let tmp = this.match(re); if (!tmp) { return null } else if (tmp.length > 1) { return tmp[1] } else return tmp[0] } let inYOUTUBE = location.hostname.match0(/^www\.youtube\.com|^youtu\.be/); if (location.href.indexOf("//www.nicovideo.jp/watch/") != -1) { aToCopyUrl(); return; } if (location.href.indexOf("//live.nicovideo.jp/watch/") != -1) { nicolive(); return; } document.addEventListener('keydown', function(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable || (inYOUTUBE && (e.target.closest('#chat-messages,ytd-comments-header-renderer,.ytd-comments-header-renderer') || document.activeElement.closest('#chat-messages,ytd-comments-header-renderer,.ytd-comments-header-renderer')))) return; var key = (e.shiftKey ? "Shift+" : "") + (e.altKey ? "Alt+" : "") + (e.ctrlKey ? "Ctrl+" : "") + e.key; if (e.key === "[") { // [ 全画面化 toggleFullScreen(); return false; function toggleFullScreen() { if (!document.fullscreenElement) { let p = document.documentElement.requestFullscreen(); p.catch(() => {}); } else { if (document.exitFullscreen) { let p = document.exitFullscreen(); p.catch(() => {}); } } } } if (key === "b") { toJP("forced"); e.preventDefault(); return false; } if (key === "n" || key === "Enter") { elecli("forced", '//a[@class="ytp-next-button ytp-button"]'); e.preventDefault(); //return false; } if (key === "p" || key === "]") { elecli("forced", '//a[@class="ytp-prev-button ytp-button"]'); e.preventDefault(); return false; } if ((key === "a" || key === "Shift+A") && location.href.match(/\/\/www\.youtube\.com\/watch\?.*v\=([a-zA-Z0-9_\-]{11})/)) { // a:: var ctimeEle = eleget0('//video'); if (ctimeEle) { var ctime = Math.floor(ctimeEle.currentTime); var ret = (navigator.platform.indexOf("Win") != -1) ? "\r\n" : "\n"; let newurl = key === "a" ? "https://www.youtube.com/watch?v=" + location.href.match0(/v\=([a-zA-Z0-9_\-]{11})/) + (ctime > 0 ? ("&t=" + ctime) : "") : location.href.replace(/&t=\d*s?/, "").replace(/[\?|\&]index=\d+/, "") + (ctime > 0 ? ("&t=" + ctime) : ""); let transratedTitle = (eleget0('//h1[@class=\"title style-scope ytd-video-primary-info-renderer\"]/yt-formatted-string/font/font|//div[@id=\"title\" and contains(@class,\"style-scope ytd-watch-metadata\")]/h1/yt-formatted-string/font/font')?.innerText?.replace(/(?!= - YouTube)$/, " - YouTube")) if (key === "Shift+A") history.pushState(null, null, newurl); if (kaisuuA == 1 && key != "Shift+A") { newurl = newurl.replace('https://www.youtube.com/watch?v=', 'https://youtu.be/').replace("&t=", "?t="); } else { newurl = newurl.replace('https://youtu.be/', 'https://www.youtube.com/watch?v=').replace("?t=", "&t="); } var cb = ((transratedTitle || document.title) + ret + newurl + ret); GM.setClipboard(cb); popup(cb) kaisuuA = ++kaisuuA % KEY_A_VARIATIONS; } e.preventDefault(); return false; } }, false); var latestVideo = elegeta('//video').map(e => e.src).toString(); setInterval(() => { if (elegeta('//video').map(e => e.src).toString() != latestVideo) { // video要素の.srcたちの変化を監視 latestVideo = elegeta('//video').map(e => e.src).toString(); checkROAJAlready = 0; if (VERBOSE_NOTIFY) vn(latestVideo, "//video変化 : " + elegeta('//video').length + "要素 "); // run(wait_pageOpen * 3); if (latestVideo.length > 3) run(wait_pageOpen * 3); // src属性の中身があるvideo要素が1つ以上あれば } }, 1000); if (window === parent && location.href.match(/^https:\/\/www\.youtube\.com\/watch/)) run(wait_pageOpen * 2); for (let ele of elegeta('//div[@id="player"]/div/div/button[@aria-label="再生"]|//div[@id="player"]/div/div/button[@aria-label="Play"]')) { ele.addEventListener('click', () => { setTimeout(run, wait_pageOpen * 2) }); } return; function run(delay = 500) { verb(elegeta('//video[@class="video-stream html5-main-video"]')) elegeta('//video').forEach(e => { if (!(e.src)) e.remove() }) let video = elegeta('//video[@class="video-stream html5-main-video"]').slice(-1)[0]; // video要素が複数ある場合があるので最後の1つを取得 if (!video) { verb("まだビデオ要素がありません"); return; } if (!video.src) { verb("まだビデオ再生が始まっていません"); setTimeout(() => run(delay), 100); return; } setTimeout(function() { elegeta('//video').forEach(e => { if (!(e.src)) e.remove() }) verb("run"); var title = eleget0('//h1/yt-formatted-string'); if (!(location.href.match(/\/embed/)) && (!title || !eleget0('//div[contains(@class,"ytp-right-controls")]/button[@aria-haspopup="true"]|//button[@class="ytp-button ytp-settings-button" and @data-tooltip-target-id="ytp-settings-button"]'))) { setTimeout(run, 100); verb("title does not exist"); return; } if (!ENABLE_EVENJAPANESE && title && ((title.innerText) || "").match(/[\u30a0-\u30ff\u3040-\u309f\u3005-\u3006\u30e0-\u9fcf]+/)) { verb("タイトルに日本語を含むのでやめます"); return; } // タイトルに日本語あるならやめる setTimeout(() => { toJP(), wait_pageOpen }); }, delay + ((window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1) ? 200 : 0)); } function toJP(f = null) { if (!(location.href.match(ENABLE_EVENEMBED ? /:\/\/www\.youtube\.com\/watch\?|:\/\/www\.youtube\.com\/embed/ : /:\/\/www\.youtube\.com\/watch\?/))) return; if (eleget0('//button[@class="ytp-subtitles-button ytp-button" and @aria-pressed="true"]|//button[@class="ytp-subtitles-button ytp-button" and @aria-pressed="false"]')) { // 押されている字幕ボタンがある? elecli(f, '//button[@class="ytp-subtitles-button ytp-button" and @aria-label="字幕(c)" and @aria-pressed="false"]|//button[@class="ytp-subtitles-button ytp-button" and @aria-pressed="false"]', 1); // 押されていない字幕ボタンがあるなら押す if (eleget0('#dualMarkStyle') && BUGGY_EXPRIMENTAL_FORCE_ENGLISH_WITH_DUALMARK) { elecli(f, '//div[@class="ytp-right-controls"]/button[@aria-label="設定"]|//button[@class="ytp-button ytp-settings-button"]', 2, "close") elecli(f, '//div[contains(@class,"ytp-menuitem-label")]/div/span[text()="字幕"]', 3, "close") elecli(f, '//div[contains(@class,"ytp-menuitem-label") and contains(text(),"英語")]', 4, "close") elecli(f, '//div[contains(@class,"ytp-right-controls")]/button[@aria-haspopup="true" and @aria-expanded="true"]|//button[@class="ytp-button ytp-settings-button" and @data-tooltip-target-id="ytp-settings-button" and @aria-expanded="true"]', 6, "blur"); } else if (!DO_NOT_SELECT_LANGUAGE) { verb("字幕を選択します") elecli(f, '//div[@class="ytp-right-controls"]/button[@aria-label="設定"]|//button[@class="ytp-button ytp-settings-button"]', 2); // 設定ボタン elecli(f, '//div[@class="ytp-menuitem-label"]/div/span[text()="字幕"]|//div[@class="ytp-menuitem-label"]/div/span[contains(text(),"Subtitles/CC")]|//font[@style="vertical-align: inherit;" and text()="字幕"]', 3, "close", "smooth"); elecli(f, '//div[@class="ytp-menuitem-label" and text()="自動翻訳"]|//div[2]/div/div[@class="ytp-menuitem-label" and contains(text(),"Auto-translate")]', 4, "close", "instant"); elecli(f, '//div[@class="ytp-menuitem-label" and text()="日本語"]|//div[@class="ytp-menuitem-label" and text()="Japanese"]', 5, "close abortDS"); elecli(f, '//div[contains(@class,"ytp-right-controls")]/button[@aria-haspopup="true" and @aria-expanded="true"]|//button[@class="ytp-button ytp-settings-button" and @data-tooltip-target-id="ytp-settings-button" and @aria-expanded="true"]', 7, "blur"); } else { verb(`字幕を選択しません`) } } } function elecli(f, xpath, delay = 0, command = "", beha = null) { ds = ds || eleget0('//div[text()="デフォルトの字幕"]') setTimeout(() => { ds = ds || eleget0('//div[text()="デフォルトの字幕"]') verb(`#${delay} : `) var title = eleget0('//h1/yt-formatted-string'); if (!f && (((title.innerText) || "").match(WORDS_TO_FORCE_DISABLE))) { elecli(1, '//button[@class="ytp-subtitles-button ytp-button" and @aria-pressed="true"]'); return } let muted = eleget0('//button[@class="ytp-mute-button ytp-button" and @title="ミュート解除(m)"]'); verb(`対象:${xpath}`, `強制クリックモード:${!f}`, `ENABLE_WHEN_MUTED=1 & ミュートされている:${(ENABLE_WHEN_MUTED && muted)}`, `DISABLE_AUTO_GENERATED_JAPANESE:${DISABLE_AUTO_GENERATED_JAPANESE}`, `強制オンタイトルにヒット:${!(((title.innerText) || "").match(WORDS_TO_FORCE_ENABLE) == null)}`) if ((!f && !(ENABLE_WHEN_MUTED && muted) && DISABLE_AUTO_GENERATED_JAPANESE && (((title.innerText) || "").match(WORDS_TO_FORCE_ENABLE) == null))) { // 自動翻訳の日本語しかなければ字幕をオフにして終わる var str = ""; for (let am of elegeta('//div[@class="ytp-menuitem-label"]')) { str += am.innerText }; verb(`//div[@class="ytp-menuitem-label"] : ${str||"NA"}`); if (['アノテーション再生速度字幕 (1)画質オフ日本語 (自動生成)自動翻訳', '自動再生アノテーション再生速度字幕 (1)画質オフ日本語 (自動生成)自動翻訳', 'オフ日本語 (自動生成)自動翻訳', '自動再生アノテーション再生速度字幕 (1)画質字幕を追加オフ日本語 (自動生成)自動翻訳', '自動再生再生速度字幕 (1)画質字幕を追加オフ日本語 (自動生成)自動翻訳', `オフ日本語 (自動生成)自動翻訳アイスランド語アイマラ語アイルランド語アカン語アゼルバイジャン語アッサム語アフリカーンス語アムハラ語アラビア語アルバニア語アルメニア語イタリア語イディッシュ語イボ語インドネシア語ウイグル語ウェールズ語ウクライナ語ウズベク語ウルドゥー語エウェ語エストニア語エスペラント語オディア語オランダ語オロモ語カザフ語カタロニア語ガリシア語ガンダ語カンナダ語キニアルワンダ語ギリシャ語キルギス語グアラニー語グジャラート語クメール語クリオ語クルド語クロアチア語ケチュア語コサ語コルシカ語サモア語サンスクリット語ジャワ語ジョージア語ショナ語シンド語シンハラ語スウェーデン語ズールー語スコットランド・ゲール語スペイン語スロバキア語スロベニア語スワヒリ語スンダ語セブアノ語セルビア語ソマリ語タイ語タジク語タタール語タミル語チェコ語ツォンガ語ティグリニア語ディベヒ語テルグ語デンマーク語ドイツ語トルクメン語トルコ語ニャンジャ語ネパール語ノルウェー語ハイチ・クレオール語ハウサ語パシュトゥー語バスク語ハワイ語ハンガリー語パンジャブ語ヒンディー語フィリピノ語フィンランド語フモン語フランス語ブルガリア語ベトナム語ヘブライ語ベラルーシ語ペルシア語ベンガル語ボージュプリー語ポーランド語ボスニア語ポルトガル語マオリ語マケドニア語マダガスカル語マラーティー語マラヤーラム語マルタ語マレー語ミャンマー語モンゴル語ヨルバ語ラオ語ラテン語ラトビア語リトアニア語リンガラ語ルーマニア語ルクセンブルク語ロシア語英語韓国語言語不明西フリジア語中国語 (簡体字)中国語 (繁体字)南部ソト語日本語北部ソト語`].includes(str) || str.match(/^(自動再生)?再生速度字幕 \(1\)画質オフ日本語 \(自動生成\)自動翻訳$|字幕を追加オフ日本語 \(自動生成\)自動翻訳$/)) { elecli(1, '//button[@class="ytp-subtitles-button ytp-button" and @aria-pressed="true"]'); verb(`%c言語の選択肢が[${str}]なので字幕をオフにして終わります`, `color:#f00;`); return; } else { if (command.split(" ").includes("abortDS") && ds) { // デュアル字幕 verb(`%cデュアル字幕があるようなので字幕を選択せず終わります`, `color:#f00;`); return; } } } var ele = eleget0(xpath); verb(`${xpath} ... found : ${(elegeta(xpath).length)}`); if (ele) { for (let ele of elegeta(xpath)) { if (command.split(" ").includes("focus")) { ele.focus(); verb("...focused") } else { ele.click(); verb("...clicked") if (window != parent && beha) { let foc = eleget0('//div[@id="movie_player"]|//div[@id="player"]'); //if (foc) foc.scrollIntoView({ behavior: beha, block: "center", inline: "center" }); } } } } else { if (command.split(" ").includes("close")) { elecli(f, '//div[contains(@class,"ytp-right-controls")]/button[@aria-haspopup="true" and @aria-expanded="true"]|//button[@class="ytp-button ytp-settings-button" and @data-tooltip-target-id="ytp-settings-button" and @aria-expanded="true"]'); // 歯車がオープンの状態の歯車 verb("なかったので中断します"); } } if (command.split(" ").includes("blur")) elecli(f, '//div[@id="movie_player"]|//div[@id="player"]/div/div/video', 0, "focus"); }, delay * WAIT_EACHACTION * (ds ? 2 : 1)); } function eleget0(xpath, node = document) { if (!/^\.?\/\//.test(xpath)) return document.querySelector(xpath); var ele = document.evaluate(xpath, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); return ele.snapshotLength > 0 ? ele.snapshotItem(0) : ""; } function elegeta(xpath, node = document) { if (!xpath) return []; if (!/^\.?\/\//.test(xpath)) return [...document.querySelectorAll(xpath)]; var ele = document.evaluate(xpath, document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); try { var array = []; var ele = document.evaluate("." + xpath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0; i < ele.snapshotLength; i++) array[i] = ele.snapshotItem(i); return array; } catch (e) { return []; } } function verb() { if (verbose) console.log(...arguments); } function roaj(str) { if (CHECK_ROAJ && !checkROAJAlready) { alert(str); checkROAJAlready = 1; } } function vn(body, title = "") { // verbose notify if (VERBOSE_NOTIFY) notify(body, title); } function notify(body, title = "") { if (!("Notification" in window)) return; else if (Notification.permission == "granted") new Notification(title, { body: body }); else if (Notification.permission !== "denied") Notification.requestPermission().then(function(permission) { if (permission === "granted") new Notification(title, { body: body }); }); } function popup(text) { var e = document.getElementById("cccbox"); if (e) { e.remove(); } if (mllID) { clearTimeout(mllID); } if (!text > "") return text = text.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/`/g, '`').replace(//g, ">").replace(/\n/gm, "
") var ele = document.body.appendChild(document.createElement("span")); let bgcolor = /www\.translatetheweb\.com|\.translate\.goog\/|translate\.google\.com/gmi.test(location.href) ? "#d06050" : "#333"; ele.innerHTML = '' + text + ''; mllID = setTimeout(function() { var ele = document.getElementById("cccbox"); if (ele) ele.remove(); }, 4000); } function aToCopyUrl() { var kaisuu = 0 document.addEventListener('keydown', function(e) { //if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable || (inYOUTUBE && (e.target.closest('#chat-messages,ytd-comments-header-renderer') || document.activeElement.closest('#chat-messages,ytd-comments-header-renderer')))) return; var key = (e.shiftKey ? "Shift+" : "") + (e.altKey ? "Alt+" : "") + (e.ctrlKey ? "Ctrl+" : "") + e.key; if ((key === "a" || key === "Shift+A") && location.href.indexOf("//www.nicovideo.jp/watch/") != -1) { var ctimeEle = eleget0('//video'); if (ctimeEle) { var ctime = Math.floor(ctimeEle.currentTime); var ret = (navigator.platform.indexOf("Win") != -1) ? "\r\n" : "\n"; let newurl = (kaisuu == 0 ? "https://www.nicovideo.jp/watch/" : "https://nico.ms/") + location.href.match0(/\/watch\/([^/?]+)/) + (ctime > 0 ? ("?from=" + ctime) : ""); if (key === "Shift+A") history.pushState(null, null, newurl); var cb = document.title + ret + newurl + ret; GM.setClipboard(cb); popup(cb) kaisuu = ++kaisuu % KEY_A_VARIATIONS } e.preventDefault(); return false; } }, false); } function nicolive() { if (NICOLIVE_EXPAND_COMMENT_LIST) { setSlider(eleget0('//div[@data-input-mode="mouse"]/div/header/div'), 384, 1700, 384, "コメント欄の幅を調整:***px", "CommentListWidth", (rangee) => { let main = eleget0('//div[contains(@class,"___player-section___")]') let fullsc = eleget0('//button[@aria-label="フルスクリーン解除"]') ? null : "95%"; if (main) { main.style.width = fullsc main.style.maxWidth = fullsc } var commentWidth = rangee; //.value; //1200; //yohakuX-videoWidth elegeta('div[class*=___player-status-panel___]').forEach(e => e.style.width = `${commentWidth}px`) elegeta('div[class*=___table___]').forEach(e => e.style.width = `${commentWidth}px`); elegeta('span[class*=___table-cell___]').forEach(e => e.style.width = `${commentWidth}px`); }, 2000) } } // intervalが1以上かつ設定値がdefValueと違っていればintervalミリ秒事に再処理、そうでなければ割り込みはせず、初回の実行もしない function setSlider(placeEle, min = 0, max = 100, defValue = 0, title = "", key = "SliderValue", onchangecallback, interval = 0, addProperty = "") { if (placeEle) { let val = pref(key) || defValue; placeEle.insertAdjacentHTML("afterend", ``) let adjustFunc = function(key, interval = 0, defValue = 0, onchange = 0) { let sliderEle = eleget0(`#setSlider${key}`) let sliderEleNum = Number(sliderEle.value) onchangecallback(sliderEleNum) if (onchange) pref(key, sliderEleNum) if (interval && sliderEleNum != defValue) setTimeout(() => { adjustFunc(key, interval, defValue) }, interval) } let sliderEle = eleget0(`#setSlider${key}`) if (sliderEle) { sliderEle.addEventListener('input', () => adjustFunc(key, interval, defValue, 1)); if (Number(sliderEle.value) != defValue) adjustFunc(key, interval, defValue) } } } function pref(name, store = null) { // prefs(name,data)で書き込み(数値でも文字列でも配列でもオブジェクトでも可)、prefs(name)で読み出し if (store === null) { // 読み出し let data = GM_getValue(name); if (data == undefined) return null; // 値がない if (data.substring(0, 1) === "[" && data.substring(data.length - 1) === "]") { // 配列なのでJSONで返す try { return JSON.parse(data || '[]'); } catch (e) { console.log("データベースがバグってるのでクリアします\n" + e); pref(name, []); return; } } else return data; } if (store === "" || store === []) { // 書き込み、削除 GM_deleteValue(name); return; } else if (typeof store === "string") { // 書き込み、文字列 GM_setValue(name, store); return store; } else { // 書き込み、配列 try { GM_setValue(name, JSON.stringify(store)); } catch (e) { console.log("データベースがバグってるのでクリアします\n" + e); pref(name, ""); } return store; } } })();