0%

Puppeteer & Twitch掛台領取忠誠點數

Puppeteer,由 Google 出品的 Node Library,可以用來控制瀏覽器和模擬使用者行為,不管用來寫爬蟲或測試程式都是不可多得的利器;而在這次學習案例中,則是以 Twitch 為研究目標,來開發個模擬使用者看台的輔助程式。

Puppeteer

學習心得就是簡單好用,尤其是結合 Chrome 的 Headless 功能,簡直讓我驚為天人。

那到底 Puppeteer 要做什麼事呢?首先把要模擬的行為先簡單列出來,仔細一看原來也就一項而已。

UserPuppeteer
開啟某個實況主頻道開啟某個 Url
觀看

Twitch-watcher

而這樣的一支程式已經有人寫好了,專案名稱就叫 Twitch-watcher,這個專案讓我學習到很多東西,實在是非常感謝該作者。

專案目標

我這邊要做的,就是以現有的 Twitch-watcher 加入自己的需求進行客製化,以下列出這次的目標:

  • 指令控制
  • 列出追隨頻道
  • 觀看多個頻道
  • 關台偵測
  • 領取特殊額外獎勵
  • 計算忠誠點數

實作-指令控制

使用 readline 重新設計使用者操作流程,並加入 help(幫助)、list(列出追隨頻道)、watch(開始觀看指定頻道)、state(查詢觀看狀態)、leave(離開指定頻道)、exit(離開程式)。

1
2
3
4
5
6
7
8
const readline = require("readline");

process.stdin.setEncoding("utf-8");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: "> ",
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rl.on("line", async (input) => {
const source = input.trim().split(" ");
const command = source[0];
const args = source[1];
rl.pause();
switch (command) {
default:
break;
case "help":
await output.help();
break;
...
...
}
rl.resume();
rl.prompt();
});

實作-列出追隨頻道

因為原專案是列出 Valorant 的相關頻道,所以只要修改取得頻道的網址和 Query 就 OK 了。

1
2
3
const streamersUrl =
process.env.streamersUrl || "https://www.twitch.tv/directory/following/live";
const channelsQuery = 'a[data-a-target="preview-card-image-link"]';

實作-觀看多個頻道

配合前面的 watch 指令重新設計了一個方法(watch)來接收頻道資料,每個觀看任務都有獨立的 Browser 和 Page,初始化完成後開始觀看任務(loopWatch)。

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (command) {
...
...
case "watch":
if (args && streamers[args] != null && !streamers[args].isRun) {
await watch(streamers[args]);
} else {
await output.help();
}
break;
...
...
}
1
2
3
4
5
6
7
8
async function watch(streamer) {
let browser = await spawnBrowser();
let page = await spawnPage(browser);
...
...
await initialWatchPage(streamer);
loopWatch(streamer);
}

實作-關台偵測

每小時重整一次頁面,當偵測到頻道離線時,停止任務並關閉瀏覽器(killStream)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function loopWatch(streamer) {
while (streamer.isRun) {
if (dayjs(streamer.reload).isBefore(dayjs())) {
await streamer.page.reload({ waitUntil: ["networkidle0"] });
...
...
let channelOfflineDiv = await queryOnWebsite(page, streamOfflineQuery);
if (channelOfflineDiv.length > 0) {
await killStream(streamer);
return;
}
}
await claimPoint(streamer);
}
}

實作-領取特殊額外獎勵

偵測領取按鈕,每秒執行一次,直到任務結束;每成功領取一次則計數器(claim)++。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function claimPoint(streamer) {
let reload = dayjs(streamer.reload);
while (!reload.isBefore(dayjs())) {
if (streamer.isRun === false) break;
let claimBtn = await queryOnWebsite(streamer.page, claimPointQuery);
if (claimBtn.length > 0) {
await eval.eval(
streamer.page,
`() => document.querySelector('${claimPointQuery}').click()`
);
streamer.claim++;
}
await streamer.page.waitFor(1000);
}
}

實作-計算忠誠點數

任務開始時紀錄初始數值,每次查詢(state)時計算相差多少,簡直不要太簡單。

1
2
3
4
5
6
7
8
9
async function state(token, streamer) {
...
...
let point = await gqlapi.channelPointsContext(token, streamer.login);
let balance = point.data.community.channel.self.communityPoints.balance;
console.log(
`✨ [${streamer.login}] Point: ${streamer.point} ~ ${balance} (claim ${streamer.claim} times)`
);
}

Demo

這次額外學習到的新工具 asciinema,可以非常輕量的錄製 terminal 執行過程並發布成影片,但在 Windows 上只能用 PowerSession 就是了(我自己測試有亂碼問題)。
asciicast