串接 Twitch 的 API(挑戰題)

來自 mentor-program-5th week4 的挑戰題。

解題方向

這一題的目標是印出「最受歡迎的 200 個實況列表」,會用到的資料是這兩個:

簡單來說就是先取得「遊戲 id」,再利用遊戲 id 去取得「實況資料」這樣子。

不過要注意 Get Streams 一次最多只能抓 100 筆資料,要取得 200 筆的話會變得複雜一點,所以我會建議先寫一個簡單版的,列出 100 筆實況列表就好,接著再來想要怎麼優化。

簡單版

這裡要做的事情很簡單,我們只要做兩件事就好:

  1. 根據輸入去找出遊戲 id
  2. 拿遊戲 id 去找出實況列表

這裡就直接貼原始碼,步驟都寫在註解裡面了:

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
/* 
陽春版,可以取得 100 筆「最受歡迎的實況列表」
*/

// 載入 dotenv(取得環境變數)
require('dotenv').config()
// 發送 request 的模組
const request = require('request')
// API 網址
const BASE_URL = 'https://api.twitch.tv/helix'


// 取得遊戲 id 的 request
function searchGame(name, callback) {
// 沒輸入遊戲名稱
if (!name) {
return console.log('請輸入遊戲名稱')
}
request({
url: `${BASE_URL}/games?name=${name}`,
headers: {
'Authorization': process.env.ACCESS_TOKEN,
'Client-ID': process.env.CLIENT_ID
}
}, callback)
}
// 拿到遊戲 id 的 callback
function handlerGameId(err, res, body) {
// 錯誤處理
if (err) {
return console.log(err)
}
// 拿到遊戲 id 後
const gameId = JSON.parse(body).data[0].id
// 再發 request 取得實況列表
getStreams(gameId, handlerStreams)
}

// 取得實況列表的 request
function getStreams(gameId, callback) {
// 沒輸入遊戲 id
if (!gameId){
return console.log('請輸入遊戲 id')
}
request({
// 一次抓 100 筆
url: `${BASE_URL}/streams?first=100&gamd_id=${gameId}`,
headers: {
'Authorization': process.env.ACCESS_TOKEN,
'Client-ID': process.env.CLIENT_ID
}
}, callback)
}
// 拿到實況列表的 callback
function handlerStreams(err, res, body) {
// 錯誤處理
if (err) {
return console.log(err)
}
// 取出實況資料
const streams = JSON.parse(body).data
// 印出實況主名稱、id
for(let stream of streams) {
console.log('==============')
console.log(`實況主名稱:${stream.user_name}`)
console.log(`實況主 id:${stream.user_id}`)
console.log(`觀看人數:${stream.viewer_count}`)
}
}

/*
node twitch1.js 'League of Legends'
執行流程:
1. 送出請求,取得遊戲 id => searchGame
2. 拿到遊戲 id => handlerGameId (callback)
3. 利用遊戲 id 送出請求,取得實況列表 => getStreams
4. 拿到實況列表,並印出實況主名稱、id => handlerStreams (callback)
*/
searchGame(process.argv[2], handlerGameId)

比較重要的地方在於 執行順序,我們希望的順序是:handlerGameId 拿到 id 後才執行 getStreams

所以 getStreams 一定是在 handlerGameId 裡面被呼叫的,如果你寫在別的地方,結果一定是錯的,可以參考 理解什麼是 race-condition

困難版

簡單版沒有問題的話,接下來要做的事情其實也不複雜,其實就是在發一次請求去找出第二筆實況列表資料這樣子而已。

可能會碰到的一個問題是「要怎麼抓第二筆實況資料?」

關於這個問題,你可以先觀察一下,在拿到第一筆實況資料的時候,資料裡有個 pagination 的欄位:

1
2
3
4
5
6
7
8
9
10
11
{
"data": [
{
"id": "41375541868",
...
},
],
"pagination": {
"cursor": "eyJiIjp7IkN1cnNvciI6ImV5SnpJam8zT0RNMk5TNDBORFF4TlRjMU1UY3hOU3dpWkNJNlptRnNjMlVzSW5RaU9uUnlkV1Y5In0sImEiOnsiQ3Vyc29yIjoiZXlKeklqb3hOVGs0TkM0MU56RXhNekExTVRZNU1ESXNJbVFpT21aaGJITmxMQ0owSWpwMGNuVmxmUT09In19"
}
}

裡面的那個 cursor 就是用來拿下一筆實況資料的時候需要用到的東西,我自己是把它當作一個「記錄點」的意思,或你也可以讀一下 API 文件的說明:

Cursor for backward pagination: tells the server where to start fetching the next set of results, in a multi-page response. The cursor value specified here is from the pagination response field of a prior query.

