Browse Source

first commit

Gogs 4 năm trước cách đây
commit
6f3c7c1ba5

+ 69 - 0
README.md

@@ -0,0 +1,69 @@
+# 哔哩哔哩番剧繁体字幕翻译
+## 服务端环境要求
+- Linux (glibc >= 2.18, libstdc++.so >= 6.0.22) / Windows
+- Python 3
+- MySQL
+
+## 客户端环境要求
+- Windows / Linux Desktop
+- Chrome或其他浏览器
+- Tampermonkey(可选) [首页](https://www.tampermonkey.net/)
+
+## 下载文件
+```
+git clone https://git.iaun.cn/bili_t2s
+```
+## 服务端
+
+### 服务端配置
+- 导入db文件(db/bili_subtitle.sql)
+- 修改db配置项(db_pool/db_config.py)
+
+### 服务端运行
+```
+cd bili_t2s
+pip install -r requirements.txt
+python index.py
+```
+
+### 服务端测试
+```
+curl http://127.0.0.1:8001/bili_translate_api/get/424718
+```
+<b>若要配合客户端请做反向代理并添加跨域请求头,并配置HTTPS</b>
+## 客户端【自动】(需要tampermonkey)
+### 配置tampermonkey
+1. 打开tampermonkey
+<br/>
+![图片](img/001.jpg)
+2. 选择实用工具
+<br/>
+![图片](img/002.jpg)
+3. 导入脚本(tempermonkey-backup-chrome.txt)
+<br/>
+![图片](img/003.jpg)
+<br/>
+![图片](img/004.jpg)
+4. 查看导入的脚本
+<br/>
+![图片](img/005.jpg)
+<br/>
+![图片](img/006.jpg)
+5. 修改请求的域名为服务端地址
+<br/>
+![图片](img/007.jpg)
+
+## 客户端【手动】
+- 替换以下代码中的bili.example.com为您的服务器地址
+- 并在每次到浏览器开发者工具中运行
+![图片](img/008.jpg)
+<br/>
+```
+(function(){DOMAIN="https://bili.example.com";try{$(".squirtle-subtitle-show-state")[0].click()}catch{};var st=$(".squirtle-subtitle-item-text")[0];var video=$(".bpx-player-video-wrap > video")[0];var eid=location.pathname.split('/').pop().slice(2);var timer=0;var subtitleData=[];st.innerText="";st.style.display="block";function subtitle(){var ct=video.currentTime;var display=false;for(var i=0,j=subtitleData.length;i<j;i++){var temp=subtitleData[i];if(ct>=temp['from']&&ct<=temp['to']){st.innerText=temp['content'];st.style.display="block";display=true;break}}if(display===false){st.innerText="";st.style.display="none"}}video.onplaying=function(){timer=setInterval(function(){subtitle()},100)};video.onwaiting=function(){clearInterval(timer)};video.onpause=video.onwaiting;if(!video.paused){video.onplaying()}$.ajax({url:DOMAIN+'/bili_translate_api/get/'+eid,success:function(data){if(data&&data['code']===200&&data['data']){subtitleData=data['data'];console.log("字幕已加载")}}})})();
+```
+
+## 我没有服务器 w(゚Д゚)w
+- 现成服务器地址(仅供试用)
+```
+https://bili.iaun.cn
+```

+ 25 - 0
db/bili_subtitle.sql

@@ -0,0 +1,25 @@
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET NAMES utf8 */;
+/*!50503 SET NAMES utf8mb4 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+
+-- 导出 bili_subtitle 的数据库结构
+CREATE DATABASE IF NOT EXISTS `bili_subtitle` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
+USE `bili_subtitle`;
+
+-- 导出  表 bili_subtitle.subtitle 结构
+CREATE TABLE IF NOT EXISTS `subtitle` (
+  `ep_id` int(11) NOT NULL,
+  `subtitle` text,
+  PRIMARY KEY (`ep_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 数据导出被取消选择。
+
+/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
+/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;

+ 36 - 0
db_pool/db_config.py

@@ -0,0 +1,36 @@
+# -*- coding: UTF-8 -*-
+import pymysql
+
+# 数据库信息
+DB_TEST_HOST = "127.0.0.1"
+DB_TEST_PORT = 3306
+DB_TEST_DBNAME = "bili_subtitle"
+DB_TEST_USER = ""
+DB_TEST_PASSWORD = ""
+
+# 数据库连接编码
+DB_CHARSET = "utf8mb4"
+
+# mincached : 启动时开启的闲置连接数量(缺省值 0 开始时不创建连接)
+DB_MIN_CACHED = 2
+
+# maxcached : 连接池中允许的闲置的最多连接数量(缺省值 0 代表不闲置连接池大小)
+DB_MAX_CACHED = 2
+
+# maxshared : 共享连接数允许的最大数量(缺省值 0 代表所有连接都是专用的)如果达到了最大数量,被请求为共享的连接将会被共享使用
+DB_MAX_SHARED = 2
+
+# maxconnecyions : 创建连接池的最大数量(缺省值 0 代表不限制)
+DB_MAX_CONNECYIONS = 5
+
+# blocking : 设置在连接池达到最大数量时的行为(缺省值 0 或 False 代表返回一个错误<toMany......> 其他代表阻塞直到连接数减少,连接被分配)
+DB_BLOCKING = True
+
+# maxusage : 单个连接的最大允许复用次数(缺省值 0 或 False 代表不限制的复用).当达到最大数时,连接会自动重新连接(关闭和重新打开)
+DB_MAX_USAGE = 0
+
+# setsession : 一个可选的SQL命令列表用于准备每个会话,如["set datestyle to german", ...]
+DB_SET_SESSION = None
+
+# creator : 使用连接数据库的模块
+DB_CREATOR = pymysql

+ 62 - 0
db_pool/db_dbutils_init.py

@@ -0,0 +1,62 @@
+from dbutils.pooled_db import PooledDB
+import db_pool.db_config as config
+
+"""
+@功能:创建数据库连接池
+"""
+
+
+class MyConnectionPool(object):
+    __pool = None
+
+    # def __init__(self):
+    #     self.conn = self.__getConn()
+    #     self.cursor = self.conn.cursor()
+
+    # 创建数据库连接conn和游标cursor
+    def __enter__(self):
+        self.conn = self.__getconn()
+        self.cursor = self.conn.cursor()
+
+    # 创建数据库连接池
+    def __getconn(self):
+        if self.__pool is None:
+            self.__pool = PooledDB(
+                creator=config.DB_CREATOR,
+                mincached=config.DB_MIN_CACHED,
+                maxcached=config.DB_MAX_CACHED,
+                maxshared=config.DB_MAX_SHARED,
+                maxconnections=config.DB_MAX_CONNECYIONS,
+                blocking=config.DB_BLOCKING,
+                maxusage=config.DB_MAX_USAGE,
+                setsession=config.DB_SET_SESSION,
+                host=config.DB_TEST_HOST,
+                port=config.DB_TEST_PORT,
+                user=config.DB_TEST_USER,
+                passwd=config.DB_TEST_PASSWORD,
+                db=config.DB_TEST_DBNAME,
+                use_unicode=False,
+                charset=config.DB_CHARSET
+            )
+        return self.__pool.connection()
+
+    # 释放连接池资源
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.cursor.close()
+        self.conn.close()
+
+    # 关闭连接归还给链接池
+    # def close(self):
+    #     self.cursor.close()
+    #     self.conn.close()
+
+    # 从连接池中取出一个连接
+    def getconn(self):
+        conn = self.__getconn()
+        cursor = conn.cursor()
+        return cursor, conn
+
+
+# 获取连接池,实例化
+def get_my_connection():
+    return MyConnectionPool()

+ 182 - 0
db_pool/mysqlhelper.py

@@ -0,0 +1,182 @@
+from db_pool.db_dbutils_init import get_my_connection
+
+"""执行语句查询有结果返回结果没有返回0;增/删/改返回变更数据条数,没有返回0"""
+
+
+class MySqLHelper(object):
+    def __init__(self):
+        self.db = get_my_connection()  # 从数据池中获取连接
+
+    def __new__(cls, *args, **kwargs):
+        if not hasattr(cls, 'inst'):  # 单例
+            cls.inst = super(MySqLHelper, cls).__new__(cls, *args, **kwargs)
+        return cls.inst
+
+    # 封装执行命令
+    def execute(self, sql, param=None, autoclose=False):
+        """
+        【主要判断是否有参数和是否执行完就释放连接】
+        :param sql: 字符串类型,sql语句
+        :param param: sql语句中要替换的参数"select %s from tab where id=%s" 其中的%s就是参数
+        :param autoclose: 是否关闭连接
+        :return: 返回连接conn和游标cursor
+        """
+        cursor, conn = self.db.getconn()  # 从连接池获取连接
+        count = 0
+        try:
+            # count : 为改变的数据条数
+            if param:
+                count = cursor.execute(sql, param)
+            else:
+                count = cursor.execute(sql)
+            conn.commit()
+            if autoclose:
+                self.close(cursor, conn)
+        except Exception as e:
+            pass
+        return cursor, conn, count
+
+    # 执行多条命令
+    # def executemany(self, lis):
+    #     """
+    #     :param lis: 是一个列表,里面放的是每个sql的字典'[{"sql":"xxx","param":"xx"}....]'
+    #     :return:
+    #     """
+    #     cursor, conn = self.db.getconn()
+    #     try:
+    #         for order in lis:
+    #             sql = order['sql']
+    #             param = order['param']
+    #             if param:
+    #                 cursor.execute(sql, param)
+    #             else:
+    #                 cursor.execute(sql)
+    #         conn.commit()
+    #         self.close(cursor, conn)
+    #         return True
+    #     except Exception as e:
+    #         print(e)
+    #         conn.rollback()
+    #         self.close(cursor, conn)
+    #         return False
+
+    # 释放连接
+    def close(self, cursor, conn):
+        """释放连接归还给连接池"""
+        cursor.close()
+        conn.close()
+
+    # 查询所有
+    def selectall(self, sql, param=None):
+        try:
+            cursor, conn, count = self.execute(sql, param)
+            res = cursor.fetchall()
+            return res
+        except Exception as e:
+            print(e)
+            self.close(cursor, conn)
+            return count
+
+    # 查询单条
+    def selectone(self, sql, param=None):
+        try:
+            cursor, conn, count = self.execute(sql, param)
+            res = cursor.fetchone()
+            self.close(cursor, conn)
+            return res
+        except Exception as e:
+            print("error_msg:", e.args)
+            self.close(cursor, conn)
+            return count
+
+    # 增加
+    def insertone(self, sql, param):
+        try:
+            cursor, conn, count = self.execute(sql, param)
+            # _id = cursor.lastrowid()  # 获取当前插入数据的主键id,该id应该为自动生成为好
+            conn.commit()
+            self.close(cursor, conn)
+            return count
+            # 防止表中没有id返回0
+            # if _id == 0:
+            #     return True
+            # return _id
+        except Exception as e:
+            print(e)
+            conn.rollback()
+            self.close(cursor, conn)
+            return count
+
+    # 增加多行
+    def insertmany(self, sql, param):
+        """
+        :param sql:
+        :param param: 必须是元组或列表[(),()]或((),())
+        :return:
+        """
+        cursor, conn, count = self.db.getconn()
+        try:
+            cursor.executemany(sql, param)
+            conn.commit()
+            return count
+        except Exception as e:
+            print(e)
+            conn.rollback()
+            self.close(cursor, conn)
+            return count
+
+    # 删除
+    def delete(self, sql, param=None):
+        try:
+            cursor, conn, count = self.execute(sql, param)
+            self.close(cursor, conn)
+            return count
+        except Exception as e:
+            print(e)
+            conn.rollback()
+            self.close(cursor, conn)
+            return count
+
+    # 更新
+    def update(self, sql, param=None):
+        try:
+            cursor, conn, count = self.execute(sql, param)
+            conn.commit()
+            self.close(cursor, conn)
+            return count
+        except Exception as e:
+            print(e)
+            conn.rollback()
+            self.close(cursor, conn)
+            return count
+
+
+#if __name__ == '__main__':
+    #db = MySqLHelper()
+    # # 查询单条
+    # sql1 = 'select * from userinfo where name=%s'
+    # args = 'python'
+    # ret = db.selectone(sql=sql1, param=args)
+    # print(ret)  # (None, b'python', b'123456', b'0')
+    # 增加单条
+    # sql2 = 'insert into userinfo (name,password) VALUES (%s,%s)'
+    # ret = db.insertone(sql2, ('old2','22222'))
+    # print(ret)
+    # 增加多条
+    # sql3 = 'insert into userinfo (name,password) VALUES (%s,%s)'
+    # li = li = [
+    #     ('分省', '123'),
+    #     ('到达','456')
+    # ]
+    # ret = db.insertmany(sql3,li)
+    # print(ret)
+    # 删除
+    # sql4 = 'delete from  userinfo WHERE name=%s'
+    # args = 'xxxx'
+    # ret = db.delete(sql4, args)
+    # print(ret)
+    # 更新
+    # sql5 = r'update userinfo set password=%s WHERE name LIKE %s'
+    # args = ('993333993', '%old%')
+    # ret = db.update(sql5, args)
+    # print(ret)

BIN
img/001.jpg


BIN
img/002.jpg


BIN
img/003.jpg


BIN
img/004.jpg


BIN
img/005.jpg


BIN
img/006.jpg


BIN
img/007.jpg


BIN
img/008.jpg


+ 66 - 0
index.py

@@ -0,0 +1,66 @@
+from db_pool.mysqlhelper import MySqLHelper
+import flask
+import requests
+import json
+import opencc
+
+db = MySqLHelper()
+app = flask.Flask(__name__, static_folder='static',
+                  static_url_path='/bili_translate_api/static/')
+converter = opencc.OpenCC('tw2sp.json')
+
+
+@app.route('/bili_translate_api/get/<eid>')
+def get_translate(eid):
+    if not eid:
+        return flask.jsonify({"code": -1, "msg": "未输入参数"})
+    data = db.selectone(
+        "select subtitle from subtitle where ep_id=%s", eid)
+    if not data:  # 在数据库找不到保存的字幕
+        # 尝试检索字幕
+        r = requests.get(
+            'https://api.bilibili.com/pgc/view/web/season?ep_id=%s' % eid)
+        r_data = json.loads(r.text)
+        if r_data['code'] != 0:
+            return flask.jsonify({"code": -1, "msg": "搜索不到字幕"})
+        found = 0
+        aid = 0
+        cid = 0
+        sid = str(r_data['result']['season_id'])
+        for i in r_data['result']['episodes']:
+            if str(i['id']) == str(eid):
+                found = 1
+                aid = str(i['aid'])
+                cid = str(i['cid'])
+                break
+        if not found:
+            return flask.jsonify({"code": -1, "msg": "搜索不到字幕"})
+        r = requests.get(
+            'https://api.bilibili.com/x/player/v2?aid=%s&cid=%s&ep_id=%s&season_id=%s' % (aid, cid, eid, sid))
+        r_data = json.loads(r.text)
+        if r_data['code'] != 0:
+            return flask.jsonify({"code": -1, "msg": "搜索不到字幕"})
+        zht_exist = 0
+        zht_url = None
+        for i in r_data['data']['subtitle']['subtitles']:
+            if i['lan'] == 'zh-Hant':
+                zht_exist = 1
+                zht_url = "https:"+i['subtitle_url']
+        if not zht_exist:
+            return flask.jsonify({"code": -1, "msg": "搜索不到字幕"})
+        r = requests.get(zht_url)  # 请求繁体字幕
+        r_data = json.loads(r.text)
+        if not r_data['body']:
+            return flask.jsonify({"code": -1, "msg": "搜索不到字幕"})
+        for i in r_data['body']:
+            i['content'] = converter.convert(i['content'])
+        db.insertone("insert into subtitle values (%s, %s)",
+                     (eid, json.dumps(r_data['body'])))  # 写入数据库缓存
+        return flask.jsonify({"code": 200, "data": r_data['body']})
+    else:
+        d = json.loads(str(data[0], encoding='utf8'))
+        return flask.jsonify({"code": 200, "data": d})
+
+
+if __name__ == "__main__":
+    app.run('127.0.0.1', 8001)

+ 5 - 0
requirements.txt

@@ -0,0 +1,5 @@
+DBUtils==2.0.2
+Flask==2.0.1
+PyMySQL==1.0.2
+requests==2.26.0
+OpenCC==1.1.2

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
static/jquery-3.6.0.min.js


+ 92 - 0
static/waitForKeyElements.js

@@ -0,0 +1,92 @@
+/*--- waitForKeyElements():  A utility function, for Greasemonkey scripts,
+    that detects and handles AJAXed content.
+
+    Usage example:
+
+        waitForKeyElements (
+            "div.comments"
+            , commentCallbackFunction
+        );
+
+        //--- Page-specific function to do what we want when the node is found.
+        function commentCallbackFunction (jNode) {
+            jNode.text ("This comment changed by waitForKeyElements().");
+        }
+
+    IMPORTANT: This function requires your script to have loaded jQuery.
+*/
+function waitForKeyElements (
+    selectorTxt,    /* Required: The jQuery selector string that
+                        specifies the desired element(s).
+                    */
+    actionFunction, /* Required: The code to run when elements are
+                        found. It is passed a jNode to the matched
+                        element.
+                    */
+    bWaitOnce,      /* Optional: If false, will continue to scan for
+                        new elements even after the first match is
+                        found.
+                    */
+    iframeSelector  /* Optional: If set, identifies the iframe to
+                        search.
+                    */
+) {
+    var targetNodes, btargetsFound;
+
+    if (typeof iframeSelector == "undefined")
+        targetNodes     = $(selectorTxt);
+    else
+        targetNodes     = $(iframeSelector).contents ()
+                                           .find (selectorTxt);
+
+    if (targetNodes  &&  targetNodes.length > 0) {
+        btargetsFound   = true;
+        /*--- Found target node(s).  Go through each and act if they
+            are new.
+        */
+        targetNodes.each ( function () {
+            var jThis        = $(this);
+            var alreadyFound = jThis.data ('alreadyFound')  ||  false;
+
+            if (!alreadyFound) {
+                //--- Call the payload function.
+                var cancelFound     = actionFunction (jThis);
+                if (cancelFound)
+                    btargetsFound   = false;
+                else
+                    jThis.data ('alreadyFound', true);
+            }
+        } );
+    }
+    else {
+        btargetsFound   = false;
+    }
+
+    //--- Get the timer-control variable for this selector.
+    var controlObj      = waitForKeyElements.controlObj  ||  {};
+    var controlKey      = selectorTxt.replace (/[^\w]/g, "_");
+    var timeControl     = controlObj [controlKey];
+
+    //--- Now set or clear the timer as appropriate.
+    if (btargetsFound  &&  bWaitOnce  &&  timeControl) {
+        //--- The only condition where we need to clear the timer.
+        clearInterval (timeControl);
+        delete controlObj [controlKey]
+    }
+    else {
+        //--- Set a timer, if needed.
+        if ( ! timeControl) {
+            timeControl = setInterval ( function () {
+                    waitForKeyElements (    selectorTxt,
+                                            actionFunction,
+                                            bWaitOnce,
+                                            iframeSelector
+                                        );
+                },
+                300
+            );
+            controlObj [controlKey] = timeControl;
+        }
+    }
+    waitForKeyElements.controlObj   = controlObj;
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
tampermonkey-backup-chrome.txt


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác