編寫一個谷歌外掛翻譯Udemy+NetFlix字幕

紅茶配綠茶發表於2018-08-28

題外話

說起這個應該是自身的需求和(英文水平有限,很不爽啊,所以決定好好學習英語,但是當務之急還是寫個用用。

準備工作

官方出處(需要科學上網 因為新手,先擼一遍官方教程。

需要原材料如下:

  • 新建資料夾
  • mainfest.json
  • background.js
  • popup.html
  • popup.js
  • options.html
  • options.js

將這些檔案放入資料夾中,那麼你的初始的谷歌外掛已經搭好了,不過裡面沒有任何內容。

  • mainfest.json
  {
    "name": "Getting Started Example", // 外掛名
    "version": "1.0", // 版本
    "description": "Build an Extension!", // 描述
    "permissions": ["declarativeContent", "storage",activeTab], // 授予外掛能訪問到的模組 
    "background": { // 外掛執行時後臺js
      "scripts": ["background.js"],
      "persistent": false
    },
    "page_action": { // 谷歌外掛欄 點選觸發彈窗的提示頁面檔案
      "default_popup": "popup.html",
      "default_icon": { // 外掛圖示
        "16": "images/get_started16.png",
        "32": "images/get_started32.png",
        "48": "images/get_started48.png",
        "128": "images/get_started128.png"
      }
    },
    "options_page": "options.html", // 配置項頁面檔案,點選單獨開一個窗體
    "icons": { // 擴充程式頁面顯示圖示
      "16": "images/get_started16.png",
      "32": "images/get_started32.png",
      "48": "images/get_started48.png",
      "128": "images/get_started128.png"
    },
    "manifest_version": 2
  }
複製程式碼
  • background.js
  chrome.runtime.onInstalled.addListener(function() {
      //外掛安裝完成時的事件觸發,下方執行了一次設定了storage的操作,特別注意到的是谷歌的storage是沙盒內的,不能通過H5的獲取到
      chrome.storage.sync.set({
          color: '#3aa757'
      }, function() {
          console.log('The color is green.');
      });
      chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
          // 這裡定義的是 何時執行popup.js+popup.html的指令碼和作用域,預設情況下 點選彈出窗體執行指令碼
          chrome.declarativeContent.onPageChanged.addRules([{
              conditions: [new chrome.declarativeContent.PageStateMatcher({
                  pageUrl: {
                      hostEquals: 'developer.chrome.com'
                  },
              })],
              actions: [new chrome.declarativeContent.ShowPageAction()]
          }]);
      });
  });
複製程式碼
  • popup.html
<!DOCTYPE html>
<html>
// 這裡的html縮減了很多標籤,實際上我們完全可以用h5,這裡書寫的是點選外掛彈出的頁面, 類似下拉框按鈕樣式,你可以當初一個網頁來寫,一般這裡做開關控制或者配置項

<head>
    <style>
        button {
            height: 30px;
            width: 30px;
            outline: none;
        }
    </style>
</head>

<body>
    <button id="changeColor"></button>
    <script src="popup.js"></script> // 這裡我們匯入彈窗的指令碼
</body>

</html>
複製程式碼
  • popup.js
  let changeColor = document.getElementById('changeColor');
  // 點選彈窗,出現按鈕,這裡執行指令碼,會繫結一個點選改變顏色的事件,將當前頁面顏色改變,如果你的需求不是很多,到這裡我們一個簡單按鈕 已經初步實現一個外掛了(done
  ...
  changeColor.onclick = function(element) {
      let color = element.target.value;
      chrome.tabs.query({
          active: true,
          currentWindow: true
      }, function(tabs) {
          chrome.tabs.executeScript(
              tabs[0].id, {
                  code: 'document.body.style.backgroundColor = "' + color + '";'
              });
      });
  };
複製程式碼
  • options.html
<!DOCTYPE html>
<html>
// 當稍複雜業務時,我們需要新開一個窗體進行一些配置,或者展示頁,這裡options的功能在此,允許匯入外部js指令碼等 這裡實現的是背景顏色切換選擇不同背景顏色

<head>
    <style>
        button {
            height: 30px;
            width: 30px;
            outline: none;
            margin: 10px;
        }
    </style>
</head>

<body>
    <div id="buttonDiv">
    </div>
    <div>
        <p>Choose a different background color!</p>
    </div>
</body>
<script src="options.js"></script>

</html>
複製程式碼
  • options.js
let page = document.getElementById('buttonDiv');
const kButtonColors = ['#3aa757', '#e8453c', '#f9bb2d', '#4688f1'];
// 該檔案是配置的js 作用是點選按鈕選擇不同顏色並存入到外掛的storage中,點選改變顏色頁面顏色隨配置色變化
function constructOptions(kButtonColors) {
    for (let item of kButtonColors) {
        let button = document.createElement('button');
        button.style.backgroundColor = item;
        button.addEventListener('click', function() {
            chrome.storage.sync.set({
                color: item
            }, function() {
                console.log('color is ' + item);
            })
        });
        page.appendChild(button);
    }
}
constructOptions(kButtonColors);
複製程式碼