簡單來說就是告訴後端「從哪裡開始撈資料」的意思啦!我們剛剛不是已經抓了 100 筆嗎?,所以第二次發請求時要加上 cursor 來告訴後端「我要從這裡開始拿資料哦(第 101 筆)」,大概是這樣的概念。

沒問題後,接下來就是思考解題方向:

  1. 送出請求,拿到遊戲 id
  2. 用遊戲 id 請求第一筆實況列表
  3. 拿到實況列表後,把第一筆資料存起來
  4. 用遊戲 id 請求第二筆實況列表,這次要加上 cursor,來從第 101 筆開始抓
  5. 拿到實況列表後,把第二筆資料存起來
  6. 把所有資料印出來

這裡要特別注意我們得新增兩個變數 gameIdallStreams 來儲存「遊戲 id」與「所有實況列表」。

為什麼?因為你在拿到第一筆實況資料的時候,callback 的參數裡是不會有遊戲 id 的,如果要在這個 callback 裡送出第二次請求,就一定要有遊戲 id 的這個值,所以才要建立一個變數來儲存。

至於 allStreams 也一樣,我們每次都只會拿到 100 筆實況資料,所以拿到資料的時候當然得存起來,最後才有辦法湊成 200 筆。

接下來一樣參考原始碼跟註解,應該都寫得蠻清楚的了:

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
// 載入 dotenv(取得環境變數)
require('dotenv').config()
// 發送 request 的模組
const request = require('request')
// API 網址
const BASE_URL = 'https://api.twitch.tv/helix'
// 儲存遊戲 id
let gameId = null
// 儲存所有實況列表資料
let allStreams = []

// 取得遊戲 id 的 request
function searchGame(name, callback) {
// 沒輸入遊戲名稱
if (!name) return console.log('請輸入遊戲名稱')
request({
url: `${BASE_URL}/games?name=${name}`,
headers: {
'Authorization': process.env.ACCESS_TOKEN,
'Client-ID': process.env.CLIENT_ID
}
}, callback)
}
// 拿到遊戲 id 的 callback
function handlerGameId(err, res, body) {
// 錯誤處理
if (err) {
return console.log(err)
}
// 拿到遊戲 id 後,把 id 儲存起來。
gameId = JSON.parse(body).data[0].id
// 接著發 request 取得第一筆實況列表
getFirstStreams(gameId, handlerFirstStreams)
}

// 取得第一筆實況列表的 request
function getFirstStreams(gameId, callback) {
// 沒輸入遊戲 id
if (!gameId){
return console.log('請輸入遊戲 id')
}
request({
// 一次抓 100 筆
url: `${BASE_URL}/streams?first=100&gamd_id=${gameId}`,
headers: {
'Authorization': process.env.ACCESS_TOKEN,
'Client-ID': process.env.CLIENT_ID
}
}, callback)
}
// 拿到第一筆實況列表的 callback
function handlerFirstStreams(err, res, body) {
// 錯誤處理
if (err) {
return console.log(err)
}
// 取出實況資料
const streams = JSON.parse(body).data
// 取得記錄點位置(分頁)
const cursor = JSON.parse(body).pagination.cursor
// 把資料拼起來
allStreams = allStreams.concat(streams)
// 再發一次請求,取得第二筆實況列表
getSecondStreams(gameId, cursor, handlerSecondStreams)
}
// 取得第二筆實況列表的 request
function getSecondStreams(gameId, cursor, callback) {
// 沒有輸入 id 或 記錄點
if (!gameId || !cursor) return console.log('請輸入 id 及 記錄點')
request({
url: `${BASE_URL}/streams?after=${cursor}&first=100&gamd_id=${gameId}`,
headers: {
'Authorization': process.env.ACCESS_TOKEN,
'Client-ID': process.env.CLIENT_ID
}
}, callback)
}
// 取得第二筆實況列表的 callback
function handlerSecondStreams(err, res, body) {
// 錯誤處理
if (err) {
return console.log(err)
}
// 取出實況資料
const streams = JSON.parse(body).data
// 把資料拼起來(這時候就有 200 筆資料了)
allStreams = allStreams.concat(streams)
// 印出實況主名稱、id 和觀看人數
for(let stream of allStreams) {
console.log('=============')
console.log(`實況主:${stream.user_name}`)
console.log(`id:${stream.user_id}`)
console.log(`觀看人數:${stream.viewer_count}`)
}
}



