6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-07

はじめに

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

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

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

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

christmas_santa_sori.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

全体の流れ

  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

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")
}

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

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になります。

// 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 からコメント情報を取得します。

// 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情報は

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

として取得できます。

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

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

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

christmas_santa_hello (1).png

以上です。

おわりに

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

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?