到此官方的demo結束,在此基礎上我們需要實現翻譯字幕的功能,我們會遇到兩個問題一個是資料儲存和外部js,css引入報csp安全機制禁止的問題.

實際開發

先上效果圖

lynda

配置

│  background.js // 後臺執行檔案
│  end.js // 點選關閉js
│  LICENSE
│  localstorage.js // 模組檔案
│  manifest.json // 配置檔案
│  options.html // 配置頁面
│  options.js // 配置指令碼
│  popup.html // 彈出頁面
│  popup.js // 彈窗指令碼
│  README.md
│  README_zh.md
│  start.js // 點選開始js
│
├─css
│      style.css
│
├─images // 靜態資源
│      get_started128.png
│      get_started16.png
│      get_started32.png
│      get_started48.png
│
├─lib // 依賴
│      jquery-3.1.1.min.js
│      md5.js
│      metro.min.js
│
└─media
        config.png
        download.png
        netflix.png
        show.png
        step1.png
        step2.png
        step3.png
        step4.png
複製程式碼

在開發過程中會報

æ§å¶å°é误ï¼æç»å è½½èæ¬âhttp://evil.example.com/evil.jsâï¼å 为å®è¿åäºä»¥ä¸å
容å®å
¨æ¿ç­æ令ï¼script-src 'self' https://apis.google.com
csp錯誤和匯入外部依賴的步驟

{
  "name": "udemy translate",
  "version": "1.0",
  "description": "udemy translate",
  "background": {
    "scripts": [ // 後臺依賴指令碼在這裡引入
      "background.js",
      "lib/jquery-3.1.1.min.js",
      "lib/md5.js"
    ],
    "persistent": true // 持久化
  },
  "browser_action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "images/get_started16.png",
      "32": "images/get_started32.png",
      "48": "images/get_started48.png",
      "128": "images/get_started128.png"
    }
  },
  "content_scripts": [ // 這裡解決csp安全問題,當需要在某個域下應用相關指令碼和樣式檔案
    {
      "matches": ["https://*.udemy.com/*","https://*.youtube.com/*","https://*.netflix.com/*"],
      "css": ["css/style.css"],
      "js": ["localstorage.js","lib/jquery-3.1.1.min.js","lib/md5.js"]
    }
  ],
  "permissions": [ // 這裡是授予谷歌外掛的許可權
    "http://*/*",
    "https://*/*",
    "tabs",
    "contextMenus",
    "notifications",
    "webRequestBlocking",
    "storage",
    "activeTab",
    "declarativeContent"
  ],
  "options_page": "options.html",
  "icons": {
    "16": "images/get_started16.png",
    "32": "images/get_started32.png",
    "48": "images/get_started48.png",
    "128": "images/get_started128.png"
  },
  "manifest_version": 2
}
複製程式碼

檔案目錄結構剖析 我們執行的順序是

編寫一個谷歌外掛翻譯Udemy+NetFlix字幕

思路,字幕檔案是事實的dom監聽,我們需要定位到相關的頁面節點並捕獲其內容。 之後便是請求翻譯介面來實現翻譯並配置到頁面上,開關定義原字幕的顯示和隱藏。

關鍵popup解析

// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

$(function() {
// popup按鈕事件區域
    let start_btn = document.getElementById('on');
    let end_btn = document.getElementById('off');
    let option_btn = document.getElementById('options');

    chrome.storage.sync.get('currentState', function(data) {
        console.log(data.currentState)
        if (data.currentState == 'off') { // if is off before
            chrome.storage.sync.get('color', function(data) {
                end_btn.style.backgroundColor = data.color;
                end_btn.style.color = 'white';
            });

        } else {
            chrome.storage.sync.get('color', function(data) {
                start_btn.style.backgroundColor = data.color;
                start_btn.style.color = 'white';
            });
            $('#on').click();

        }
    });

    // options event
    option_btn.onclick = function() {
        chrome.tabs.query({
            active: true,
            currentWindow: true
        }, function(tabs) {
            chrome.runtime.openOptionsPage()
        });
    }

    // start event
    start_btn.onclick = function(element) {
        let color = element.target.value;
        // 這裡之前遇到一個storage跨域的問題,最終換成chrome沙盒內的storage,可以通過chrome.storage.sync.get/set 進行讀取,得以解決實現配置儲存的功能
        chrome.storage.sync.get('color', function(data) { // get color property
            resetBtn(end_btn);
            start_btn.style.backgroundColor = data.color;
            start_btn.setAttribute('value', data.color);
            start_btn.style.color = 'white';
        });
        chrome.storage.sync.set({
            currentState: 'on'
        });

        chrome.tabs.query({
            active: true,
            currentWindow: true
        }, function(tabs) { // 這裡我們因為域的問題,我們通過自帶的api引入動態的指令碼來執行我們的開始和關閉指令碼
            chrome.tabs.executeScript(null, {
                file: "lib/jquery-3.1.1.min.js"
            });
            chrome.tabs.executeScript(null, {
                file: "lib/md5.js"
            });
            chrome.tabs.executeScript(null, {
                file: "start.js"
            });
        });
    };


    // end_btn event
    end_btn.onclick = function(element) {
        let color = element.target.value;
        chrome.storage.sync.get('color', function(data) {
            resetBtn(start_btn);
            end_btn.style.backgroundColor = data.color;
            end_btn.setAttribute('value', data.color);
            end_btn.style.color = 'white';

        });

        chrome.storage.sync.set({
            currentState: 'off'
        });
        chrome.tabs.query({
            active: true,
            currentWindow: true
        }, function(tabs) {
            chrome.tabs.executeScript(null, {
                file: "end.js"
            });
        });
    };

    function resetBtn(dom) {
        dom.style.color = '';
        dom.style.backgroundColor = ''
    }

})
複製程式碼

