YouTube好き集合。Go言語でLIVEコメントをリアルタイム取得する

# はじめに

こんにちは。サーバーサイドエンジニアです。アドベントカレンダーということでクリスマスカラーの赤色にちなんで
赤い動画配信サービスYouTubeのネタです。

YouTubeは言わずと知れた動画配信サービスですが、最近ではLIVE配信によるサービスの盛り上がりを見せています。LIVE配信は動画配信者に対して視聴者がコメントで意思を伝えたりする双方向のコミュニケーションが可能になっています。

そんなLIVE配信のコメントですがAPIを利用して取得することができます。

今回はGo言語を利用して、簡単にLIVE配信のコメントを取得する方法をご紹介します。

![christmas_santa_sori.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540446/055944f6-479f-7202-6470-a7eb4cdaa7ff.png)

# 技術
言語: Go
フレームワーク: Gin
すぺしゃるさんくす: YouTube API

# 準備
YouTube APIのページから事前にAPIキーを取得します。
https://developers.google.com/youtube/registering_an_application?hl=ja


サンプルにニュース番組のLIVE配信URLを拝借します。(番組はLIVE配信終了済み)
`https://www.youtube.com/watch?v=vefQ9-Dz73U`

https://www.youtube.com/watch?v=vefQ9-Dz73U

# 全体の流れ

1. 動画URLからVideo IDを取得
2. Video IDからChat IDを取得 (エンドポイントA)
3. Chat IDからコメント一覧を取得 (エンドポイントB)
4. 前回取得したコメントのIDを使って差分のコメントを取得

`https://www.youtube.com/watch?v=vefQ9-Dz73U`

# コーディング
まずはエンドポイントを定義します。リクエストはREST形式で受け付けます。
実際の処理はControlerという階層を設けてそちらに記述しました。

エンドポイントA
`/chatId/:videoId`

エンドポイントB
`/comment/:chatId?nextPageToken=xxxxxx`




```go
package main

import (
    "github.com/gin-gonic/gin"
    "app/controller"
    "net/http"
    "github.com/gin-contrib/cors"
    "time"
)

const location = "Asia/Tokyo"

func init() {
    loc, err := time.LoadLocation(location)
    if err != nil {
        loc = time.FixedZone(location, 9*60*60)
    }
    time.Local = loc
}


func main() {
    engine:= gin.Default()

    engine.Use(cors.New(cors.Config{
        AllowOrigins: []string{
            "http://localhost:3000",
        },
        AllowMethods: []string{
            "GET",
            "OPTIONS",
        },
        AllowHeaders: []string{
            "Access-Control-Allow-Credentials",
            "Access-Control-Allow-Headers",
            "Content-Type",
            "Content-Length",
            "Accept-Encoding",
        },
      }))

    engine.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "hello world",
        })
    })

    v1 := engine.Group("/v1")
    {
        // エンドポイントA Video IDを使って、Chat IDを取得するためのエンドポイント
        v1.GET("/chatId/:videoId", func(c *gin.Context) {
            videoId := c.Param("videoId")
            c.JSON(http.StatusOK, gin.H{
                "chatId": controller.GetChatId(videoId),
            })
        })
        
        // エンドポイントB Chat IDを使って、コメントを取得するためのエンドポイント
        v1.GET("/comment/:chatId", func(c *gin.Context) {
            chatId := c.Param("chatId")
            pageToken := c.Query("pageToken")
            c.JSON(http.StatusOK, gin.H{
                "chatDetail": controller.GetComment(chatId, pageToken),
            })
        })
    }

    engine.Run(":8080")
}
```

続いて実際の処理内容をエンドポイントごとに説明します。

```go
package controller

import (
	"net/http"
	"time"
	"log"
	"io/ioutil"
	"encoding/json"
	"app/utils"
)
```
説明のため細切れにします

## エンドポイントA
レスポンスの受け取りに `GetChatIdResponce` を定義します。
Video IDを添えて、 `https://www.googleapis.com/youtube/v3/videos` からChat IDを含む動画情報を取得します。
`LiveStreamingDetails.ActiveLiveChatId` が Chat IDになります。

