Youtube_v3を使ってプレイリストに動画を自動追加

#目次
- Youtube Data APIの前に
- 環境
- Youtube Data APIについて(本題)
- そもそも何をするのか整理
- 下準備(google cloud platform編)
- 早速書いていく


# Youtube Data APIの前に

[公式ドキュメント](https://developers.google.com/youtube/v3/docs?hl=ja) 

主に、公式ドキュメントに沿って書いていく。といっても、最初から公式のドキュメントを読むのは疲れますよね、、、
そこで、[この記事](https://qiita.com/gyu_outputs/items/af4319258b3d67c57d24#videos)がおすすめです。とてもかみ砕かれていてYoutube Data APIがどのようなもので、どんな感じに使っていくのか雰囲気を掴めます!
自分は公式ドキュメントを最初から読むのはしんどいので、とりあえず記事を読んで「こいうメソッドがあるのか」、「こんなクラスがあるのか」と雰囲気を掴みます。そこから、詳しいところは公式ドキュメントを見て使うライブラリのGit Hub見に行ったりしています。

### 注意
とりあえずコードだけコピペしたいっていう人にはあまり向かない記事ですのでご了承ください(コードから見たい人は早速書いていくまで飛ばして下さい)。どのように、Youtube Data APIを使っていくのかどのようにYoutube_v3でコードを書いていくのか雰囲気を掴めるような記事になっています。また、間違っているところもあると思うのでご指摘頂ければ幸いです。

#環境
ruby 2.7.3
windows

#Youtube Data APIについて(本題)
それでは、本題。ここからは、概要を説明します。細かい実装は「早速書いていく」で記述します。
Youtube Data APIとは
<details><summary>youtubeアプリでできることをプログラムから操作できる</summary></datails>

このおかげで、youtube動画の分析や手動でやるのがめんどくさいものを自動化できます。なので、とても効率アップでストレスも減り楽しくなります!
・・・自分の場合は自動化したかったので今回使用することを決めました!(後述)

主に、操作できるものは以下の三つです。
・Video
・Channel
・Playlist

この三つについて、それぞれに以下の四つのメソッドがあります。
・list
・insert
・update
・delete

このメソッドの引数には以下の二つがあります
part -> 戻り値としてどのような情報が欲しいか指定する
options -> リクエストの際の指定ができる(maxResultやmineなど)
ここら辺は以下で調べ方を書いていきます。

###Youtube Data APIのリファレンスはめちゃくちゃ分かりやすい
[公式ドキュメント](https://developers.google.com/youtube/v3/docs?hl=ja)の見方としては以下の画像見てください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1708420/ec72ea2c-1647-e3cf-27b7-517661dcb654.png)
リファレンスタブを開くと、左側に色々出てきます。この色々が、操作できるリソースになってきます。
###channelを例に見ていく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1708420/9f510a6c-90ad-205f-adbb-76cc8d54f729.png)

上記画像の概要でリソース(channelのこと)のメソッドによる戻り値が書いてある。どのような情報が返ってくるかということ。ここの情報で、snippetやcontentDetailsの部分がpartにあたる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1708420/49641a94-1208-fb47-5fbe-f602f98a6f5f.png)

リスト(list)では、そのメソッドでの必須パラメータ(optionsやpartのこと)や使えるpartが分かる。この、mineやidなどはoptionsにあたる。

ここら辺をくわしく知りたかったら、このようにドキュメントを読むかまだよくわからないという方は[この記事](https://qiita.com/gyu_outputs/items/af4319258b3d67c57d24#videos)を参考に。

これらを駆使して、youtube上のデータを扱っていきます。では!今回はRubyで作ったのでRubyのソースコードと共に使い方を書いていきます。
とその前に以下から、自分の話になるので早くソースコード見たいという方は「早速書いていく」まで飛ばして構わないです。

#そもそも何をするのか整理
###背景
　自分はEDMがとても好きで毎日youtubeで曲を探しています。その時間って、楽しいんですけど時間がかかってしまうので自動化できないかなと思っていました。そこで、最近API接続をやっていたのでyoutubeにもapiがあるのではないかと思い調べるとあったのでYoutube Data API(youtube_v3)を使って曲の探索を自動化しようと思いました。さらに、友達にもEDM好きが割といて、「共有してほしい!」ということになりました。
###概要
自分の好きな動画(今回でいうとEDM)を探してきて、それを自分が指定したプレイリストに追加し、追加した動画の詳細情報をspreadsheetに書き出します。そのspreadsheetはgoogle driveの共有スペースに保存するので、友達にも共有できます。
###機能
・動画検索
・再生回数フィルター : 指定した再生回数以上の動画を取得
・動画の詳細情報の取得
・プレイリスト検索 :今回は自分のプレイリストを取得します
・既存動画フィルター : プレイリストに既に動画があるならその動画は追加しない
・プレイリストに動画を追加 
・spreadsheetに書き込む(別記事にします!) : google drive上に作成したものを使う


#下準備(google cloud platform編)->以下GCPとする
ここは、別の人が分かりやすく記事にしてくれているので以下を参考にしてください。
[まずはGCPへ登録](https://qiita.com/Brutus/items/22dfd31a681b67837a74)
[GCPでyoutube api有効化](https://qiita.com/koki_develop/items/4cd7de3898dae2c33f20)
[GCPとOAuth通信するまでに必要な設定](https://dev.classmethod.jp/articles/oauth2-youtube-data-api/)
正直、自分はGCPへの登録はもちろんしましたがファイアーウォールの設定やssh接続の設定はまだしていません。とりあえず、今回の目的は先ほど書いた通りyoutube apiによる自動化と共有ができることだからです。GCPをさらに、使い込むようになれば設定したいと思っています。。

#早速書いていく
###OAuth認証から
まずは、OAuth認証のコードを載せます。

```ruby
require 'googleauth'
require 'googleauth/stores/file_token_store'
require 'google/apis/youtube_v3'

class Oauthorize
  
  OOB_URI = 'urn:ietf:wg:oauth:2.0:oob' #これは、コマンドラインで実行するのでWebで実行するときのRIDIRECT_PATHの代わり。
  APPLICATION_NAME = '自分のアプリの名前'
  #GCPでOAuthを有効化してjsonファイルを同じディレクトリ配下にダウロードする
  #ちなみに、ダウンロードしたファイルは以下のように名前を変えています
  CLIENT_SECRETS_PATH = 'client_secret.json'
  CREDENTIALS_PATH = File.join(Dir.home, '.credentials', "tokens.yaml")
  #SCOPEとはOAuth認証でいう権限のこと。
  #API利用するアプリにどれくらいの、API利用の権限を与えるかということ。
  SCOPE = [ "https://www.googleapis.com/auth/youtube" ]
def self.authorize
    #FileUtilsはFileクラスよりメソッドが色々ある
    #mkdir_pは2階層以上のディレクトリを一気に作成できる
    FileUtils.mkdir_p(File.dirname(CREDENTIALS_PATH))
    #API接続のためのclient設定
    client_id = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH)
    #p client_id client.idとclient.secretとしてが格納されていた。
    token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
    authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPE, token_store)
    user_id = 'default'
    credentials = authorizer.get_credentials(user_id)
    p credentials
  #初回だけ必要な処理。credentialを取得したら、2回目はCREDENTIALS_PATHに作られたtokens.yamlから参照する。
    unless credentials
      #となると、トークンの有効期限を見て、更新の処理が必要になってくる。
      #credentailの中にrefresh_tokenもあるので、oauth/tokenへrefresh_tokenなどと一緒にリクエストすると更新できるはず。
      url = authorizer.get_authorization_url(base_url: OOB_URI)
      puts 'Open the following URL in the browser and enter the ' +
          'resulting code after authorization'
      puts url
      code = gets.chomp
      #以下で、credentialの入手に成功すると、CREDENTIALS_PATHへの書き込みがされる。
      credentials = authorizer.get_and_store_credentials_from_code(
        user_id: user_id, code: code, base_url: OOB_URI)
    end
    
    return credentials
  end
end
```

分かりやすさ重視でコメントアウトで結構書いてしまいました。各自、requireするものでgem installしていなかったらしましょう。今回はgoogleauthだけで大丈夫です。

:::note info
info
gemをインストールする時はrubygemsからしましょう。安定版が分かるし、そのgemがどれくらい評価されているかも分かります。
:::
また、「メソッドの引数が詳しく知りたい、クラスについて知りたい」や「他のメソッドを使いたい」などあれば今回の[googleauthのGit Hub](https://github.com/googleapis/google-auth-library-ruby/tree/main/lib/googleauth)を見に行きましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1708420/d9d0b6c1-54d6-ed4c-f3b9-1f309323abfd.png)

```ruby
 authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPE, token_store)
```
例えば、上のUserAuthorizerクラスについて知りたい時。以下のファイルに行く。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1708420/8fcf4e13-0a53-c884-b0e5-f2b45313c67f.png)
60行目ぐらいにinitializeがある。

```ruby
 def initialize client_id, scope, token_store, callback_uri = nil
        raise NIL_CLIENT_ID_ERROR if client_id.nil?
        raise NIL_SCOPE_ERROR if scope.nil?

        @client_id = client_id
        @scope = Array(scope)
        @token_store = token_store
        @callback_uri = callback_uri || "/oauth2callback"
      end
```
このように、client_idとscopeとtoken_storeがあるから上記のようにUserAuthorizerのインスタンスに引数を渡していたんですね。ちなみに、callback_uriは渡していないので
デフォルト値の"/oauth2callback"が使われます。

### 補足
SCOPEについて。このSCOPEにはどのような種類があるかは[Youtube Data APIのドキュメント](https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps?hl=ja)を参考に。以下のように、掲載されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1708420/84a1b2ec-3b52-97fd-3f0c-3981e71ad93c.png)

さて、ここでようやく認証ができたのでプレイリストに動画を追加するために、まずは動画を検索していきたいと思います。

### 動画を検索
早速コードを載せていきます。まずは、欲しい動画を検索します。listメソッドのsearchを使っていきます。[google-api-cliet](https://rubygems.org/gems/google-api-client)をインストールしてない人はしましょう。

```ruby
        next_page_alt_token = ""
        filtered_video_ids = []
        result_hash = {items: []}
        loop do
            next_page_token = next_page_alt_token
            option = {
                q: keyword,
                type: 'video',
                page_token: next_page_token,
                video_duration: video_duration,
                max_results: 50,
                order: :rating, #評価順
                published_after: after.iso8601, #iso8601という形式にしている。
                published_before: before.iso8601
            }
            #@youtubeはinitializeで初期化されています。後述。
            youtube_search_list = @youtube.list_searches(:snippet, option)
            #file_operationはレスポンスをハッシュにして返してくれます。
            result = file_operation(youtube_list: youtube_search_list, file_write_flag: false) 
            #検索結果の動画のidを格納
            video_ids = []
            result[:items].each do |item|
                video_ids << item[:id][:videoId]
            end
    
            break if filtered_video_ids.length >= search_count || result[:nextPageToken].nil?
#video_contentの戻り値は再生回数でフィルターされた検索結果の動画のidと
#再生回数でフィルターされた動画の詳細情報をハッシュにしたもの
            return_result_hash, return_filtered_video_ids = video_content(video_ids: video_ids, video_view_count: video_view_count) 
            #検索結果のフィルターされた動画の詳細情報とidを配列に結合する。
            result_hash[:items].concat(return_result_hash[:items]) 
            filtered_video_ids.concat(return_filtered_video_ids)
            #max_result=50が終わっても、youtube上に動画があるなら
            #nextPageTokenが返される
            #それを、次のリクエストでオプションに渡すと
            #1~50の次は50~100までの、動画が取得できる。
            next_page_alt_token = result[:nextPageToken]
        end
```
###解説
変数などは、このメソッドの引数で受け取っています。まずは、オプションから解説していきます。公式リファレンスからでは、少し分かりずらいところを中心に行きます。
・max_results
　一回のリクエストで、1~50の範囲で取得する動画の数を指定できます。

・published_after, published_before
　iso8601という形式を渡さないといけないです。Time型をiso8601へと変換させて渡しています。apiでは、iso8601が割と使われるらしいです。また、video_content関数内で動画の詳細情報を取得する際に、durationというキーで動画時間が返ってきます。その時は、[ruby-durationというgem](https://rubygems.org/gems/ruby-duration)を使ってみてください。git hubのread.meに使い方が書いてあります。

・nextPageToken
　まず、listメソッドのオプションのmax_resultsが1~50までしかないです。そして、動画なんて無数に存在するので50件取得しても欲しい動画があるか分かりません。そこで、50件まで取得したらその取得した動画の続きから連続でリクエストを投げたいものです。そのために、どこまで動画を取得したのかnextPageTokenというもので判別します。(youtubeの動画はリクエストするたびにランダムで取得ではなく、決まった順番で常にあるということですね。)

次に、分かりにくい変数を説明します。
・@youtube
　先ほど載せたOAuth認証のクラスのauthorizeメソッドで取得したcredentials(認証情報)をセットしたインスタンスです。これで、自分のチャンネルについての操作など認証が必要な操作ができるようになります。

```ruby
　      @youtube = Google::Apis::YoutubeV3::YouTubeService.new
        credentials = Oauthorize.authorize
        @youtube.authorization = credentials
```

・video_view_count
　自分が欲しい動画の再生回数。1万回以上の動画欲しかったら10000とする。

・search_count
　自分が取得したい動画の数です。(後のfiltered_already_videoによって、結果として取得できる動画数は減ることがあります。)

・filtered_video_ids
　video_view_countより多い再生数の動画のidが格納された配列です。

・return_result_hash
　filtered_video_idsに格納されたidに対応する動画の詳細情報を記したハッシュです。

####何をしているのか
　loop doでループしているのは、keyで検索かけて50件の動画で条件を満たした動画がsearch_count(今回欲しい動画の数)以上取得するまで繰り返すためです(一回のリクエストの50件じゃ、終わらないからです)。こいう時は先ほども述べましたがnextPageTokenを使います。では、このloopを抜けるのはどのような時か具体的に見ていきます。これは、動画をview_contentにvideo_view_countを渡しているのが分かると思います。そこで、video_view_countより多い再生数の動画のidが格納された配列(filtered_video_ids)と詳細情報を記したハッシュ(return_result_hash)を返します。
　そして、filetered_video_idsつまり再生数の条件を満たした検索結果の動画がsearch_count以上になったらloopを抜けるようにしています。また、nextPageTokenがなくなっても終わりです。それはもう、動画がないということですから。

```ruby
break if filtered_video_ids.length >= search_count || result[:nextPageToken].nil?
```
さて、これでプレイリストに追加したい動画がそろいました。あとは、プレイリストに追加とその前に、追加するプレイリストのidを探しましょう。idがないと、プレイリストに対して操作ができません。

###プレイリストidを取得する
今回自分のチャンネルのプレイリストに追加していきたいので、playlistリソースのlistを使い、さらにオプションにmineを指定します。

```ruby
def my_playlist_id(filtered_video_ids)
        options = {
            mine: true
        }
        youtube_channel_playlist = @youtube.list_playlists('snippet, contentDetails', options)
        #file_operationはレスポンスをハッシュに変えてくれます。
        #実はもう一つあって、レスポンスをファイルに書き出しています。
        result = file_operation(youtube_list: youtube_channel_playlist, file_write_flag: false)
        #ここで、追加するプレイリストを変えることができる。
        play_list_id = result[:items][0][:id] 
        return play_list_add_video(play_list_id: play_list_id, filtered_video_ids: filtered_video_ids)
    end
```

オプションで、mine: trueとすることで自分のチャンネルのプレイリストが取得できます。あとはレスポンスを見て何番目のプレイリストに追加するか決めます。それと、追加する動画をplay_list_add_videoに渡します。

####レスポンスの確認
少し脱線。レスポンスの確認はファイルへの書き出しでしてました。

```ruby
#dumpよりpretty_generateがおすすめ。見やすい形で書き出してくれる。
results = JSON.pretty_generate(youtube_list) #返値はJSON文字列

        if file_write_flag
            File.open("serch_result.json", "a") do |f|
                f.write(results)
                f.write("\n")
            end
        end
```
JSONのメソッドがあまり分からない人は[こちら](https://pikawaka.com/ruby/json)を参考にして見てください。json文字列とハッシュの相互変換ができるようになります。

###プレイリストに動画を追加
 さあ、ついにこの記事の最後です。プレイリストに動画を追加していきます。また、ここではプレイリストに動画を追加の部分しか解説しません。
 さて、最後が正直少しきつかったです。まさかの、~~Youtube_v3のYoutubeServicesにプレイリストに動画を追加のメソッドがないのです!!~~(自分が使い方分からないだけです。。。)
 [ここ](https://github.com/googleapis/google-api-ruby-client/blob/main/google-api-client/generated/google/apis/youtube_v3/service.rb)にメソッドはたしかにあります。以下コードです。(playlist関連が知りたいのでctr + fでコード内検索していきます。)

```ruby
def insert_playlist_item(part, playlist_item_object = nil, on_behalf_of_content_owner: nil, fields: nil, quota_user: nil, options: nil, &block)
    command = make_simple_command(:post, 'youtube/v3/playlistItems', options)
    command.request_representation = Google::Apis::YoutubeV3::PlaylistItem::Representation
    command.request_object = playlist_item_object
    command.response_representation = Google::Apis::YoutubeV3::PlaylistItem::Representation
    command.response_class = Google::Apis::YoutubeV3::PlaylistItem
    command.query['onBehalfOfContentOwner'] = on_behalf_of_content_owner unless on_behalf_of_content_owner.nil?
    command.query['part'] = part unless part.nil?
    command.query['fields'] = fields unless fields.nil?
    command.query['quotaUser'] = quota_user unless quota_user.nil?
    execute_or_queue_command(command, &block)
  end
```
　引数を見ると、video_idやplaylist_idみたいなものがないのです。playlist_item_object が怪しいですが、自分には分からなかったです。(だれか分かったら教えてください。。。)
　そこで、公式のものではないのですがYtというgemを見つけました。これも、Youtube_v3のようなものでyoutubeを操作できます。
[Ytドキュメント](https://fullscreen.github.io/yt/playlists.html)もとても分かりやすいです。そして！しっかりYtにはadd_videosというメソッドがありました。めちゃくちゃ分かりやすい。。

```ruby
playlist.add_videos %w{BPNYv0vd78A 2NW_AqhkYZZ}
```
では、これを使って最後にプレイリストに動画を追加していきます。

```ruby
 def play_list_add_video(play_list_id: "", filtered_video_ids: [])
        video_already_ids = play_list_item_ids(play_list_id)
        #以下が最終的にプレイリストに追加する動画のid配列。
        addition_video_ids = filter_already_video(filtered_video_ids: filtered_video_ids, video_already_ids: video_already_ids)
        
        youtube_play_list =  Yt::Playlist.new id: play_list_id, auth: @youtube_yt
        #add_videosとadd_videoがありadd_videosは引数にvideo_id: []を取る。
        youtube_play_list.add_videos addition_video_ids 
        return addition_video_ids
    end
```
####解説
・filter_already_video
　プレイリストに既に存在する動画は追加しないというフィルターをかけるメソッドです。戻り値はその、フィルターがかかった動画のidが格納された配列です。

・@youtube_yt
　これは、Ytのインスタンスです。以下にこれを初期化するコードを示します。

```ruby
#ここで、YtでのOAuth認証の仕方はgoogleauthで先ほども出てきたauthorizeメソッドで取得したcredentialsを利用する。
        Yt.configure do |config|
          config.client_id = credentials.client_id
          config.client_secret = credentials.client_secret
        end
        @youtube_yt = Yt::Account.new(refresh_token: credentials.refresh_token)
```
これで完成です。ここでは、触れなかった部分などコード全体は[Git Hub](https://github.com/haruya3/google-api)にありますのでよかったら見て下さい。
※可読性と保守性が低いコードなのでアドバイス頂けると幸いです。特にオブジェクト指向面でお願いします。

# まとめ
　ここまで、Youtube Data APIから始まり、GCPの登録、OAuth認証、実際にプレイリストに動画を追加していきました。今回初めて、ガッツリAPIを利用してみてたのでとてもいい経験になりました。後は、spreadsheetに検索結果の動画を書き込みGoogle Drive上に共有するところまで自動化するのでそちらも記事にしようかなと思っています。(間違ってるところ色々あったと思うのでその時はご指摘ください!)
　今回、記事の投稿は初めてだったのですがAdvent Calenderといういい機会に書けて良かったです。@KunishiLeonさんありがとうございました。