├── .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 | 
3 | 
4 | 
5 |
6 | Demo预览
7 |
8 | 
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 | 
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 |
--------------------------------------------------------------------------------