├── .github └── workflows │ ├── actionflow-reset-cache.yml │ ├── actionsflow.yml │ └── clean-workflow-runs.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── ReadMe.md ├── config.js ├── docs ├── cookie.jpg ├── preview.gif └── preview_uploadsubs.gif ├── package.json ├── u2bili.sh ├── upload.js └── workflows └── youtube.yml /.github/workflows/actionflow-reset-cache.yml: -------------------------------------------------------------------------------- 1 | name: Reset subscribe cache 2 | concurrency: 3 | group: actionsflow 4 | permissions: 5 | actions: write 6 | contents: write 7 | on: 8 | workflow_dispatch: 9 | push: 10 | paths: 11 | - 'workflows/youtube.yml' 12 | jobs: 13 | reset: 14 | runs-on: ubuntu-latest 15 | name: Reset Actionsflow Cache 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Run Actionsflow Clean 19 | uses: actionsflow/actionsflow-action@v1 20 | with: 21 | args: clean 22 | json-secrets: ${{ toJSON(secrets) }} -------------------------------------------------------------------------------- /.github/workflows/actionsflow.yml: -------------------------------------------------------------------------------- 1 | name: Daily update 2 | concurrency: 3 | group: actionsflow 4 | permissions: 5 | actions: read 6 | contents: read 7 | on: 8 | schedule: 9 | - cron: "0 4 * * *" # GMT+8 12:00 10 | repository_dispatch: 11 | workflow_dispatch: 12 | inputs: 13 | include: 14 | description: "--include: workflow file filter, you can use glob format to filter your workflows, the default value is empty value, means no filter will be using" 15 | required: false 16 | default: "" 17 | force: 18 | description: "--force: whether force to run workflow, true or false" 19 | required: false 20 | default: "false" 21 | verbose: 22 | description: "--verbose: debug workflow, true or false" 23 | required: false 24 | default: "false" 25 | jobs: 26 | run: 27 | runs-on: ubuntu-latest 28 | name: Run 29 | timeout-minutes: 90 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Run Actionsflow 33 | uses: actionsflow/actionsflow-action@v1 34 | with: 35 | args: "build --include ${{ github.event.inputs.include || ''}} -f ${{github.event.inputs.force=='true' && 'true' || 'false'}} --verbose ${{github.event.inputs.verbose=='true' && 'true' || 'false'}}" 36 | json-secrets: ${{ toJSON(secrets) }} 37 | json-github: ${{ toJSON(github) }} 38 | - name: Setup act 39 | run: | 40 | wget -q https://github.com/nektos/act/releases/download/v0.2.23/act_Linux_x86_64.tar.gz 41 | tar -xf act_Linux_x86_64.tar.gz act 42 | sudo mv ./act /usr/local/bin/act && sudo chmod a+x /usr/local/bin/act 43 | - name: Run act 44 | run: act --workflows ./dist/workflows --secret-file ./dist/.secrets --eventpath ./dist/event.json --env-file ./dist/.env -P ubuntu-20.04=catthehacker/ubuntu:act-22.04-20231101 -P ubuntu-latest=catthehacker/ubuntu:act-22.04-20231101 -------------------------------------------------------------------------------- /.github/workflows/clean-workflow-runs.yml: -------------------------------------------------------------------------------- 1 | name: Clean workflow runs 2 | on: 3 | workflow_dispatch: 4 | permissions: 5 | actions: write 6 | jobs: 7 | run: 8 | runs-on: ubuntu-latest 9 | name: Clean latest 100 actions log 10 | steps: 11 | - uses: actions/github-script@v4 12 | with: 13 | github-token: ${{secrets.GITHUB_TOKEN}} 14 | script: | 15 | const all = await github.actions.listWorkflowRunsForRepo({ 16 | owner: context.repo.owner, 17 | repo: context.repo.repo, 18 | status: "completed", 19 | per_page:100 20 | }); 21 | for (let run of all.data.workflow_runs) { 22 | await github.actions.deleteWorkflowRun({ 23 | owner: context.repo.owner, 24 | repo: context.repo.repo, 25 | run_id: run.id, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | package-lock.json 3 | meta.json 4 | downloads/ 5 | videos/ 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.1.1 20230416 2 | 1. 隐藏上传页操作提示,避免遮挡按钮导致失败 3 | 2. 补充actions所需权限字段 4 | 5 | ## 2.1.0 20221011 6 | 1. 上传视频自动添加中英字幕(如果有) 7 | 2. act环境更新 ubuntu:act-20.04 -> ubuntu:act-22.04 8 | 9 | ## 2.0.0 20220914 10 | 1. 适配新的上传页面 11 | 12 | ## 1.2.0 20220512 13 | 1. youtube-dl 换成 yt-dlp 14 | 15 | ## 1.1.0 20211023 16 | 1. 填坑,定期扫描视频自动上传字幕脚本 17 | 2. 补充上传字幕脚本使用文档 18 | 19 | ## 1.0.0 20210806 20 | 1. 固定act运行的镜像,原catthehacker/ubuntu:act-20.04镜像node被升成了v14.17.4,和playwright存在兼容问题。导致元素找不到。现使用catthehacker/ubuntu:act-20.04-20210721 21 | 2. 不再使用原ActionsFlow的方法检查同时运行中的工作流,详情Issue:https://github.com/actionsflow/actionsflow/issues/28 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kyle Zhou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # youtube->bilibili 搬运脚本 2 | ![STARS](https://img.shields.io/github/stars/ame-yu/u2bili?color=%231cd&style=for-the-badge) 3 | ![LICENSE](https://img.shields.io/github/license/ame-yu/u2bili?style=for-the-badge) 4 | ![LAST_COMMIT](https://img.shields.io/github/last-commit/ame-yu/u2bili?style=for-the-badge) 5 |
6 | Demo预览 7 | 8 | ![Preview](docs/preview.gif) 9 |
10 | 11 | ## 🍔使用 12 | > 预置环境 node16.x+python3+[jq](https://github.com/stedolan/jq) 13 | > 14 | 1. `yarn`或者`npm install`安装node依赖,`npx playwright install`安装playwright 15 | 2. 安装yt-dlp [文档](https://github.com/yt-dlp/yt-dlp#installation) 16 | 3. 提供cookies:通过 .env 提供或设置`BILIBILI_COOKIE`环境变量 17 | 4. ./u2bili.sh \ 18 | 19 |
20 | 关于获取Cookie 21 | 22 | 登录后F12,Application(应用程序)面板,选择cookie进行查看。 23 | ![Cookie](docs/cookie.jpg) 24 | 以下方法选择一个即可: 25 | 26 | - 创建文件 .env,内容格式如下,填入你的变量值即可 27 | 28 | ```shell 29 | DedeUserID: "xxxxxx" 30 | DedeUserID__ckMd5: "xxxxxx" 31 | bili_jct: "xxxxxx" 32 | SESSDATA: "xxxxxx" 33 | ``` 34 | 35 | 注意:虽然 .gitignore 里已经写上了 .env,但还是要提醒一下**一定不要**将 .env 文件 push 到远端,否则有账号被盗的危险。 36 | 37 | - 设置`BILIBILI_COOKIE`环境变量 38 | ``` 39 | BILIBILI_COOKIE环境变量格式如下: 40 | DedeUserID=XXX;DedeUserID__ckMd5=XXX;bili_jct=XXX;SESSDATA=XXX 41 | ``` 42 |
43 | 44 | ## 🍱使用框架 Frameworks 45 | - yt-dlp 46 | - playwright 47 | - actionsflow 48 | 49 | ## 🧂Q&A 50 |
51 | 📺关于下载的清晰度 52 | 53 | 如有装有ffmpeg则会自动选择高画质视频和高画质音频然后合并。 54 | yt-dlp文档 [github.com/yt-dlp/yt-dlp#format-selection](https://github.com/yt-dlp/yt-dlp#format-selection) 55 |
56 | 57 |
58 | 🍥使用Github Action 59 | 60 |

❗重要提示:请clone后push到自己的私有仓库,使用额度内action时间!

61 |
62 | 63 | Actions面板设置Secret `BILIBILI_COOKIE` (必要步骤) 64 | ``` 65 | DedeUserID=XXX;DedeUserID__ckMd5=XXX;bili_jct=XXX;SESSDATA=XXX 66 | ``` 67 | 👆 Cookie有效期大概6个月(多设备异地登录会提前过期,所以建议使用小号,获取Cookie后本地浏览器删除Cookie不再登录) 68 | 69 | 几个重要参数 70 | - 扫描周期`schedule.cron` [.github/workflows/actionsflow.yml](.github/workflows/actionsflow.yml) 71 | - 订阅频道`channel_id` [workflows/youtube.yml](workflows/youtube.yml) 72 | - 视频条目过滤`filterScript` 默认只对比了时间选取24小时内的视频 [workflows/youtube.yml](workflows/youtube.yml) 73 | - [脚本文档](https://actionsflow.github.io/docs/workflow/#ontriggerconfigfilterscript) 74 | - [完整视频参数](https://actionsflow.github.io/docs/triggers/youtube/#outputs) 75 |
76 | 77 | ## ⚠免责声明 78 | 项目仅用于学习参考,如存在违反B站用户协议请使用者风险自负。 79 | 80 | ## 📜Licence 81 | MIT 82 | 83 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import 'dotenv/config' 3 | 4 | /** 5 | * 默认只有Windows系统浏览器可视化, 方便调试和排错 6 | * @type {boolean} */ 7 | export const showBrowser = process.platform === "win32" 8 | 9 | /** 10 | * youtube-dl下载位置。修改则需和u2bili.sh内的下载地址保持一致 11 | * @type {string} 12 | */ 13 | export const downloadPath = "./downloads/" 14 | /** 15 | * ! 必填项,从环境变量中读取或直接填写下面参数, 环境变量优先级高 16 | * 具体如何获取请查看ReadMe.md 17 | * @type {object} 18 | */ 19 | export const bilibiliCookies = { 20 | DedeUserID: process.env.DedeUserID, 21 | DedeUserID__ckMd5: process.env.DedeUserID__ckMd5, 22 | bili_jct: process.env.bili_jct, 23 | SESSDATA: process.env.SESSDATA, 24 | } -------------------------------------------------------------------------------- /docs/cookie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ame-yu/u2bili/c2761feb39f7deb1439d28c9e54bdb7f1d085152/docs/cookie.jpg -------------------------------------------------------------------------------- /docs/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ame-yu/u2bili/c2761feb39f7deb1439d28c9e54bdb7f1d085152/docs/preview.gif -------------------------------------------------------------------------------- /docs/preview_uploadsubs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ame-yu/u2bili/c2761feb39f7deb1439d28c9e54bdb7f1d085152/docs/preview_uploadsubs.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "u2bili", 3 | "version": "2.1.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:ame-yu/u2bili.git" 8 | }, 9 | "homepage": "https://github.com/ame-yu/u2bili", 10 | "type": "module", 11 | "dependencies": { 12 | "dotenv": "^16.0.3", 13 | "playwright": "1.39.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /u2bili.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Set save folder here 4 | downloadPath="./downloads/" 5 | 6 | if [[ $# -eq 0 ]]; then 7 | read -r -p "Youtube video URL: " yturl 8 | else 9 | yturl=$1 10 | fi 11 | 12 | mkdir -p $downloadPath 13 | vid=$(yt-dlp "$yturl" --get-id) 14 | yt-dlp "$yturl" -J > "${downloadPath}$vid.json" || exit 0 # exit if prompt event live error 15 | duration=$(cat "${downloadPath}$vid.json"| jq .duration) 16 | 17 | # Set max duration here, default is 30min 18 | if [ "$duration" -ge 1800 ]; then 19 | echo "Video longer than 30 min,skip..." 20 | exit 0 21 | fi 22 | 23 | set -x # Show following commands 24 | yt-dlp "$yturl" --quiet --write-subs --all-subs --embed-subs --write-thumbnail -o "${downloadPath}%(id)s.%(ext)s" --exec "node upload.js ${downloadPath}$vid.json" -------------------------------------------------------------------------------- /upload.js: -------------------------------------------------------------------------------- 1 | import { bilibiliCookies, showBrowser, downloadPath } from "./config.js" 2 | import { firefox as browserCore } from "playwright" 3 | import { existsSync, readFileSync, writeFileSync } from "fs" 4 | /** 5 | * 使用例 node upload.sh MetaFile [VideoFile] 6 | * MetaFile 是必须的 7 | * 如果视频文件命名和Meta文件一致则可不写 8 | */ 9 | const uploadPageUrl = "https://member.bilibili.com/york/videoup" 10 | 11 | if (process.argv.length < 3) { 12 | console.error( 13 | "至少传入视频信息JSON路径 node upload.js json_file [video_file]" 14 | ) 15 | process.exit(-1) 16 | } 17 | 18 | var [metaPath, videoPath] = process.argv.slice(2, 4) 19 | const meta = JSON.parse(readFileSync(metaPath)) 20 | 21 | function getCookies() { 22 | const envCookies = process.env["BILIBILI_COOKIE"] 23 | if (envCookies) { 24 | console.log("从环境变量读取Cookie") 25 | return envCookies.split(";").map((i) => { 26 | const [key, value] = i.split("=") 27 | return { 28 | domain: ".bilibili.com", 29 | path: "/", 30 | name: key, 31 | value: value, 32 | } 33 | }) 34 | } else { 35 | console.log("从.env读取Cookie") 36 | return Object.keys(bilibiliCookies).map((k) => { 37 | return { 38 | domain: ".bilibili.com", 39 | path: "/", 40 | name: k, 41 | value: bilibiliCookies[k], 42 | } 43 | }) 44 | } 45 | } 46 | 47 | async function main() { 48 | const browser = await browserCore.launch({ 49 | headless: !showBrowser, 50 | }) 51 | const context = await browser.newContext({ 52 | recordVideo: { dir: "videos/" }, 53 | userAgent: 54 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Edg/88.0.705.74", 55 | storageState: { 56 | origins: [ 57 | { 58 | origin: "https://member.bilibili.com", 59 | localStorage: [ 60 | { 61 | name: "SHOW_GUIDE", 62 | value: "1", 63 | }, 64 | { 65 | name: "bili_videoup_submit_auto_tips", 66 | value: "1" 67 | } 68 | ], 69 | }, 70 | ], 71 | }, 72 | }) 73 | context.addCookies(getCookies()) 74 | const page = await context.newPage() 75 | try { 76 | await Promise.all([ 77 | page.goto(uploadPageUrl, { 78 | waitUntil: "networkidle", 79 | timeout: 20 * 1000, 80 | }), 81 | page.waitForResponse(/\/OK/), //Fix:库未加载完的无效点击 82 | ]) 83 | } catch (error) { 84 | if (error.name === "TimeoutError") { 85 | console.error( 86 | `::error file=upload.js::等待上传页面超时! 当前页面:${page.url()}` 87 | ) 88 | } 89 | } 90 | let fileChooser = null 91 | try { 92 | const [chooser] = await Promise.all([ 93 | page.waitForEvent("filechooser", { timeout: 10_000 }), 94 | page.click(".bcc-upload-wrapper") 95 | ]) 96 | fileChooser = chooser 97 | } catch (error) { 98 | if (error.name === "TimeoutError") { 99 | console.error(`::error::点击上传按钮超时! 当前页面:${page.url()}`) 100 | } 101 | } 102 | 103 | if (!videoPath) { 104 | const ext = ["webm", "mp4", "mkv"].find((ext) => 105 | existsSync(`${downloadPath}${meta["id"]}.${ext}`) 106 | ) 107 | if (!ext) { 108 | console.error( 109 | `::error::无法在${downloadPath}找到${meta["id"]}命名的视频文件,上传未成功。` 110 | ) 111 | process.exit(-1) 112 | } 113 | videoPath = `${downloadPath}${meta["id"]}.${ext}` 114 | } 115 | await fileChooser.setFiles(videoPath) 116 | console.log(`开始上传${videoPath}`) 117 | 118 | await page.click('text="转载"') 119 | await page.fill("input[placeholder^=转载视频请注明来源]", meta["webpage_url"]) 120 | 121 | // 选择分区 122 | // await page.click("div.select-box-v2-container") 123 | // await page.click('text="知识"') 124 | // await page.click("div.drop-cascader-list-wrp > div:nth-child(8)") // 修复问题:找不到二级选项导致堵塞,数字对应二级列表位置 125 | 126 | // 创建标签 127 | await page.click("input[placeholder*=创建标签]") 128 | await page.keyboard.type(meta["uploader"]) 129 | await page.keyboard.down("Enter") 130 | 131 | // 视频描述 132 | await page.click("div.ql-editor[data-placeholder^=填写更全]") 133 | await page.keyboard.type(`u2bili自动上传\n${meta["description"]}`) 134 | 135 | await page.fill("input[placeholder*=标题]", meta["title"]) 136 | 137 | await uploadSubtitles(page, meta) 138 | 139 | // 上传封面 140 | try { 141 | const [chooser] = await Promise.all([ 142 | page.waitForEvent("filechooser", { timeout: 10_000 }), 143 | page.click(".cover-upload"), 144 | ]) 145 | await chooser.setFiles(`${downloadPath}${meta["id"]}.webp`) 146 | await page.click('text="完成"') 147 | } catch (error) { 148 | console.error('上传封面失败,使用自动生成的封面', error.message) 149 | await page 150 | .waitForSelector('text="更改封面"', { 151 | timeout: 3 * 60_000, // 等待自动生成封面 152 | }) 153 | .catch(() => { 154 | console.log("等待封面自动生成时间过长") 155 | }) 156 | } 157 | 158 | await page 159 | .waitForSelector('text="上传完成"', { 160 | timeout: 10 * 60_000, // 等待上传完毕 161 | }) 162 | .catch(() => { 163 | console.log("上传时间过长") 164 | }) 165 | 166 | await page.click('text="立即投稿"') 167 | 168 | await page.waitForTimeout(3000) 169 | await page.close() 170 | await context.close() 171 | await browser.close() 172 | } 173 | 174 | async function vtt2srt(path) { 175 | if (!existsSync(path)) return 176 | 177 | let num = 1 178 | const vtt = readFileSync(path, "utf-8") 179 | // 去除头部meta信息,改为逗号分割,增加序号,空行修整 180 | let srt = vtt 181 | .split("\n") 182 | .slice(4) 183 | .join("\n") 184 | .replace( 185 | /(\d{2}:\d{2}:\d{2}.\d{3} --> \d{2}:\d{2}:\d{2}.\d{3})/g, 186 | (match, p1) => { 187 | return `${num++}\n${p1.replaceAll(".", ",")}` 188 | } 189 | ) 190 | .replace(/\n+$/g, "") 191 | 192 | writeFileSync(path, srt) 193 | } 194 | 195 | async function uploadSubtitles(page, meta) { 196 | // 寻找中文字幕和英文字幕 197 | const langCodes = Object.keys(meta["subtitles"]) 198 | const enSub = langCodes.find((code) => code.startsWith("en")) 199 | const zhSub = langCodes.find((code) => code.startsWith("zh-Hans")) 200 | 201 | if (!enSub && !zhSub) return 202 | 203 | await page.click('text="更多设置"') 204 | 205 | await page.click('text="上传字幕"') 206 | 207 | async function selectSub(lang, path) { 208 | if (!existsSync(path)) return 209 | 210 | await page.click(`[placeholder="选择字幕语言"]`) 211 | await page.click(`li:has-text("${lang}")`) 212 | 213 | const [chooser] = await Promise.all([ 214 | page.waitForEvent("filechooser", { timeout: 10_000 }), 215 | page.click(".modal-content-upload button"), 216 | ]) 217 | 218 | await chooser.setFiles(path) 219 | } 220 | 221 | if (zhSub) { 222 | const subPath = `${downloadPath}${meta["id"]}.${zhSub}.vtt` 223 | vtt2srt(subPath) 224 | await selectSub("中文", subPath) 225 | console.log("已添加中文字幕") 226 | } 227 | 228 | if (enSub) { 229 | const subPath = `${downloadPath}${meta["id"]}.${enSub}.vtt` 230 | vtt2srt(subPath) 231 | await selectSub("英语", subPath) 232 | console.log("已添加英文字幕") 233 | } 234 | 235 | await page.click('text="确认"') 236 | 237 | // 即使格式错误也继续上传 238 | await page 239 | .click('text="取消"', { timeout: 1000, delay: 1000 }) 240 | .catch(() => {}) 241 | } 242 | 243 | main() 244 | -------------------------------------------------------------------------------- /workflows/youtube.yml: -------------------------------------------------------------------------------- 1 | on: 2 | youtube: 3 | channel_id: 4 | - UCwXdFgeE9KYzlDdR7TG9cMw #Flutter 5 | - UCsBjURrPoezykLs9EqgamOA #Fireship 6 | - UCP7uiEZIqci43m22KDl0sNw #Kotlin by JetBrains 7 | # 找不到UC开头的ID? channel主页 ctrl+u ctrl+f 搜channel_id= 8 | playlist_id: 9 | - PL0lo9MOBetEFCNnxB1uZcDGcrPO1Jbpz8 #GitHub Changelog 10 | config: 11 | filterScript: | 12 | return new Date() - 24 * 3600 * 1000 < new Date(item.pubDate) //Recent 24 hours Only 13 | jobs: 14 | print: 15 | name: U2bili workflow 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup headless environment 20 | run: | 21 | node -v 22 | corepack enable 23 | apt-get update >> apt.log 24 | apt-get install -y wget ffmpeg python3 >> apt.log 25 | pnpm i 26 | npx -qy playwright install firefox 27 | # Install Chromium dependency 28 | apt-get install -y 'fonts-liberation' 'libasound2' 'libatk-bridge2.0-0' 'libatk1.0-0' 'libatspi2.0-0' 'libcairo2' 'libcups2' 'libdbus-1-3' 'libdrm2' 'libgbm1' 'libgdk-pixbuf2.0-0' 'libglib2.0-0' 'libgtk-3-0' 'libnspr4' 'libnss3' 'libpango-1.0-0' 'libpangocairo-1.0-0' 'libx11-6' 'libx11-xcb1' 'libxcb-dri3-0' 'libxcb1' 'libxcomposite1' 'libxdamage1' 'libxext6' 'libxfixes3' 'libxi6' 'libxrandr2' 'libxtst6' >> apt.log 29 | # Install FireFox dependency 30 | apt-get install -y 'libatk1.0-0' 'libcairo-gobject2' 'libcairo2' 'libdbus-1-3' 'libdbus-glib-1-2' 'libfontconfig1' 'libfreetype6' 'libglib2.0-0' 'libgtk-3-0' 'libgtk2.0-0' 'libharfbuzz0b' 'libpango-1.0-0' 'libpangocairo-1.0-0' 'libpangoft2-1.0-0' 'libx11-6' 'libx11-xcb1' 'libxcb-shm0' 'libxcb1' 'libxcomposite1' 'libxcursor1' 'libxdamage1' 'libxext6' 'libxfixes3' 'libxi6' 'libxrender1' 'libxt6' >> apt.log 31 | # Install latest yt-dlp 32 | curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp 33 | chmod a+rx /usr/local/bin/yt-dlp 34 | - name: u2bili 35 | timeout-minutes: 30 # Set timeout 30min 36 | env: 37 | link: ${{on.youtube.outputs.link}} 38 | BILIBILI_COOKIE: ${{ secrets.BILIBILI_COOKIE }} 39 | run: | 40 | chmod +x ./u2bili.sh && ./u2bili.sh "${link}" 41 | # - name: upload Fail video if failure 42 | # uses: actions/upload-artifact@v3 43 | # if: failure() 44 | # with: 45 | # name: video record 46 | # path: | 47 | # videos/ 48 | --------------------------------------------------------------------------------