0%

自動上傳影片至 youtube 爬蟲

 

最近因為上課的關係, 覺得登入學校平台好麻煩, 更習慣用 youtube 來看影片, 所以把資源用爬蟲備份, 才發現原來 youtube 有提供 youtube data api 來自動化這檔事

首先先到學校網頁爬下 mp4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import puppeteer from "puppeteer";
import fs from "fs";
import https from "https";
import axios from "axios";

(async () => {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: null,
userDataDir: './my-user-data'
});
const page = await browser.newPage();

//進入首頁
await page.goto("https://elearning.nkust.edu.tw/moocs/#/home", {
waitUntil: "networkidle2"
});

//按登入按鈕
await page.click(".action__button-text");

//等燈箱出現
await page.waitForSelector(".mat-dialog-container");

//輸入帳號密碼
await page.type("#account", "帳號", { delay: 50 });
await page.type("#password", "密碼", { delay: 50 });

//按登入
await page.click(".mat-dialog-container button");

//進到課程頁
const page2 = await browser.newPage();
await page2.goto("https://elearning.nkust.edu.tw/moocs/#/learning/10107712");


await page2.waitForSelector("mat-expansion-panel:last-of-type");

//等最後一個章節連結出現
const lastLinkSelector = "mat-expansion-panel:last-of-type mat-expansion-panel-header + div a";
await page2.waitForSelector(lastLinkSelector, { timeout: 10000 });

//點擊影片連結(Puppeteer click 觸發 Angular 事件)
const linkHandle = await page2.$(lastLinkSelector);
if (linkHandle) {
await linkHandle.click();
} else {
console.log("影片連結還沒生成");
return;
}

//等 video source 出現
await page2.waitForSelector("video source", { timeout: 10000 });

//取得影片 src
const videoSrc = await page2.$eval("video source", el => el.src);
const filename = videoSrc.split("/").pop();
console.log("影片連結:", videoSrc);
console.log("檔名:", filename);

const cookies = await page2.cookies();
const cookieString = cookies.map(c => `${c.name}=${c.value}`).join(";");

const headers = {
"Cookie": cookieString,
"Referer": "https://elearning.nkust.edu.tw/moocs/#/learning/10107712",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
};

//下載影片並顯示進度
const response = await axios.get(videoSrc, { headers, responseType: "stream" });

const totalBytes = parseInt(response.headers['content-length'], 10);
let downloaded = 0;

const fileStream = fs.createWriteStream(filename);

response.data.on('data', chunk => {
downloaded += chunk.length;
const percent = ((downloaded / totalBytes) * 100).toFixed(2);
process.stdout.write(`下載進度: ${percent}%\r`);
});

response.data.pipe(fileStream);

fileStream.on("finish", async () => {
console.log(`\n下載完成: ${filename}`);
//瀏覽器在影片下載完成後關閉
await browser.close();
});
})();

這裡要先到 google cloud console => api 和服務 => 憑證 設定 oauth2 用戶端, 他會給你類似下面這樣格式的東東, 請好好保存然後填入等等的程式內

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"installed": {
"client_id": "xxxxx.apps.googleusercontent.com",
"project_id": "xxxx-xxxx-xxxxx",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_secret": "xxxxx",
"redirect_uris": [
"http://localhost"
]
}
}

用 vibe coding 很快就可以生出想要的 code 實在可怕, 第一次執行的時候會 redirect 到 localhost 網址上會有 code, 貼上去就搞定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import fs from "fs";
import readline from "readline";
import { google } from "googleapis";
import path from "path";

const CLIENT_ID = "你的 CLIENT_ID";
const CLIENT_SECRET = "你的 CLIENT_SECRET";
const REDIRECT_URI = "OAuth2 REDIRECT_URI";
const TOKEN_PATH = "token.json";
const PLAYLIST_ID = "播放清單ID";

const oauth2Client = new google.auth.OAuth2(
CLIENT_ID,
CLIENT_SECRET,
REDIRECT_URI
);

// 產生授權網址
function getAuthUrl() {
const authUrl = oauth2Client.generateAuthUrl({
access_type: "offline",
scope: [
"https://www.googleapis.com/auth/youtube.upload",
"https://www.googleapis.com/auth/youtube"
],
});
console.log("第一次使用請打開網址授權:", authUrl);
}

// 儲存 token
async function saveToken(code) {
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens));
console.log("已儲存 token.json,之後可自動上傳影片");
}

// 讀取 token
function loadToken() {
if (fs.existsSync(TOKEN_PATH)) {
const token = JSON.parse(fs.readFileSync(TOKEN_PATH));
oauth2Client.setCredentials(token);
return true;
}
return false;
}

// 上傳影片並加入播放清單
// 假設 uploadVideo 改成接受 mp4 檔案路徑和 title
async function uploadVideo(filePath, videoTitle) {
const youtube = google.youtube({ version: "v3", auth: oauth2Client });
const fileSize = fs.statSync(filePath).size;

const res = await youtube.videos.insert(
{
part: "snippet,status",
requestBody: {
snippet: {
title: videoTitle, // 使用輸入的 title
//description: "這是測試上傳的影片",
//tags: ["測試", "YouTube API"],
//categoryId: "22",
},
status: {
privacyStatus: "unlisted", // 限定公開
selfDeclaredMadeForKids: false, // 非兒童影片
},
},
media: {
body: fs.createReadStream(filePath),
},
},
{
onUploadProgress: (evt) => {
const progress = (evt.bytesRead / fileSize) * 100;
process.stdout.write(`\r上傳進度: ${progress.toFixed(2)}%`);
},
}
);

console.log("\n影片上傳成功! 影片ID:", res.data.id);

await youtube.playlistItems.insert({
part: "snippet",
requestBody: {
snippet: {
playlistId: PLAYLIST_ID,
resourceId: {
kind: "youtube#video",
videoId: res.data.id,
},
},
},
});

console.log("影片已加入播放清單:", PLAYLIST_ID);
}

// 主程式
(async () => {
const folderPath = "./";
const files = fs.readdirSync(folderPath)
.filter(file => path.extname(file).toLowerCase() === ".mp4");

if (files.length === 0) {
throw new Error("資料夾裡沒有 mp4 檔案!");
}
if (files.length > 1) {
throw new Error("資料夾裡有多個 mp4 檔案,無法自動選擇!");
}

const mp4File = path.join(folderPath, files[0]);
console.log("找到的 mp4 檔案:", mp4File);

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

rl.question("請輸入影片標題:", async (title) => {
rl.close();

if (!loadToken()) {
// 第一次授權
getAuthUrl();

const rl2 = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

rl2.question("請貼上授權碼(code):", async (code) => {
await saveToken(code);
rl2.close();
await uploadVideo(mp4File, title);
});
} else {
// 已有 token,自動上傳
await uploadVideo(mp4File, title);
}
});
})();
關閉