Go + YouTube Data API v3 で特定のチャンネルの動画データ一覧を取得する

# 目的
YouTube Data API v3を使って、特定のチャンネルにアップロードされた動画に関するデータをすべて取得することが目的です。
APIの仕様上取得できる件数に制限があったり、動画のIDであるVideoIdの一覧を取得する際にちょっと苦労ので、それらを中心に解説していきますが、基本的には以下のリファレンスを見れば、時間はかかるかもしれませんが大体の操作は簡単に実装できるかと思います。
[YouTube API Reference](https://developers.google.com/youtube/v3/docs?hl=ja)

# 前提
* Go言語を使える環境が前提です
  * 使っているバージョンは `1.18.3`
* 以下の記事を参考に、YouTube Data API v3を有効にし、APIキーが取得済み
[YouTube Data API v3 を使って YouTube 動画を検索する](https://qiita.com/koki_develop/items/4cd7de3898dae2c33f20)

# 流れ
私のGitHubページに以下の一連の流れを実装したリポジトリがありますので、よかったら参考にしてみてください
https://github.com/tokuchi765/youtube-analyze/compare/v1.0.0...main

* クライアント / サービス生成
* チャンネルのアップロード動画の動画リストID取得（ここがややこしい）
  * アップロード動画すべてが格納されている再生リストが存在するので、その再生リストのIDを取得することで、後続の動画ID取得が可能となる
  * チャンネルIDを指定したらすべてのアップロード動画IDが取得できるような仕組みにしてくれないかな・・・（小声）
* 動画リストIDを元にチャンネルにアップロードされたすべての動画IDを取得
  * ここも少し面倒で、50件以上の動画が存在する場合に `NextPageToken` を使って再度APIを呼び出す必要がある
* すべての動画データ取得

#### コード全体
* 各章でメソッドの中身を解説していきます

```go
	// クライアント / サービス生成
	service := createService()

	// チャンネルのアップロード動画の動画リストID取得
	uploadsID := getUploadsID(channelID, service)

	// 動画リストIDを元にチャンネルにアップロードされたすべての動画IDを取得
	videoIDs := getVideoIDs(uploadsID, service)

	// 最終目的：すべての動画データ取得
	videoDatas := getVideoDatas(videoIDs, service)
```

# クライアント / サービス生成
* APIキーを用いてクライアントを生成
  * `developerKey` の部分にAPIキーを設定する
  * 生成した `http.Client` を元にサービスを作成
    * 作成したサービスを使いまわす形でこの後のAPI呼び出しを行う
  * 現在は非推奨なので改修予定、課題に詳細を記載

```go
import (
	"log"
	"net/http"

	"google.golang.org/api/googleapi/transport"
	"google.golang.org/api/youtube/v3"
)

func createService() (service *youtube.Service) {

	client := &http.Client{
		Transport: &transport.APIKey{Key: "developerKey"},
	}

	service, err := youtube.New(client)
	if err != nil {
		log.Fatalf("Error creating new YouTube client: %v", err)
	}

	return service
}
```

# チャンネルのアップロード動画の動画リストID取得
* 指定したチャンネルの動画をすべて取得するためには、この後の動画リスト取得時に `動画リストID` が必要
そのIDを取得するための処理
* Channels API を使用する
  * 公式リファレンスにあるようにYouTubeチャンネルに関する情報を取得できるAPI
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602508/f728a377-0294-1162-7866-14626d7dd156.png)
  * そのデータの中でも、この処理では以下の `動画リストID` が欲しい
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602508/08e87515-f897-6e4a-d7a4-736a7c6e9929.png)
* APIコールの流れ
基本的なAPIコールの流れをここで説明しちゃいますが、今後もAPIコール処理が出てきますのでこの流れを覚えておいてください
  * `service.Xxxxx` で呼び出したいAPIの種別を指定（今回の場合はChannels）
  * `service.Xxxxx.List([]string{"任意のプロパティ"})` で、APIコール時に取得したいリソースを指定する
    * どういったリソースがあるかは公式リファレンスを参照
    * 例 [channelsのリソース](https://developers.google.com/youtube/v3/docs/channels?hl=ja#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3)
  * `service.Xxxxx.List([]string{"任意のプロパティ"}).Id("任意のID")` でフィルターを設定
    * 今回はチャンネルIDを指定しているが、カテゴリーやユーザー名なども指定可能
    * 詳しくは公式リファレンスを参照 [channelsのパラメータ](https://developers.google.com/youtube/v3/docs/channels/list?hl=ja#%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF)
  * 上記の一連の流れで `youtube.ChannelsListCall` が取得可能（コード例の `channelsCall`）
  * 取得したオブジェクトに対して `Do()` を実行
  * レスポンスが取得できるのであとはよしなに（コード例の `channelsResponse`）
    * 今回は動画リストIDが欲しいので、`ContentDetails.RelatedPlaylists.Uploads` を取得

```go
import (
	"log"
	"net/http"

	"google.golang.org/api/youtube/v3"
)

func getUploadsID(channelID string, service *youtube.Service) string {

	// channelIDには任意のYouTubeチャンネルIDを指定
	channelsCall := service.Channels.List([]string{"contentDetails"}).Id(channelID)
	channelsResponse, err := channelsCall.Do()

	if err != nil {
		log.Fatalf("Error call YouTube API: %v", err)
	}

	channelsItem := channelsResponse.Items[0]

	// 目的の動画リストID
	return channelsItem.ContentDetails.RelatedPlaylists.Uploads
}

```

# 動画リストIDを元にチャンネルにアップロードされたすべての動画IDを取得
* 先ほど取得した動画リストIDを用いて、チャンネルに紐づくすべての動画IDを取得する
* PlaylistItems API を使用する
  * 公式リファレンスにあるように、再生リストに含まれる動画のリソースを取得可能なAPIです
  * 最初に流し見してた時は「再生リストに関するAPIだから今回の目的とは合わないか・・・」とか思ってたけどがっつり「アップロード動画」って書いてあった
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602508/4a637c54-9213-1d82-f34f-847a9836a0e9.png)
* API呼び出しの流れは先ほど説明した通りで、指定した動画リストIDに紐づくデータを最大50件取得可能
  * `PlaylistId` に先ほど取得した `uploadsID` を設定し、`MaxResults` に50を指定（これを指定しないとデフォルト設定で5件しか取れない）
  * じゃあ50件以上動画がアップロードされてるチャンネルはどうすればええの？というと `NextPageToken` を使う
  * 50件以上結果が存在する場合、レスポンス内の `NextPageToken` が入力されているので、それを `PageToken` の引数に設定してあげると次の50件が取得可能
  * 以下のコードも複雑に見えるが、`NextPageToken` が取得できなくなるまでAPIを呼び出してるだけ
  * もうちょっとシンプルなロジックで書けそうだけどゆるして

```go
import (
	"log"
	"net/http"

	"google.golang.org/api/youtube/v3"
)

func getVideoIDs(uploadsID string, service *youtube.Service) (videoIDs []string) {
	playlistsCall := service.PlaylistItems.List([]string{"contentDetails"}).PlaylistId(uploadsID).MaxResults(50)

	playlistsResponse, err := playlistsCall.Do()
	if err != nil {
		log.Fatalf("Error call YouTube API: %v", err)
	}

	for _, item := range playlistsResponse.Items {
		videoIDs = append(videoIDs, item.ContentDetails.VideoId)
	}

	if playlistsResponse.NextPageToken != "" {
		nextPageToken := playlistsResponse.NextPageToken
		for {
			nextCall := service.PlaylistItems.List([]string{"contentDetails"}).PlaylistId(uploadsID).PageToken(nextPageToken).MaxResults(50)
			nextResponse, err := nextCall.Do()
			if err != nil {
				log.Fatalf("Error call YouTube API: %v", err)
			}
			for _, nextItem := range nextResponse.Items {
				videoIDs = append(videoIDs, nextItem.ContentDetails.VideoId)
			}
			nextPageToken = nextResponse.NextPageToken
			if nextPageToken == "" {
				break
			}
		}
	}

	return videoIDs
}
```

# すべての動画データ取得
* 先ほど取得した、チャンネルに紐づくすべての動画IDリストをループして、動画データを取得する
* 動画データ取得に使うのは [Videos](https://developers.google.com/youtube/v3/docs/videos/list?hl=ja)
  * 使い方は簡単で、動画IDを引数に設定してあげるだけ
  * `VideoData` のstructを用意してデータを格納してるが、のちの処理で扱いやすいかと思ってやってるだけ

```go
import (
	"log"
	"net/http"

	"google.golang.org/api/youtube/v3"
)

type VideoData struct {
	VideoID       string
	Title         string
	ViewCount     uint64
	LikeCount     uint64
	DislikeCount  uint64
	FavoriteCount uint64
	CommentCount  uint64
	PublishedAt   string
}

func getVideoDatas(videoIDs []string, service *youtube.Service) (videoDatas []VideoData) {
	for _, videoID := range videoIDs {
		call := service.Videos.List([]string{"snippet", "statistics"}).Id(videoID)
		response, err := call.Do()
		if err != nil {
			log.Fatalf("Error call YouTube API: %v", err)
		}

		item := response.Items[0]
		videoData := VideoData{
			VideoID:       videoID,
			Title:         item.Snippet.Title,
			ViewCount:     item.Statistics.ViewCount,
			LikeCount:     item.Statistics.LikeCount,
			DislikeCount:  item.Statistics.DislikeCount, // ※oauth認証じゃないと取得できない
			FavoriteCount: item.Statistics.FavoriteCount,
			CommentCount:  item.Statistics.CommentCount,
			PublishedAt:   item.Snippet.PublishedAt,
		}
		videoDatas = append(videoDatas, videoData)
	}
	return videoDatas
}
```

# 課題
* APIキーだと低評価などの非公開データが取得できないので、oauth認証によるクライアント生成が必要
* クライアント生成に使用している "google.golang.org/api/googleapi/transport" パッケージが非推奨
  * https://pkg.go.dev/google.golang.org/api@v0.86.0/googleapi/transport
  * 代わりに、"google.golang.org/api/youtube/v3" NewService を使用する

# 参考
* 公式のサンプル（これを見れば大体実装できる）
  * https://github.com/youtube/api-samples 
* 公式のリファレンス
  * https://developers.google.com/youtube/v3/docs?hl=ja