Bilibili Danmaku Translator

Add translations on streaming user comments(弾幕;danmaku) of bilibili, with the translation of Google Chrome.

目前為 2019-07-09 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Bilibili Danmaku Translator
  3. // @name:ja Bilibili Danmaku Translator
  4. // @name:zh-CN Bilibili Danmaku Translator
  5. // @namespace knoa.jp
  6. // @description Add translations on streaming user comments(弾幕;danmaku) of bilibili, with the translation of Google Chrome.
  7. // @description:ja Google Chrome の翻訳ツールを使って、ビリビリのユーザーコメント(弾幕)を自動翻訳します。
  8. // @description:zh-CN 使用 Google Chrome 的翻译工具,自动翻译 bilibili 的用户评论(弹幕)。
  9. // @include /^https://www\.bilibili\.com/video/av[0-9]+/
  10. // @include /^https://live\.bilibili\.com/[0-9]+/
  11. // @version 2.1.8
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/pako/1.0.10/pako_inflate.min.js
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. (function(){
  17. const SCRIPTNAME = 'BilibiliDanmakuTranslator';
  18. const DEBUG = false;/*
  19. [update] 2.1.8
  20. 適訳辞書を更新。
  21.  
  22. [bug]
  23.  
  24. [to do]
  25.  
  26. [to research]
  27. Chrome翻訳負荷制限
  28. キューはクリアしない方針?遅れた翻訳は意義薄い?
  29. 文字列の長さの可能性?
  30. Chromeがサボるだけなら自家製クエリに手を出す手も?
  31. Chromeがどんどん反応を遅くしていった?
  32. 新語に対する複数回クエリなど謎の挙動?
  33. 右の一覧内でも特殊案内は訳したいかも
  34. 主要UI要素を指定翻訳語として登録しておきたい
  35. 動的に生成される要素の対応がめんどくさい
  36. 自分のコメントの翻訳時も逆辞書で節約と蓄積?
  37. 日本語と英語は翻訳しない方針で問題ないよね?
  38. Google翻訳の一般Webユーザーのフリをして各ユーザーにAPIを叩かせる手もあるようだが
  39. https://github.com/andy-portmen/Simple-Translate/blob/master/src/lib/common.js
  40. それが許されるならBaiduのAPIを叩かせることも可能?
  41. 翻訳辞書を共有サーバーに溜め込む仕組み?
  42. iframe内で映像配信する放送に対応できていない。
  43. https://live.bilibili.com/76
  44. pako.deflate + TextDecoder でdictionaryを無理やり圧縮して保存できる?
  45. 動画のタイトル下に翻訳を挿入したいね。
  46. MODIFICATIONSはたまに翻訳の確認が必要。
  47. MODIFICATIONSの活用率も確認したい。
  48.  
  49. [memo]
  50. 1. 翻訳辞書構築の流れ
  51. 1-1. core.listenWebSocketsで弾幕テキストを取得(要素出現より1秒ほど早く取得できる)
  52. 1-2. Translatorに弾幕テキストを登録
  53. 1-3. TranslatorがpriorDanmaku要素に弾幕テキスト要素を設置
  54. 1-4. Chromeが弾幕テキスト要素を自動翻訳してくれる
  55. 1-5. Translatorが察知して辞書として登録
  56.  
  57. 2. 弾幕訳文追加の流れ
  58. 2-1. core.observeVideoDanmakuで弾幕要素を発見
  59. 2-2. Danmakuインスタンスを作成してTranslatorに登録
  60. 2-3. 弾幕テキストに一致する辞書がすでにあればすぐに訳文を追加
  61. 2-4. なければ1-5.のタイミングで訳文を追加
  62.  
  63. 3. 自分の投稿コメント翻訳
  64. Google Apps Script (推定1日7000回(=1回5文字で月100万文字相当)を超えたあたりで制限がかかる)
  65. https://qiita.com/tanabee/items/c79c5c28ba0537112922
  66. */
  67. if(window === top && console.time) console.time(SCRIPTNAME);
  68. const NOW = Date.now();
  69. const ISMAC = (window.navigator.userAgent.match(/Mac/) !== null);
  70. const VIDEOINFOAPI = 'https://api.bilibili.com/x/web-interface/view'
  71. const COMMENTLISTAPI = 'https://comment.bilibili.com/{cid}.xml';/*動画用*/
  72. const CHATSERVER = 'chat.bilibili.com';/*直播用*/
  73. const TRANSLATOR = 'https://script.google.com/macros/s/AKfycby29iFLZ742UEC6TlN8-b4Dxtlu_7XYbVeo2GgiYVWMtuzIcbA/exec?text={text}&source={source}&target={target}';
  74. const TRANSLATIONSATONCE = 4;/*同時最大翻訳リクエスト数(Chrome翻訳負荷の低減)*/
  75. const TRANSLATIONSINTERVAL = 1000;/*最短翻訳リクエスト間隔(ms)(Chrome翻訳負荷の低減)*/
  76. const HISTORYLENGTH = 50000;/*辞書の最大保持数(5万で5MB見込み)*/
  77. const TRANSLATIONEXPIRED = 90*24*60*60*1000;/*翻訳の有効期限(翻訳精度の改善に期待する)*/
  78. const WAITING_LIMIT = 10*1000;/* Chrome翻訳の待機時間(ms)(過負荷時には実質休憩時間となる) */
  79. const BILIBILILANGUAGE = 'zh-CN';
  80. const USERLANGUAGE = window.navigator.language;
  81. const TRANSLATIONS = {
  82. ja: {
  83. inputTranslationKey: ISMAC ? '(Command+Enterで翻訳)' : '(Ctrl+Enterで翻訳)',
  84. },
  85. en: {
  86. inputTranslationKey: ISMAC ? '(Command+Enter to translate)' : '(Ctrl+Enter to translate)',
  87. },
  88. };
  89. const DICTIONARIES = {
  90. ja: {/* original: [translation, count, created] */
  91. '哔哩哔哩 (゜-゜)つロ 干杯~': ['ビリビリ (゜-゜)つロ 乾杯~', 0, NOW],
  92. },
  93. en: {
  94. '哔哩哔哩 (゜-゜)つロ 干杯~': ['bilibili (゜-゜)つロ cheers~', 0, NOW],
  95. },
  96. };
  97. const MODIFICATIONS = {/* およそ5000件で1ms (Core i7-3740QM) */
  98. /* '単語': [/誤訳(削除する)/, '適訳(挿入する)'] */
  99. ja: {
  100. // 日本語
  101. '发言': [/話す|スピーチ|スピーキング|ステートメント/, '発言'],
  102. '残念': [/残り/, '残念'],
  103. '干杯': [/トースト|干杯/, '乾杯'],
  104. '乾杯': [/トースト/, '乾杯'],
  105. '万岁': [/長生き(する|させる)?|ロングライブ/, '万歳'],
  106. '大丈夫': [/夫(ですか)?/, '大丈夫'],
  107. '正解': [/ポジティブソリューション/, '正解'],
  108. '無駄': [/イノセント|無実/g, '無駄'],
  109. '草': [/グラス|カオ/, '草'],
  110. '有能': [/エネルギーを持っている/, '有能'],
  111. '神回': [/神が戻ってきた|神様/, '神回'],
  112. '全裸待机': [/フルヌードスタンバイ/, '全裸待機'],
  113. '完全一致': [/完全に一貫性のある|完全に一貫しています/, '完全に一致'],
  114. '上手上手': [/手をつないで/, '上手上手'],
  115. '上手': [/はじめに/, '上手'],
  116. '清楚清楚': [/クリアとクリア/, '清楚清楚'],
  117. '清楚': [/クリア|明らか|明確/, '清楚'],
  118. '理解理解': [/理解(を|し)理解する/, '理解理解'],
  119. '余裕余裕': [/ゆうゆうゆうゆう/, '余裕余裕'],
  120. '兽耳': [/獣(の)?耳|動物の耳|獣|耳|ビーストイヤー/, 'ケモミミ'],
  121. '幻听': [/錯視|錯覚|聴覚幻覚|黙る|ありがとう|イリュージョン|イルルイン|イルリング|(オーディオ)?オーディション|ファンタジー|Illuing/, '幻聴'],
  122. '幻视': [/ファントム|マジック|魔法/, '幻視'],
  123. '错乱': [/(無秩序(に)?|乱雑|疾患|障害|カオス)/, '錯乱'],
  124. '混乱': [/カオス|混沌/, '混乱'],
  125. '认真': [/(本気|真剣に)/, '迫真'],
  126. '确信': [/(確認済み|信じ(る|て|ます)|納得|確信してい(る|ます))/, '確信'],
  127. '狂喜': [/エクスタシー/, '狂喜'],
  128. '震声': [/衝撃|ショック/, '震え声'],
  129. '棒读': [/(素晴らしい|良い|スティック|棒)(読書|リーディング)/, '棒読み'],
  130. '野生': [/ワイルド/, '野生'],
  131. '字幕组': [/字幕グループ/, '字幕組'],
  132. '字幕': [/キャプション/, '字幕'],
  133. '君中国语本当上手': [/6月中国語は始めるための方法です/, '君中国語本当上手'],
  134. '君日本语本当上手': [/6月日本語は始める方法です/, '君日本語本当上手'],
  135. // 中国語
  136. '帅': [/ハンサム(な)?/g, 'カッコイイ'],
  137. '大人': [/(?<!あの|この)(の)?(大人|成人|おとな|テーブル)/g, 'さま'],
  138. '老大': [/上司|ボス/, '老大'],
  139. '加油': [/さあ|さて|燃料補給|燃料を供給|給油|来て|来ます|歓声を上げる/g, 'がんばれ'],
  140. '厉害厉害': [/非常に強力/, 'すごいすごい'],
  141. '厉害': [/強力(な)?/, 'すごい'],
  142. '表白': [/エクスプレス|(の)?告白$|白|(を)?表現する|Express/, 'すき'],
  143. '来了来了': [/ここに来(る|て)/, '来ました'],
  144. '来了': [/来(る|て(います|いる)?)|さあ/, '来ました'],
  145. '辛苦了': [/ハードワーク|勤勉|(一生)?懸命(に)?働い(た|ている|ています)|大変です|難しい|つらい/, 'おつかれさま'],
  146. '冲冲冲': [/急いで/, 'いけいけいけ'],
  147. '见证历史': [/証人の歴史/, '歴史の証人'],
  148. '喷麦': [/スプレー|小麦散布/, '吐息音'],
  149. '配音': [/ダビング|Dubbing|声/, '吹き替え'],
  150. '眨眼': [/点滅/, 'まばたき'],/*ウィンクの場合もあるが点滅よりはマシ*/
  151. 'up主': [/アップ(マスター|メイン)?/, 'うp主'],
  152. '主播': [/アンカー/, '配信主'],
  153. '在现场': [/現場で/, '生で見てた'],
  154. '虚拟': [/(虚偽|虚似|虚俗|偽|仮想)(の)?/, 'バーチャル'],
  155. '穿模': [/金型/, 'モデル'],
  156. '安全裤': [/安全(ズボン|パンツ)/, 'スパッツ'],
  157. // 当て字
  158. '欧拉': [/オイラー|オウラ/g, 'オラ'],
  159. '木大': [/大きな木|ウッドビッグ/g, '無駄'],
  160. '赛高': [/サイガオ|さいがお|試合高|高(いです)?/, '最高'],
  161. '拜拜': [/さようなら|^バイ$/, 'バイバイ'],
  162. '奶思': [/ミルク思考/, 'ナイス'],
  163. '奶声': [/ミルクの声/, 'ナイス'],
  164. '奶死': [/ミルクデス|(牛乳|ミルク)(が|は)死ん(だ|でいる|でいます)/, 'ナイスデス'],
  165. '牙白': [/歯の白|白い歯|白|ホワイト/, 'やばい'],
  166. '纳尼': [/なな|にに|Nani/ig, 'なに'],
  167. '贴贴': [/ステッカー|それを固執する|投稿|貼付/, 'てぇてぇ'],
  168. '斯哈斯哈': [/ハシャ|シハハ|Sihasha/, 'スハスハ'],
  169. '嘶哈嘶哈': [/ヒップホップヒップホップ/, 'スハスハ'],
  170. '昆卡昆卡': [/クエンカクエンカ/, 'クンカクンカ'],
  171. '压力马斯内': [/突然の圧力|プレッシャー(マス(ヌ|ネ)|スニーク)/, 'やりますねぇ'],
  172. '压力马斯奈': [/(プレッシャー|圧力)マスネイ/, 'やりますねぇ'],
  173. '亚拉那一卡': [/ヤラナ(カード)?|Yalanaからのカード/, 'やらないか'],
  174. '妮可妮可妮': [/ニコールニコール/, 'にっこにっこにー'],
  175. // スラング
  176. '谢谢茄子': [/(ナス|茄子)(を)?ありがとう/, 'ありがとナス'],
  177. '謝謝茄子': [/(ナス|茄子)(を)?ありがとう/, 'ありがとナス'],
  178. '太草': [/草が多すぎる/, '大草原'],
  179. '鬼畜': [/ゴースト(アニマル)?|幽霊/, 'MAD'],
  180. '沙雕': [/砂(の)?彫刻|サンドカービング/, 'あほ'],
  181. '口区': [/口の面積|口地域|口元エリア/, 'ヴォエ'],
  182. '手冲': [/ハンドパンチ/, '手コキ'],
  183. '冲国': [/チョング(オ|ァ)|チョン(オ)?|Chongguo|忠国|急ぐ国/, 'チューゴク'],
  184. '康康': [/カンカン(させて(ください)?|が欲しい)/, 'ほら見せろよ'],
  185. '人类一败涂地': [/男は敗北|人間は敗北します|敗北した人間/, '人類は一敗地にまみれた'],
  186. '牛逼': [/ニウビ|ニー|強気/, 'すごい'],
  187. '棒棒哒': [/スティック|すごい/, 'しゅごい'],
  188. '白给': [/ホワイト(ギブ|がくれた)?|白/, '無駄死に'],
  189. '什么鬼': [/何ゴースト|なんて幽霊/, 'なんだこれ'],
  190. '单推': [/(シングル|ワン)?(プッシュ|クリック)|单推/, '単推し'],
  191. '烤肉': [/バーベキュー|肉のグリル/, '焼肉'],
  192. '石油佬': [/油(いかだ)?|いかだ|オイル(佬)?/, '石油王'],
  193. '大佬': [/ダクシー|ダキシー|ダラット|ダウェイ|Daxie|(大|ビッグ)(ショー|big|佬)|大きい|大学ビル/, 'ニキ'],
  194. 'C位': [/C(ビット)?/i, 'センター'],
  195. 'c位': [/C(ビット)?/i, 'センター'],
  196. '打Call': [/(お)?電話(をかける|する|ください)|呼ぶ|コール/, 'コールする'],
  197. '打call': [/(お)?電話(をかける|する|ください)|呼ぶ|コール/, 'コールする'],
  198. // 超意訳
  199. '酸死了': [/痛い/, '裏山死'],
  200. '酸了': [/(私は)?酸っぱい(です)?/, '裏山'],
  201. '才八点': [/たった8(時|つ)/, 'はえーよ'],
  202. '才8点': [/たった8(時|つ)/, 'はえーよ'],
  203. '光速下播': [/光速で放送する/, '急にオワタ'],
  204. '火钳刘明': [/((火)?トング|ファイアークランプ)劉明/g, '記念カキコ'],
  205. '前方高能': [/(先に|今後の)?高エネルギー(先)?(警告)?/, 'くるぞ'],
  206. '这不是演习': [/これは練習では(ありません|ないことに注意してください)/, 'これは演習ではない'],
  207. // 翻訳不能\(^o^)/
  208. '臣卜木曹': [/(チェン・ブカオ|Chen Bumu Cao)/, '(゚Д゚)'],
  209. '卧槽...': [/(横になって(いる)?|横)?トラフ(に横たわって|に横になっています)?/, '(゚Д゚)...'],
  210. '卧槽': [/(横になっている)?トラフ(に横たわって|は|で|の)?|横になって|横溝/, '(゚Д゚)!?'],
  211. // awsl
  212. '阿伟死了': [/魏は死ん(でいる|だ)/, 'awsl'],
  213. '阿伟输了': [/魏は失った/, 'awsl'],
  214. '阿伟爽了': [/魏がかっこいい/, 'awsl'],
  215. '阿伟射了': [/ウェイショット/, 'awsl'],
  216. '阿伟少林': [/魏少林寺/, 'awsl'],
  217. '啊我睡了': [/ああ、私は寝ました。/, 'awsl'],
  218. '爱我苏联': [/(私を愛して)?ソビエト連邦(を愛してください)?/, 'awsl'],
  219. '奥维丝丽': [/オビスリ/, 'awsl'],
  220. '阿伟乱葬岗': [/アウェイマスグレイブ/, 'awsl墓地'],
  221. '阿伟乱葬场': [/魏古墳/, 'awsl墓地'],
  222. // 固有名詞
  223. '谷酱': [/谷/, 'グーグルちゃん'],
  224. 'goo酱': [/グーソース/, 'Googleちゃん'],
  225. '油管': [/(オイル|(石)?油)パイプ|管|チューブ|チュービング/, 'YouTube'],
  226. '诸葛孔明': [/Zhuge Kongming|ジュージュコミング/, '諸葛孔明'],
  227. '孔明': [/Kong Ming|コミング|コングミン/, '孔明'],
  228. // キズナアイ
  229. '绊爱酱': [/ラブソース|ソースが大好き(です)?/, 'キズナアイちゃん'],
  230. '爱酱': [/(ラブ)?ソース|Love Sauce/g, 'アイちゃん'],
  231. // 白上フブキ
  232. '白上吹雪': [/白で吹(く)?雪|白雪姫/g, '白上フブキ'],
  233. '吹雪酱': [/吹雪ソース/g, 'フブキちゃん'],
  234. // リブドル
  235. '战斗吧歌姬': [/戦う、歌手|歌手と戦(う|ってください)|歌手との戦い/, 'リブドル'],
  236. 'Swan': [/白鳥/, 'スワン'],
  237. 'swan': [/白鳥/, 'スワン'],
  238. '罗兹': [/ロードス(島)?|ウッチ|Rhodes|Roz/ig, 'ローズ'],
  239. '神宫司': [/神社|神宮課|神宮シー/g, '神宮司'],
  240. '玉藻': [/ゆう(ざお|ぞう)|遊蔵|翡翠(藻)?|玉藻|玉|藻|湯田|遊戯王|裕蔵|湯蔵|ug尾|Yuzao|Yugao/g, '玉藻'],
  241. '清歌': [/(Li )?Qingge|(清の)?歌|(クリアな)?(曲|歌)|(クリア)?ソング|クリア|チンゲ/g, '清歌'],
  242. '清哥': [/Qingge|清の(兄弟|兄|弟)|チンゲ/g, '清歌'],
  243. '墨汐': [/インク(兄弟)?|Mo (Yan|Zhen)|Moxi/g, 'モーシィ'],
  244. '卡缇娅': [/ケイヤ|カヤ|キャシー|Kayya|Katya|缇卡缇娅|缇卡娅娅/ig, 'カティア'],
  245. '鸭鸭': [/アヒル(と|や|に)アヒル|アヒル|あひる/g, '鴨鴨'],
  246. '伊莎贝拉': [/イザベラ/g, 'イザベラ'],
  247. '贝拉拉': [/ベララ|ベッラーラ|Bellala/g, 'ベララ'],
  248. // Overidea
  249. '张京华': [/張晶華|チャン静華|チャンジンファ|(Zhang )?Jinghua/, '張京華'],
  250. '京华': [/晶華|景華|清華|ジンファ|Jinghua/, '京華'],
  251. '谢拉': [/シ(ー)?ラ|シェ(イ)?ラ|セラ|ゼラ|Sheila|Shera|Sierra|Xie( La)?|謝(La|ラ)?|谢拉/i, 'シエラ'],
  252. '謝拉': [/シ(ー)?ラ|シェ(イ)?ラ|セラ|ゼラ|Sheila|Shera|Sierra|Xie( La)?|謝(La|ラ)?|谢拉/i, 'シエラ'],
  253. '米娅': [/アミア|Mia/i, 'ミア'],
  254. // 織田信姫
  255. '信姬': [/Xin Ji|Xinji|新(地|自)/i, '信姫'],
  256. // 朝ノ姉妹
  257. '瑠璃': [/(着色)?ガラス|Liuli/i, '瑠璃'],
  258. // 神楽めあ
  259. 'mea酱': [/(ミート|ミース)ソース/ig, 'めあちゃん'],
  260. // 湊あくあ
  261. '阿库娅': [/アクア|あくや|阿久谷|Akuya|Akua/ig, 'あくあ'],
  262. // 猫宮ひなた
  263. '猫宫酱': [/猫(の)?宮殿ソース/ig, '猫宮ちゃん'],
  264. '猫宫': [/猫(の)?宮殿|キャットパレス|Cat Palace/ig, '猫宮'],
  265. '貓宮': [/猫(の)?宮殿|キャットパレス|Cat Palace/ig, '猫宮'],
  266. 'Hinata酱': [/(ひなた|日向|Hinata)(ソース|醤油)/ig, 'ひなたちゃん'],
  267. 'hinata酱': [/(ひなた|日向|Hinata)(ソース|醤油)/ig, 'ひなたちゃん'],
  268. 'HINATA酱': [/(ひなた|日向|Hinata)(ソース|醤油)/ig, 'ひなたちゃん'],
  269. // 物述有栖
  270. '爱丽丝酱': [/アリスソース/g, 'ありすちゃん'],
  271. // Paryi
  272. '迷迭迷迭': [/ローズマリー/, '見て見て'],
  273. '帕里桑': [/パリサン|プリッサン|パリジャン|Parrysan/g, 'Paryiさん'],
  274. '帕里': [/パリ(ー)?|Parry/g, 'Paryi'],
  275. 'Paryi': [/パリリ/g, 'Paryi'],
  276. 'paryi': [/パリリ/g, 'Paryi'],
  277. // 米米米
  278. '米米米': [/ミミ/, '米米米'],
  279. '光姬': [/広智|広義|グァンジ|ジジ|Guang()?Ji|Gigi Hime/, 'つや姫'],
  280. '光酱': [/(ライト|軽い)ソース/, 'つやちゃん'],
  281. '萌实里': [/メン(シリ|グリ)|Meng Shili/, '萌えみのり'],
  282. 'Milky女王': [/(ミルキー|ミルクの|牛乳)女王/, 'ミルキークイーン'],
  283. // 洛天依
  284. '洛天依': [/羅天(一)?|Luotianyi/, '洛天依'],
  285. // Siva
  286. 'Siva小虾鱼': [/シバエビ|Siva Shrimp Fish/, 'Sivaえび'],
  287. 'siva小虾鱼': [/シバエビ|Siva Shrimp Fish/, 'Sivaえび'],
  288. '虾虾': [/えび(と|や|、)えび|エビ(と|や|、)エビ|海老(と|や|、)海老|Shrimp and Shrimp|えび|エビ/, 'えびえび'],
  289. '雪风': [/雪の風|雪|風|スノーウィンド/, '雪風'],
  290. // ビリビリ
  291. '哔哩哔哩': [/(哔)+(哩)+|こんにちは|ビ(ブ)?リリ/, 'ビリビリ'],
  292. 'bilibili': [/Bilibili|ビリビリ|ビ(ブ)?リリ/, 'bilibili'],
  293. 'B站': [/駅B|B駅|(B|ビー)ステーション|ステーションB|駅/i, 'ビリビリ'],
  294. 'b站': [/駅b|b駅|(b|ビー)ステーション|ステーションB|駅/i, 'ビリビリ'],
  295. '舰长': [/船長|キャプテン/, '艦長'],
  296. '提督': [/提督/, '提督'],
  297. '总督': [/総督|知事/, '総督'],
  298. '友爱社': [/愛の友達|友情協会/, '友愛社'],
  299. '粉丝勋章': [/ファンメダル/, 'ファンバッジ'],
  300. '金瓜子': [/(ゴールデン|金)メロンの種/, '金瓜子'],
  301. '银瓜子': [/(シルバー|銀)メロンの種/, '銀瓜子'],
  302. '瓜子': [/メロンの種/, '瓜子'],
  303. '辣条': [/(スパイシー|ホット)(な)?(ストリップ|バー)/, '辣条'],
  304. '关注': [/注意|注目|懸念|心配(されている|する)/, 'フォロー'],
  305. // 禁断の一字置換
  306. '酱': [/ソース/g, 'ちゃん'],
  307. },
  308. en: {
  309. '草': [/grass/, 'lol'],
  310. },
  311. };
  312. const REGEXP = {
  313. hasKana: /[ぁ-んァ-ン]/,
  314. allAlphabet: /^[a-zA-Z0-9,.'"!?\s]+$/,
  315. allEmoji: /^(\ud83c[\udf00-\udfff]|\ud83d[\udc00-\ude4f]|\ud83d[\ude80-\udeff]|\ud7c9[\ude00-\udeff]|[\u2600-\u27BF])+$/,
  316. };
  317. const RETRY = 10;
  318. let sites = {
  319. video: {
  320. targets: {
  321. danmakuSetting: () => $('.bilibili-player-video-danmaku-setting'),/*弾幕設定*/
  322. videoDanmaku: () => $('.bilibili-player-video-danmaku'),/* div or canvas */
  323. },
  324. translationTargets: [
  325. [false, () => $('title')],
  326. [false, () => $('body')],
  327. ],
  328. get: {
  329. commentlistApi: (videoInfo) => COMMENTLISTAPI.replace('{cid}', videoInfo.pages[sites.video.get.page() - 1].cid),
  330. page: () => location.href.includes('?p=') ? parseInt(location.href.match(/\?p=([0-9]+)/)[1]) : 1,
  331. danmakuTypeCSS: (danmakuSetting) => danmakuSetting.querySelector('li.bui-select-item[data-value="div"]'),
  332. danmakuInput: () => $('input.bilibili-player-video-danmaku-input'),
  333. },
  334. },
  335. live: {
  336. targets: {
  337. operableContainer: () => $('.bilibili-live-player-video-operable-container'),/*特殊弾幕枠*/
  338. videoDanmaku: () => $('.bilibili-live-player-video-danmaku'),
  339. chatHistoryList: () => $('#chat-history-list'),
  340. chatActions: () => $('#chat-control-panel-vm .bottom-actions'),
  341. },
  342. translationTargets: [
  343. [false, () => $('title')],
  344. [false, () => $('body')],
  345. [ true, () => $('.bilibili-live-player-video-controller')],/*プレイヤ内コントローラ*/
  346. [ false, () => $('.bilibili-live-player-video-controller-duration-btn > div > span')],
  347. [ true, () => $('#chat-control-panel-vm')],/*投稿欄内コントローラ*/
  348. [ false, () => $('#chat-control-panel-vm .bottom-actions')],
  349. ],
  350. get: {
  351. operableSpace: (operableContainer) => operableContainer.querySelector('#pk-vm ~ div[style*="height:"]'),
  352. danmakuInput: () => $('textarea.chat-input'),/*divからtextareaに置換される*/
  353. },
  354. },
  355. };
  356. let html, elements = {}, storages = {}, timers = {}, sizes = {}, site;
  357. let translator, translations = {}, videoInfo;
  358. class Packet{
  359. /* Bilibili Live WebSocket message packet */
  360. /* thanks to:
  361. https://segmentfault.com/a/1190000017328813
  362. https://blog.csdn.net/xuchen16/article/details/81064372
  363. https://github.com/shugen002/userscript/blob/master/BiliBili%20WebSocket%20Proxy%20Rebuild.user.js
  364. */
  365. constructor(buffer){
  366. Packet.VERSION_COMPRESSED = 2;/* protocol version for compressed body */
  367. Packet.OPERATION_COMMAND = 5;/* operation type for command */
  368. Packet.COMMAND_DANMAKU = 'DANMU_MSG';/* command code for 弾幕(danmaku/danmu) */
  369. this.buffer = buffer;
  370. this.dataView = new DataView(buffer);
  371. this.views = {
  372. package: this.dataView.getUint32(0),/* packet length */
  373. header: this.dataView.getUint16(4),/* header length = offset for body */
  374. version: this.dataView.getUint16(6),/* protocol version */
  375. operation: this.dataView.getUint32(8),/* operation type */
  376. };
  377. try{
  378. this.array = this.getArray();
  379. this.messages = this.getMessages();
  380. }catch(e){
  381. log(e, this.views, new Uint8Array(this.buffer));
  382. }
  383. }
  384. getArray(){
  385. return (this.isCompressed)
  386. ? pako.inflate(new Uint8Array(this.buffer, this.views.header))
  387. : new Uint8Array(this.buffer)
  388. ;
  389. }
  390. getMessages(){
  391. let dataView = new DataView(this.array.buffer);
  392. let messages = [], headerLength = this.views.header, decoder = new TextDecoder();
  393. for(let pos = 0, packetLength = 0; pos < this.array.length; pos += packetLength){
  394. packetLength = dataView.getUint32(pos);
  395. let subarray = this.array.subarray(pos + headerLength, pos + packetLength);
  396. let string = decoder.decode(subarray);
  397. messages.push(string[0] === '{' ? JSON.parse(string) : string);
  398. }
  399. return messages;
  400. }
  401. getDanmakuContents(){
  402. return this.getDanmakus().map(d => {
  403. if(d.info === undefined) return log('Unexpected Danmaku JSON.', d), null;
  404. return d.info[1];
  405. });
  406. }
  407. getDanmakus(){
  408. if(this.isCommand === false) return [];
  409. return this.messages.filter(m => {
  410. if(m.cmd === undefined) return log('Unexpected Command JSON:', m), false;
  411. return m.cmd.startsWith(Packet.COMMAND_DANMAKU);
  412. });
  413. }
  414. get isCompressed(){
  415. return (this.views.version === Packet.VERSION_COMPRESSED);
  416. }
  417. get isCommand(){
  418. return (this.views.operation === Packet.OPERATION_COMMAND);
  419. }
  420. }
  421. class Translator{
  422. /* Danmaku translator using the browser's auto translation */
  423. constructor(){
  424. Translator.TRANSLATIONSATONCE = TRANSLATIONSATONCE;
  425. Translator.TRANSLATIONSINTERVAL = TRANSLATIONSINTERVAL;
  426. Translator.HISTORYLENGTH = HISTORYLENGTH;
  427. Translator.TRANSLATIONEXPIRED = TRANSLATIONEXPIRED;
  428. Translator.DICTIONARY = DICTIONARIES[USERLANGUAGE] || DICTIONARIES[USERLANGUAGE.substring(0, 2)] || {};
  429. Translator.MODIFICATIONS = MODIFICATIONS[USERLANGUAGE] || MODIFICATIONS[USERLANGUAGE.substring(0, 2)] || {};
  430. Translator.MODIFICATIONSKEYS = Object.keys(Translator.MODIFICATIONS);
  431. Translator.PRIOR_WAITING_LIMIT = WAITING_LIMIT;
  432. this.counters = {pushes: 0, registerTranslations: 0, fails: 0};
  433. this.dictionary = this.getDictionary();
  434. this.history = Storage.read('history') || [];
  435. this.priorDanmaku = this.createPriorDanmaku();
  436. this.priorDanmakuWaitings = {};/* waiting for getting translated */
  437. this.priorDanmakuRequested = 0;/* last requested time */
  438. this.priorDanmakuQueue = [];/* queue for preventing multiple request in TRANSLATIONSINTERVAL */
  439. this.timer = 0;/* timer to next TRANSLATIONSINTERVAL */
  440. this.danmakuWaitings = {};/* waiting for getting translation */
  441. }
  442. getDictionary(){
  443. /* use browser language dictionary */
  444. let dictionary;
  445. if(Storage.read('USERLANGUAGE') !== USERLANGUAGE) dictionary = Translator.DICTIONARY;
  446. else dictionary = Storage.read('dictionary') || Translator.DICTIONARY;
  447. Storage.save('USERLANGUAGE', USERLANGUAGE);
  448. dictionary = this.updateDictionary(dictionary);
  449. return dictionary;
  450. }
  451. updateDictionary(dictionary){
  452. /* update structure (2019/6/11) */
  453. let keys = Object.keys(dictionary);
  454. if(typeof dictionary[keys[0]] === 'string') keys.forEach(key => {
  455. dictionary[key] = [dictionary[key], 1, NOW];
  456. });
  457. /* update key (2019/6/23) */
  458. let oldKey = 'BilibiliLiveCommentTranslator';
  459. let oldDictionary = localStorage[`${oldKey}-dictionary`], oldHistory = localStorage[`${oldKey}-history`];
  460. if(oldDictionary && oldHistory){
  461. dictionary = JSON.parse(oldDictionary).value;
  462. this.history = JSON.parse(oldHistory).value;
  463. localStorage.removeItem(`${oldKey}-dictionary`);
  464. localStorage.removeItem(`${oldKey}-history`);
  465. }
  466. return dictionary;
  467. }
  468. createPriorDanmaku(){
  469. /* Append danmaku comments from WebSocket for translating by browser as fast as possible */
  470. let priorDanmaku = elements.priorDanmaku = createElement(core.html.priorDanmaku());
  471. document.body.appendChild(priorDanmaku);
  472. return priorDanmaku;
  473. }
  474. pushAll(originals){
  475. originals.forEach(o => this.push(o));
  476. this.throttle();
  477. }
  478. push(original){
  479. this.counters.pushes++;
  480. if(this.dictionary[original] !== undefined) return this.dictionary[original][1]++;/* already exists in the dictionary */
  481. if(this.priorDanmakuQueue.includes(original) === true) return;/* already queued */
  482. if(this.priorDanmakuWaitings[original] !== undefined) return;/* already waiting for translation */
  483. if(this.shouldBeTranslated(original) === false) return;/* seems not to be Chinese */
  484. this.priorDanmakuQueue.push(original);
  485. }
  486. throttle(){
  487. if(this.priorDanmakuQueue.length === 0) return;
  488. /* throttle for single waiting query to Chrome Translation */
  489. if(this.priorDanmaku.children.length > 0) return;
  490. /* throttle for TRANSLATIONSINTERVAL */
  491. let now = Date.now(), elapsed = now - this.priorDanmakuRequested;
  492. clearTimeout(this.timer);
  493. if(elapsed <= Translator.TRANSLATIONSINTERVAL){
  494. this.timer = setTimeout(() => this.putOnPriorDanmaku(), Translator.TRANSLATIONSINTERVAL - elapsed);
  495. }else{
  496. this.putOnPriorDanmaku();
  497. }
  498. }
  499. putOnPriorDanmaku(){
  500. //log(this.priorDanmakuQueue);
  501. this.priorDanmakuRequested = Date.now();
  502. let putOnce = this.putOnPriorDanmaku.putOnce ? true : false;/* it can put more only on first time */
  503. let atOnce = putOnce ? Translator.TRANSLATIONSATONCE : 10*1000;
  504. let fragment = document.createDocumentFragment();
  505. this.priorDanmakuQueue.reverse();/* from latest danmaku */
  506. for(let i = 0, original; original = this.priorDanmakuQueue[i]; i++){
  507. if(atOnce <= i) break;
  508. let li = createElement(core.html.danmakuContent(original));
  509. this.priorDanmakuWaitings[original] = li;
  510. fragment.appendChild(li);
  511. /* Observe auto translation by browser */
  512. let observer = observe(li, (records) => {
  513. //log('Got translated:', original);
  514. this.registerTranslation(original, li.textContent);
  515. this.removeWaiting(original, li, observer);
  516. this.throttle();
  517. });
  518. /* Time to give up */
  519. setTimeout(() => {
  520. if(li && li.isConnected){
  521. log('Give up for waiting translated:', original);
  522. this.counters.fails++;
  523. this.removeWaiting(original, li, observer);
  524. }
  525. }, (putOnce) ? Translator.PRIOR_WAITING_LIMIT : 60*60*1000);
  526. }
  527. //log(Array.from(fragment.children).map(c => c.textContent));
  528. this.priorDanmaku.appendChild(fragment);
  529. this.priorDanmakuQueue = [];/* dropped */
  530. this.putOnPriorDanmaku.putOnce = true;
  531. }
  532. registerTranslation(original, translation){
  533. this.counters.registerTranslations++;
  534. this.dictionary[original] = [translation, 1, Date.now()];
  535. this.history.push(original);
  536. /* append the translation for each streaming danmakus */
  537. if(this.danmakuWaitings[original]){
  538. this.danmakuWaitings[original].forEach(d => this.appendTranslation(d, translation));
  539. delete this.danmakuWaitings[original];
  540. }
  541. }
  542. removeWaiting(original, span, observer){
  543. observer.disconnect();
  544. span.parentNode.removeChild(span);
  545. delete this.priorDanmakuWaitings[original];
  546. }
  547. requestTranslation(danmaku){
  548. if(this.shouldBeTranslated(danmaku.textContent) === false) return;/* seems not to be Chinese */
  549. if(this.dictionary[danmaku.textContent] === undefined){
  550. if(this.danmakuWaitings[danmaku.textContent] === undefined) this.danmakuWaitings[danmaku.textContent] = [];
  551. this.danmakuWaitings[danmaku.textContent].push(danmaku);
  552. }else{
  553. if(danmaku.textContent === this.dictionary[danmaku.textContent][0]) return;/* original and translation are the same */
  554. this.appendTranslation(danmaku, this.dictionary[danmaku.textContent][0]);
  555. }
  556. }
  557. appendTranslation(danmaku, translation){
  558. log(danmaku.textContent, translation);
  559. /* it's better to modify before writing to dictionary, but MODIFICATIONS may often be updated */
  560. Translator.MODIFICATIONSKEYS.filter(key => danmaku.textContent.includes(key)).forEach(key => {
  561. if(DEBUG && Translator.MODIFICATIONS[key][0].test(translation) === false) log(
  562. 'Doesn\'t match:', danmaku.textContent, key, translation, Translator.MODIFICATIONS[key],
  563. );
  564. translation = translation.replace(Translator.MODIFICATIONS[key][0], Translator.MODIFICATIONS[key][1]);
  565. });
  566. danmaku.appendTranslation(translation);
  567. }
  568. shouldBeTranslated(textContent){
  569. switch(true){
  570. case(this.dictionary[textContent] !== undefined):/* has a translation */
  571. return true;
  572. case(textContent.match(REGEXP.hasKana) !== null):/* seems to be Japanese */
  573. case(textContent.match(REGEXP.allAlphabet) !== null):/* seems to be English */
  574. case(textContent.match(REGEXP.allEmoji) !== null):/* seems to be Emoji */
  575. return false;
  576. default:
  577. return true;
  578. }
  579. }
  580. save(){
  581. /* log usage statistics */
  582. let c = this.counters, saved = (((c.pushes - c.fails - c.registerTranslations)/((c.pushes - c.fails) || 1))*100).toFixed(0) + '%';
  583. log('Total danmaku:', c.pushes, 'Newly translated:', c.registerTranslations, 'Saved:', saved, 'Fails:', c.fails);
  584. /* save the dictionary and the history of latest HISTORYLENGTH pairs */
  585. let newDictionary = {}, newHistory = [];
  586. for(let i = this.history.length - 1, count = 0, now = Date.now(); 0 <= i; i--){
  587. if(this.dictionary[this.history[i]] === undefined){
  588. log('Unknown history', this.history[i]);
  589. continue;
  590. };
  591. if(this.dictionary[this.history[i]][2] < now - Translator.TRANSLATIONEXPIRED) continue;/* old data */
  592. if(newDictionary[this.history[i]] !== undefined) continue;
  593. newDictionary[this.history[i]] = this.dictionary[this.history[i]];
  594. newHistory[count] = this.history[i];
  595. if(count++ === Translator.HISTORYLENGTH) break;
  596. }
  597. /* keep the default dictionary */
  598. Object.keys(Translator.DICTIONARY).forEach(key => {
  599. newDictionary[key] = newDictionary[key] || Translator.DICTIONARY[key];
  600. });
  601. log('Dictionary length:', newHistory.length, 'Stored size:', toMetric(JSON.stringify(newDictionary).length * 2) + 'bytes');
  602. Storage.save('dictionary', newDictionary);
  603. Storage.save('history', newHistory.reverse());
  604. }
  605. }
  606. class Danmaku{
  607. constructor(danmaku){
  608. Danmaku.zIndex = Danmaku.zIndex || 1;
  609. this.element = danmaku;
  610. this.textContent = danmaku.textContent;
  611. this.modify();
  612. }
  613. modify(){
  614. this.element.style.zIndex = parseInt(this.element.style.zIndex || 0) + Danmaku.zIndex++;/* newer comments have priority */
  615. /* Make space for appending translation text */
  616. this.element.style.top = (() => {
  617. if(this.element.style.top === '') return;
  618. let operableContainer = elements.operableContainer, operableSpace = operableContainer ? site.get.operableSpace(operableContainer) : null;
  619. if(this.element.style.top[0] === '-' || operableSpace === null || operableSpace.children.length === 0 || operableSpace.style.height === ''){
  620. return (parseFloat(this.element.style.top) * 2) + 'px';
  621. }else{
  622. let height = parseFloat(operableSpace.style.height), top = parseFloat(this.element.style.top);
  623. return (height + ((top - height) * 2)) + 'px';
  624. }
  625. })();
  626. /* Even if double long translation text added, keep streaming to completely go away */
  627. this.element.style.transitionDuration = ((transitionDuration) => {
  628. if(transitionDuration === '') return;
  629. let m = transitionDuration.match(/([0-9.]+)(m?s)/);
  630. if(m === null) return log('Unknown transitionDuration format:', transitionDuration), transitionDuration;
  631. return (parseFloat(m[1]) * 2) + m[2];
  632. })(this.element.style.transitionDuration);
  633. this.element.style.transform = ((transform) => {
  634. if(transform === '') return;
  635. let m = transform.match(/(translateX?)\(([-0-9.]+)(px)/);
  636. if(m === null) return log('Unknown transform format:', transform), transform;
  637. return transform.replace(m[0], `${m[1]}(${parseFloat(m[2]) * 2}${m[3]}`);
  638. })(this.element.style.transform);
  639. }
  640. appendTranslation(translation){
  641. let span = createElement(core.html.translation(translation));
  642. this.element.appendChild(span);
  643. span.animate([{opacity: `0`},{opacity: `1`}], {duration: 500, fill: 'forwards'});
  644. this.element.addEventListener('transitionend', (e) => {
  645. span.animate([{opacity: `1`},{opacity: `0`}], {duration: 500, fill: 'forwards'});
  646. }, {once: true});
  647. }
  648. get hasTranslation(){
  649. /* bilibili removes previous translation element when the danmaku element has reused */
  650. return (this.element.querySelector('.translation') === null) ? false : true;
  651. }
  652. }
  653. let core = {
  654. initialize: function(){
  655. html = document.documentElement;
  656. html.classList.add(SCRIPTNAME);
  657. switch(true){
  658. case(location.href.match(/^https:\/\/www\.bilibili\.com\/video\/av[0-9]+/) !== null):
  659. site = sites.video;
  660. translator = new Translator();
  661. core.listenXMLHttpRequests();
  662. core.targetTranslation();
  663. core.readyForVideo();
  664. break;
  665. case(location.href.match(/^https:\/\/live\.bilibili\.com\/[0-9]+/) !== null):
  666. site = sites.live;
  667. translator = new Translator();
  668. core.listenWebSockets();
  669. core.targetTranslation();
  670. core.readyForLive();
  671. break;
  672. default:
  673. log('Bye.');
  674. break;
  675. }
  676. core.observeHead();
  677. },
  678. readyForVideo: function(){
  679. if(document.hidden) return setTimeout(core.readyForVideo, 1000);
  680. core.getTargets(site.targets, RETRY).then(() => {
  681. log("I'm ready for Video.");
  682. core.translateUserInterface();
  683. core.setDanmakuSettings();
  684. core.observeVideoDanmaku();
  685. core.modifyDanmakuInput();
  686. core.addStyle();
  687. core.readyForUnload();
  688. core.export();
  689. });
  690. },
  691. readyForLive: function(){
  692. if(document.hidden) return setTimeout(core.readyForVideo, 1000);
  693. core.getTargets(site.targets, RETRY).then(() => {
  694. log("I'm ready for Live.");
  695. core.translateUserInterface();
  696. core.observeVideoDanmaku();
  697. core.modifyDanmakuInput();
  698. core.addStyle();
  699. core.readyForUnload();
  700. core.export();
  701. });
  702. },
  703. observeHead: function(){
  704. /* URL変化の検出の代替 */
  705. let head = $('head'), url = location.href;
  706. let observer = observe(head, function(records){
  707. if(url === location.href) return;
  708. url = location.href;
  709. log(head);
  710. observer.disconnect();
  711. core.initialize();
  712. }, {childList: true, characterData: true, subtree: true});
  713. },
  714. targetTranslation: function(){
  715. const setTranslate = function(element){
  716. element.classList.add('translate');
  717. element.translate = true;
  718. };
  719. const setNoTranslate = function(element){
  720. element.classList.add('notranslate');
  721. element.translate = false;
  722. };
  723. for(let i = 0, target; target = site.translationTargets[i]; i++){
  724. if(target[1]() === null) return setTimeout(core.targetTranslation, 1000);
  725. if(target[0] === true) setTranslate(target[1]());
  726. else setNoTranslate(target[1]());
  727. }
  728. },
  729. translateUserInterface: function(){
  730. translations = TRANSLATIONS[USERLANGUAGE] || TRANSLATIONS[USERLANGUAGE.substring(0, 2)] || TRANSLATIONS.en;
  731. /*置換したりobserveしたりする・・・かもしれない*/
  732. },
  733. listenXMLHttpRequests: function(){
  734. /* 公式の通信内容を取得 */
  735. window.XMLHttpRequest = new Proxy(XMLHttpRequest, {
  736. construct(target, arguments){
  737. const xhr = new target(...arguments);
  738. //log(xhr, arguments);
  739. xhr.addEventListener('load', function(e){
  740. if(xhr.responseURL.startsWith(VIDEOINFOAPI) === false) return;
  741. if(xhr.response[0] !== '{') return;
  742. videoInfo = JSON.parse(xhr.response).data;
  743. //log(videoInfo);
  744. core.getDanmakuList();
  745. });
  746. return xhr;
  747. }
  748. });
  749. },
  750. getDanmakuList: function(){
  751. let api = site.get.commentlistApi(videoInfo);
  752. fetch(api, {credentials: 'include', mode: 'cors'})
  753. .then(response => response.text())
  754. .then(text => new DOMParser().parseFromString(text, 'text/xml'))
  755. .then(d => {
  756. let ds = d.querySelectorAll('d');
  757. if(ds.length === 0) return log('Unknown danmaku format:', d);
  758. let danmakuContents = Array.from(ds).map(d => d.textContent);
  759. translator.pushAll(danmakuContents);
  760. });
  761. },
  762. listenWebSockets: function(){
  763. /* 公式の通信内容を取得 */
  764. window.WebSocket = new Proxy(WebSocket, {
  765. construct(target, arguments){
  766. const ws = new target(...arguments);
  767. //log(ws, arguments);
  768. if(ws.url.includes(CHATSERVER)) ws.addEventListener('message', function(e){
  769. let packet = new Packet(e.data);
  770. //log(packet.views, packet.messages);
  771. if(packet.isCommand === false) return;
  772. let danmakuContents = packet.getDanmakuContents();
  773. if(danmakuContents.length === 0) return;
  774. //log(danmakuContents.length, danmakuContents);
  775. translator.pushAll(danmakuContents);
  776. });
  777. return ws;
  778. }
  779. });
  780. },
  781. setDanmakuSettings: function(){
  782. if(elements.videoDanmaku.localName === 'canvas'){
  783. let danmakuSetting = elements.danmakuSetting;
  784. danmakuSetting.dispatchEvent(new MouseEvent('mouseover'));
  785. danmakuSetting.dispatchEvent(new MouseEvent('mouseout'));
  786. animate(function(){
  787. let danmakuTypeCSS = site.get.danmakuTypeCSS(danmakuSetting);
  788. if(danmakuTypeCSS) danmakuTypeCSS.click();
  789. else log('Can\'t find CSS3 setting.', danmakuSetting);
  790. });
  791. }
  792. },
  793. observeVideoDanmaku: function(){
  794. let videoDanmaku = elements.videoDanmaku;
  795. let observer = observe(videoDanmaku, function(records){
  796. //log(records);
  797. for(let i = 0; records[i]; i++){
  798. if(records[i].addedNodes.length === 0) continue;
  799. if(records[i].addedNodes[0].classList.contains('bilibili-danmaku') === false) continue;
  800. let danmaku = new Danmaku(records[i].addedNodes[0]);
  801. translator.requestTranslation(danmaku);
  802. observeDanmaku(danmaku);/*danmakuは再利用される!*/
  803. }
  804. });
  805. const observeDanmaku = function(danmaku){
  806. /* 再利用(新規弾幕としての生まれ変わり)を検知したい */
  807. let observer = observe(danmaku.element, function(records){
  808. if(danmaku.hasTranslation) return;/*再利用ではなく翻訳文追加だった*/
  809. danmaku = new Danmaku(danmaku.element);/*上書き*/
  810. translator.requestTranslation(danmaku);
  811. });
  812. };
  813. },
  814. modifyDanmakuInput: function(){
  815. /* 弾幕投稿内容を翻訳する機能を追加 */
  816. let danmakuInput = site.get.danmakuInput(), modifier = ISMAC ? 'metaKey' : 'ctrlKey';
  817. if(danmakuInput === null || danmakuInput.placeholder === undefined) return setTimeout(core.modifyDanmakuInput, 1000);/*属性付与が遅れる場合もあるので*/
  818. danmakuInput.placeholder += '\n' + translations.inputTranslationKey;
  819. observe(danmakuInput, function(record){
  820. if(danmakuInput.placeholder.endsWith(translations.inputTranslationKey)) return;
  821. danmakuInput.placeholder += '\n' + translations.inputTranslationKey;
  822. }, {attributes: true, attributeFilter: ['placeholder']});
  823. window.addEventListener('keydown', function(e){
  824. if(e.target !== danmakuInput) return;
  825. if(e.key === 'Enter' && e[modifier] === true){
  826. e.preventDefault();
  827. e.stopPropagation();
  828. danmakuInput.classList.add('translating');
  829. let api = TRANSLATOR.replace('{text}', danmakuInput.value).replace('{source}', USERLANGUAGE).replace('{target}', BILIBILILANGUAGE);
  830. fetch(api, {mode: 'cors'})
  831. .then(response => response.text())
  832. .then(text => {
  833. //log(text);
  834. danmakuInput.value = text;
  835. danmakuInput.dispatchEvent(new InputEvent('input'));/*実際の送信内容に反映させるために必要*/
  836. danmakuInput.classList.remove('translating');
  837. })
  838. .catch(error => {
  839. log('Error:', error);
  840. danmakuInput.classList.remove('translating');
  841. });
  842. }
  843. }, true);
  844. },
  845. readyForUnload: function(){
  846. window.addEventListener('unload', function(e){
  847. translator.save();
  848. });
  849. },
  850. export: function(){
  851. if(DEBUG === false) return;
  852. window.save = translator.save.bind(translator);
  853. window.list = function(includes, excludes = /DUMMY/){
  854. let dictionary = translator.dictionary;
  855. return Object.keys(dictionary).filter(key => {
  856. return includes.test(key) && !excludes.test(dictionary[key][0]);
  857. }).sort((a, b) => {
  858. return dictionary[b][1] - dictionary[a][1];
  859. }).map(key => {
  860. return [
  861. (new Date(dictionary[key][2])).toLocaleString(),/* used */
  862. dictionary[key][1],/* count */
  863. key,/* original */
  864. dictionary[key][0],/* translation */
  865. ];
  866. }).slice(0,100);
  867. }
  868. window.ranking = function(){
  869. let dictionary = translator.dictionary;
  870. return Object.keys(dictionary).sort((a, b) => {
  871. return dictionary[b][1] - dictionary[a][1];
  872. }).map(key => {
  873. return [
  874. (new Date(dictionary[key][2])).toLocaleString(),/* used */
  875. dictionary[key][1],/* count */
  876. key,/* original */
  877. dictionary[key][0],/* translation */
  878. ];
  879. }).slice(0,100);
  880. };
  881. },
  882. getTargets: function(targets, retry = 0){
  883. const get = function(resolve, reject, retry){
  884. for(let i = 0, keys = Object.keys(targets), key; key = keys[i]; i++){
  885. let selected = targets[key]();
  886. if(selected){
  887. if(selected.length) selected.forEach((s) => s.dataset.selector = key);
  888. else selected.dataset.selector = key;
  889. elements[key] = selected;
  890. }else{
  891. if(--retry < 0) return reject(log(`Not found: ${key}, I give up.`));
  892. log(`Not found: ${key}, retrying... (left ${retry})`);
  893. return setTimeout(get, 1000, resolve, reject, retry);
  894. }
  895. }
  896. resolve();
  897. };
  898. return new Promise(function(resolve, reject){
  899. get(resolve, reject, retry);
  900. });
  901. },
  902. addStyle: function(name = 'style'){
  903. let style = createElement(core.html[name]());
  904. document.head.appendChild(style);
  905. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  906. elements[name] = style;
  907. },
  908. html: {
  909. priorDanmaku: () => `<ul id="${SCRIPTNAME}-prior-danmaku" class="translate" translate="yes"></ul>`,
  910. danmakuContent: (content) => `<li>${content}</li>`,
  911. translation: (translation) => `<span class="translation">${translation}</span>`,
  912. style: () => `
  913. <style type="text/css">
  914. /* bilibili color: #00A1D6 */
  915. ul#${SCRIPTNAME}-prior-danmaku{
  916. /* 画面内にないと自動翻訳されない */
  917. visibility: hidden;
  918. position: fixed;
  919. top: 0;
  920. padding: 0;
  921. margin: 0;
  922. white-space: nowrap;
  923. z-index: 9999;
  924. }
  925. ul#${SCRIPTNAME}-prior-danmaku li{
  926. position: absolute;
  927. }
  928. .translation{
  929. font-size: 75%;
  930. display: block;
  931. }
  932. .translating{
  933. opacity: .25;
  934. animation: ${SCRIPTNAME}-blink 250ms step-end infinite;
  935. }
  936. @keyframes ${SCRIPTNAME}-blink{
  937. 50%{opacity: .5}
  938. }
  939. /* 放送終了後の黒背景案内 */
  940. .bilibili-live-player-video-round-counter{
  941. background: transparent;
  942. }
  943. </style>
  944. `,
  945. },
  946. };
  947. const setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, requestAnimationFrame = window.requestAnimationFrame;
  948. const getComputedStyle = window.getComputedStyle, fetch = window.fetch;
  949. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  950. class Storage{
  951. static key(key){
  952. return (SCRIPTNAME) ? (SCRIPTNAME + '-' + key) : key;
  953. }
  954. static save(key, value, expire = null){
  955. key = Storage.key(key);
  956. localStorage[key] = JSON.stringify({
  957. value: value,
  958. saved: Date.now(),
  959. expire: expire,
  960. });
  961. }
  962. static read(key){
  963. key = Storage.key(key);
  964. if(localStorage[key] === undefined) return undefined;
  965. let data = JSON.parse(localStorage[key]);
  966. if(data.value === undefined) return data;
  967. if(data.expire === undefined) return data;
  968. if(data.expire === null) return data.value;
  969. if(data.expire < Date.now()) return localStorage.removeItem(key);
  970. return data.value;
  971. }
  972. static delete(key){
  973. key = Storage.key(key);
  974. delete localStorage.removeItem(key);
  975. }
  976. static saved(key){
  977. key = Storage.key(key);
  978. if(localStorage[key] === undefined) return undefined;
  979. let data = JSON.parse(localStorage[key]);
  980. if(data.saved) return data.saved;
  981. else return undefined;
  982. }
  983. }
  984. const $ = function(s){return document.querySelector(s)};
  985. const $$ = function(s){return document.querySelectorAll(s)};
  986. const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  987. const wait = function(ms){return new Promise((resolve) => setTimeout(resolve, ms))};
  988. const createElement = function(html = '<span></span>'){
  989. let outer = document.createElement('div');
  990. outer.innerHTML = html;
  991. return outer.firstElementChild;
  992. };
  993. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  994. let observer = new MutationObserver(callback.bind(element));
  995. observer.observe(element, options);
  996. return observer;
  997. };
  998. const atLeast = function(min, b){
  999. return Math.max(min, b);
  1000. };
  1001. const atMost = function(a, max){
  1002. return Math.min(a, max);
  1003. };
  1004. const between = function(min, b, max){
  1005. return Math.min(Math.max(min, b), max);
  1006. };
  1007. const toMetric = function(number, decimal = 1){
  1008. switch(true){
  1009. case(number < 1e3 ): return (number);
  1010. case(number < 1e6 ): return (number/1e3 ).toFixed(decimal) + 'K';
  1011. case(number < 1e9 ): return (number/1e6 ).toFixed(decimal) + 'M';
  1012. case(number < 1e12): return (number/1e9 ).toFixed(decimal) + 'G';
  1013. default: return (number/1e12).toFixed(decimal) + 'T';
  1014. }
  1015. };
  1016. const log = function(){
  1017. if(!DEBUG) return;
  1018. let l = log.last = log.now || new Date(), n = log.now = new Date();
  1019. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  1020. //console.log(error.stack);
  1021. console.log(
  1022. SCRIPTNAME + ':',
  1023. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  1024. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  1025. /* :00 */ ':' + line,
  1026. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  1027. /* caller */ (callers[1] || '') + '()',
  1028. ...arguments
  1029. );
  1030. };
  1031. log.formats = [{
  1032. name: 'Firefox Scratchpad',
  1033. detector: /MARKER@Scratchpad/,
  1034. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  1035. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1036. }, {
  1037. name: 'Firefox Console',
  1038. detector: /MARKER@debugger/,
  1039. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  1040. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1041. }, {
  1042. name: 'Firefox Greasemonkey 3',
  1043. detector: /\/gm_scripts\//,
  1044. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  1045. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1046. }, {
  1047. name: 'Firefox Greasemonkey 4+',
  1048. detector: /MARKER@user-script:/,
  1049. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  1050. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1051. }, {
  1052. name: 'Firefox Tampermonkey',
  1053. detector: /MARKER@moz-extension:/,
  1054. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  1055. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  1056. }, {
  1057. name: 'Chrome Console',
  1058. detector: /at MARKER \(<anonymous>/,
  1059. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  1060. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  1061. }, {
  1062. name: 'Chrome Tampermonkey',
  1063. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  1064. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 6,
  1065. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  1066. }, {
  1067. name: 'Chrome Extension',
  1068. detector: /at MARKER \(chrome-extension:/,
  1069. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  1070. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  1071. }, {
  1072. name: 'Edge Console',
  1073. detector: /at MARKER \(eval/,
  1074. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  1075. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  1076. }, {
  1077. name: 'Edge Tampermonkey',
  1078. detector: /at MARKER \(Function/,
  1079. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  1080. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  1081. }, {
  1082. name: 'Safari',
  1083. detector: /^MARKER$/m,
  1084. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  1085. getCallers: (e) => e.stack.split('\n'),
  1086. }, {
  1087. name: 'Default',
  1088. detector: /./,
  1089. getLine: (e) => 0,
  1090. getCallers: (e) => [],
  1091. }];
  1092. log.format = log.formats.find(function MARKER(f){
  1093. if(!f.detector.test(new Error().stack)) return false;
  1094. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  1095. return true;
  1096. });
  1097. const time = function(label){
  1098. if(!DEBUG) return;
  1099. const BAR = '|', TOTAL = 100;
  1100. switch(true){
  1101. case(label === undefined):/* time() to output total */
  1102. let total = 0;
  1103. Object.keys(time.records).forEach((label) => total += time.records[label].total);
  1104. Object.keys(time.records).forEach((label) => {
  1105. console.log(
  1106. BAR.repeat((time.records[label].total / total) * TOTAL),
  1107. label + ':',
  1108. (time.records[label].total).toFixed(3) + 'ms',
  1109. '(' + time.records[label].count + ')',
  1110. );
  1111. });
  1112. time.records = {};
  1113. break;
  1114. case(!time.records[label]):/* time('label') to create and start the record */
  1115. time.records[label] = {count: 0, from: performance.now(), total: 0};
  1116. break;
  1117. case(time.records[label].from === null):/* time('label') to re-start the lap */
  1118. time.records[label].from = performance.now();
  1119. break;
  1120. case(0 < time.records[label].from):/* time('label') to add lap time to the record */
  1121. time.records[label].total += performance.now() - time.records[label].from;
  1122. time.records[label].from = null;
  1123. time.records[label].count += 1;
  1124. break;
  1125. }
  1126. };
  1127. time.records = {};
  1128. core.initialize();
  1129. if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME);
  1130. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址