實現功能程式碼

在start.js裡面我們定位到了字幕節點並獲取文字資訊,之後翻譯並送到窗體中。

1.獲取文字資訊

   let typeUrl = window.location.href;
   if (typeUrl.includes('udemy')) {
       if (document.getElementsByClassName('captions-display--vjs-ud-captions-cue-text--38tMf')[0]) {
           var oldSub = document.getElementsByClassName('captions-display--vjs-ud-captions-cue-text--38tMf')[0].outerText;
       }
   } else if (typeUrl.includes('netflix')) {
       if ($('.player-timedtext-text-container').length) {
           var oldSub = '';
           var container = $('.player-timedtext-text-container').find('span');
           for (let i = 0, len = container.length; i < len; i++) {
               oldSub += container.eq(i).html().replace('<br>', ' ').replace('-', '').replace(/\[(.+)\]/, '');
           }
       }
複製程式碼

2.翻譯替換

function youdaoSend(configInfo, apiKey, key, subtitle, md5) { // youdao translate request 
    var apiKey = apiKey;
    var key = key;
    var salt = (new Date).getTime();
    var query = subtitle;
    var from = '';
    var to = configInfo.aimLang == 'undefined' ? 'zh-CHS' : configInfo.aimLang;
    var str1 = apiKey + query + salt + key;
    var sign = md5(str1);
    // console.log(apiKey, key);

    $.ajax({
        url: 'https://openapi.youdao.com/api',
        type: 'post',
        dataType: 'json',
        data: {
            q: query,
            appKey: apiKey,
            salt: salt,
            from: from,
            to: to,
            sign: sign
        },
        success: function(data) {
            if (typeof data.translation == "undefined") {
                chrome.storage.sync.set({
                    currentState: 'off'
                }, function() {
                    console.log('error,reset state')
                });

                return
            }
            let subtitle = typeof data.translation == "undefined" ? '當前配置錯誤,或目標語言相同' : data.translation[0]
            // judge typeUrl
            let typeUrl = window.location.href;
            if (typeUrl.includes('udemy')) {
                var wrapper = $('.vjs-ud-captions-display div').eq(1);
                if (!wrapper.has('h2').length) {
                    wrapper.append(`<div class="zh_sub" style="padding:0 5px 5px 5px;text-align:center;position:relative;top:-12px;background:#4F5155"><h2 style="text-shadow:0.07em 0.07em 0 rgba(0, 0, 0, 0.1);">${subtitle}</h2></div>`)
                } else {
                    wrapper.find('h2').text(subtitle)
                }
            }
            if (typeUrl.includes('netflix')) {
                var wrapper = $('.player-timedtext')
                chrome.storage.sync.set({
                    netflixSubCache: wrapper.html()
                }, function() {
                    console.log('saved')
                });
                if (wrapper.siblings(".zh_sub").length < 1) {
                    wrapper.append(`<div class="zh_sub" 
                    style="padding:0 8px 2px 8px;
                    text-align:center;
                    position:absolute;
                    bottom:10%; 
                    left: 50%;
                    transform: translateX(-50%);
                    background:#4F5155">
                    <h2 style="text-shadow:0.07em 0.07em 0 rgba(0, 0, 0, 0.1);font-size:1.5rem;text-align:center">${subtitle}</h2></div>`)
                } else {
                    $('.zh_sub h2').text(subtitle);
                }
                console.log(subtitle);
            }

        },
        error: function() {
            alert('使用者配置有誤,或當前介面流量已達上限');
        }
    });
}
複製程式碼

這裡的請求做了定時器限制,思路為100ms進行一次dom獲取並比較字幕是否發生改變,改變才得以傳送請求,避免流量超標(

結語

在開發過程中,參考官方文件較多,同時也翻看了大多免費翻譯api貼上地址供有興趣的人蔘考。

chrome標籤操作

web-csp

chrome api

最後附上地址歡迎使用和star

udemy+netflix谷歌翻譯外掛

歷史文章傳送門

?‍?圖片壓縮Canvas

Vue嵌入iframe,iframe如何跨域呼叫vue內路由

記vue下懸浮貼合頂部實現

一加5t ,安卓p系統降級

相關文章