/*
執行流程:
1. 發出請求,取得遊戲 id => searchGame
2. 拿到遊戲 id 後 => handlerGameId (callback)
3. 利用遊戲 id 發出請求,取得第一筆實況列表 => getFirstStreams
4. 拿到第一筆實況列表,把資料跟記憶點儲存起來 => handlerFirstStreams (callback)
5. 發出請求,取得第二筆實況列表 => getSecondStreams
6. 拿到第二筆實況列表,把資料儲存起來,最後印出內容 => handlerSecondStreams (callback)
*/
searchGame(process.argv[2], handlerGameId)

一樣要注意執行順序,只要是非同步的 function,結果都一定要用 callback 來處理。

困難版(優化)

這裡是我慢慢修出來的,我也還不知道怎麼解釋比較好,所以請直接看註解:

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
// 載入 dotenv(取得環境變數)
require('dotenv').config()
// 發送 request 的模組
const request = require('request')
// API 網址
const BASE_URL = 'https://api.twitch.tv/helix'
// 一次要抓的數量
const BATCH_LIMIT = 50
// 全部要抓的數量
const TOTAL = 200

// 取得遊戲 id 的 request
function searchGame(name, callback) {
// 沒輸入遊戲名稱
if (!name) return console.log('請輸入遊戲名稱')
request({
url: `${BASE_URL}/games?name=${name}`,
headers: {
'Authorization': process.env.ACCESS_TOKEN,
'Client-ID': process.env.CLIENT_ID
}
}, callback)
}
// 拿到遊戲 id 的 callback
function handlerGameId(err, res, body) {
// 錯誤處理
if (err) {
return console.log(err)
}
// 拿到遊戲 id 後,把 id 儲存起來。
const gameId = JSON.parse(body).data[0].id
// 取得所有實況列表,拿到後印出資料
getAllStreams(gameId, BATCH_LIMIT, TOTAL, handlerAllStream)
}


/*
取得所有實況列表
gameId: 遊戲 id
limit: 一次抓幾筆資料
total: 總共有幾筆資料
callback: 成功的話會拿到所有實況列表的資料

備註:
這個 function 的用意其實是把 getStreams 跟 handlerStreams 給包裝起來,
這樣就可以把需要用到的變數都綁在裡面,例如 gameId、allStreams,
就不用再宣告成全域變數了
*/
function getAllStreams(gameId, limit, total, callback) {
// 儲存所有實況列表資料
let allStreams = []
// 拿到部分實況資料的 callback,會重複使用所以才寫成命名函式
function handlerStreams(err, res, body) {
// 錯誤處理
if (err) {
return callback(err)
}
// 取出實況資料
const streams = JSON.parse(body).data
// 取得記錄點位置(分頁)
const cursor = JSON.parse(body).pagination.cursor
// 把資料拼起來
allStreams = allStreams.concat(streams)
// 還沒抓夠,在發一次請求,這次加上 cursor
if (allStreams.length < total) {
// 遞迴 handlerStreams,直到數量抓夠為止
return getStreams(gameId, cursor, limit, handlerStreams)
}
// 夠了的話,切割成正確的數量在回傳,不然可能會有超過的問題
// 第一個參數是代表 error,後面才是真正要傳回去的資料
return callback(null, allStreams.slice(0, total))
}
// 抓第一次資料
// 這個還沒有記憶點所以 cursor 傳入 null
getStreams(gameId, null, limit, handlerStreams)
}

// 拿到所有實況列表後的 callback
function handlerAllStream(err, streams) {
// 錯誤處理
if (err) {
return console.log(err)
}
// 印出實況資訊
for (let stream of streams) {
console.log('==============')
console.log(`實況主名稱:${stream.user_name}`)
console.log(`實況主 id:${stream.user_id}`)
console.log(`觀看人數:${stream.viewer_count}`)
}
}


// 取得部分實況列表的 request
function getStreams(gameId, cursor, limit, callback) {
// 預設的 url
let url = `${BASE_URL}/streams?first=${limit}&gamd_id=${gameId}`
// 沒輸入遊戲 id
if (!gameId){
return console.log('請輸入遊戲 id')
}
// 如果有傳入記憶點,url 加上 after 參數
if (cursor) {
url += `&after=${cursor}`
}
request({
url: url,
headers: {
'Authorization': process.env.ACCESS_TOKEN,
'Client-ID': process.env.CLIENT_ID
}
}, callback)
}



/*
執行流程:
1. 發出請求,取得遊戲 id => searchGame
2. 拿到遊戲 id 後 => handlerGameId (callback)
3. 利用遊戲 id 發出請求,取得所有的實況列表 => getAllStreams
4. 拿到所有實況列表後,印出實況資訊 => handlerAllStreams (callback)
*/
searchGame(process.argv[2], handlerGameId)

mentor-program-day28 mentor-program-day27
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×