```go
// GetChatId

type GetChatIdResponce struct {
	Items []GetChatIdItem `json:"items"`
}

type GetChatIdItem struct {
	LiveStreamingDetails LiveStreamingDetail `json:"liveStreamingDetails"`
}

type LiveStreamingDetail struct {
	ActiveLiveChatId string `json:"activeLiveChatId"`
}

func GetChatId(videoId string) string{
    // videoIdからchatIdを取得する
    req, err := http.NewRequest("GET", "https://www.googleapis.com/youtube/v3/videos", nil)
    if err != nil {
        log.Fatal(err)
    }

    params := req.URL.Query()
    params.Add("key", "ここにYouTubeから取得したトークンが入ります")
    params.Add("id", videoId)
    params.Add("part", "liveStreamingDetails")
    req.URL.RawQuery = params.Encode()

    timeout := time.Duration(5 * time.Second)
    client := &http.Client{
        Timeout: timeout,
    }

    res, err := client.Do(req)
    if err != nil{
        log.Fatal(err)
    }

    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        log.Fatal(err)
     }

    var responce GetChatIdResponce
    if err := json.Unmarshal(body, &responce); err != nil {
        log.Fatal(err)
    }
    return responce.Items[0].LiveStreamingDetails.ActiveLiveChatId
}
```
# コメントを取得する

続いて、先程取得した Chat ID (LiveStreamingDetails.ActiveLiveChatId)を使用して
コメントを取得します。なお、一度取得したChat IDは動画でユニークなのでフロントに一時保存して使い回す事ができます。

また連続してチャットを取得する場合に何度もエンドポイントBをコールする事になります。前回取得したコメント以降のコメントを取得するために、レスポンスで取得できる `nextPageToken` を利用します。

## エンドポイントB
レスポンスの受け取りに `GetCommentResponce` を定義します。
Chat IDを添えて、 `https://www.googleapis.com/youtube/v3/liveChat/messages` からコメント情報を取得します。


```go
// GetComment

type GetCommentResponce struct {
	Items []GetCommentItem `json:"items"`
	NextPageToken string `jsonn:"nextPageToken"`
}

type GetCommentItem struct {
	AuthorDetails AuthorDetails `json:"authorDetails"`
	Snippet Snippet `json:"snippet"`
}

type AuthorDetails struct {
  DisplayName string `json:"displayName"`
}

type Snippet struct {
	DisplayMessage string `json:"displayMessage"`
	PublishedAt time.Time `json:"publishedAt"`
}

type ChatDetail struct {
	NextPageToken string `json:"nextPageToken"`
	ChatMessages []ChatMessage `json:"chatMessages"`
}

type ChatMessage struct {
	Datetime string `json:"datetime"`
	Name string `json:"name"`
	Comment string `json:"comment"`
}

func GetComment(chatId string, pageToken string) ChatDetail{
	// chatIdからcommentを取得する
	req, err := http.NewRequest("GET", "https://www.googleapis.com/youtube/v3/liveChat/messages", nil)
	if err != nil {
		log.Fatal(err)
	}

	params := req.URL.Query()
	params.Add("key", "ここにYouTubeから取得したトークンが入ります")
	params.Add("liveChatId", chatId)
	params.Add("part", "id,snippet,authorDetails")
	params.Add("pageToken", pageToken)
	req.URL.RawQuery = params.Encode()
	
	timeout := time.Duration(5 * time.Second)
	client := &http.Client{
				Timeout: timeout,
	}

	res, err := client.Do(req)
	if err != nil{
		log.Fatal(err)
	}

	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Fatal(err)
	}

	var responce GetCommentResponce
	if err := json.Unmarshal(body, &responce); err != nil {
		log.Fatal(err)
	}

	nextPageToken := responce.NextPageToken

	chatMessages := []ChatMessage{}

	for _, v := range responce.Items {
    timetext := utils.TimeFormat(v.Snippet.PublishedAt) + ": " + utils.TimeToSinceText(v.Snippet.PublishedAt)
		chatMessage := ChatMessage{
			Datetime: timetext,
			Name:  v.AuthorDetails.DisplayName,
			Comment: v.Snippet.DisplayMessage,
		}
		chatMessages = append(chatMessages, chatMessage)
	}

	return ChatDetail{
		NextPageToken: nextPageToken,
		ChatMessages: chatMessages,
	}
}
```


`type ChatMessage` で定義しているようにChat情報は


- 日付
- コメント者名
- コメント内容

として取得できます。

レスポンスのサンプルです。

```JSON
{
  "chatDetail": {
    "nextPageToken": "最後に取得したコメントのトークン",
    "chatMessages": [
      {
        "datetiime": "2021-12-06 12:34:56",
        "name": "ユーザー名",
        "comment": "コメントの内容",
      }
    ]
} 

```
コメントを取得した時に同時に最後のコメントのページング用のトークン `nextPageToken` も取得しているので、2回目以降のコメント取得でエンドポイントBに添えてリクエストしてみてください。

![christmas_santa_hello (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540446/173c17d3-98df-2caa-caf0-65f577cceda0.png)

以上です。

# おわりに

コメント取得ができることで分析や独自のクライアントなどを作る事ができます。
なお、コメントで取得した情報を永続化する場合はYouTubeの規約を確認し、保存可能な情報であることを確かめてから実施しましょう :smile: