Youtube Data APIv3で購読チャンネル新規動画(newsubscriptionvideos相当)を取得する...Rで

YouTubeで自分の購読しているチャンネルの新規投稿動画を取得しようとしたところ，意外なことに一筋縄では行かなかったのでとりあえずココにメモ．

## やりたいこと
今更紹介する必要も無い気はするが，YouTubeにはお気に入りのチャンネルを「購読(登録)」し，それらの更新動画を一覧表示する機能がある．

YouTubeの登録チャンネルページ(https://www.youtube.com/feed/subscriptions)

これをYouTubeのAPIで取得して一覧にまとめたいと思った．
初めは「購読チャンネルの新規投稿動画なんてAPIですぐ取れるでしょ」とか思ってたが，その機能(newscbscriptionvideos)はYouTube Data APIのv2まで．v3では未実装．
ということでv3に実装されている機能を上手く組み合わせ，v2のnewscbscriptionvideos相当の事をしようというのがこの記事の趣旨になる．

## 環境
* Ubuntu 14.04 LTS
* R 3.3.1 + httr 1.2.1 ( + dplyr, rlist, pipeRも使用)

pythonの使いドコロな気もするが，httrパッケージを使えばRでもOAuthを使えるというので使ってみた．

## 前準備(APIの利用登録など)

### ブラウザ上での作業
[Googleのインストラクション](https://developers.google.com/youtube/v3/getting-started)に従い，YouTubeのAPIを使えるようにする．

まずは[Google Developers Console](https://console.developers.google.com/)に移動し，適当なプロジェクト(画像ではyoutube)を作成する
<img width="661" alt="2A8C2864-51E1-4B2D-802C-5B592D324663.png" src="https://qiita-image-store.s3.amazonaws.com/0/56445/2b8f845e-6857-93fe-58b6-6c5e86ed3fbd.png">

プロジェクトを作成したら，このプロジェクトに対しAPIを有効にする．
検索窓にyoutubeと打ち込み，「YouTube Data API v3」を選択．先程作成したプロジェクトを選択し，「続行」．
<img width="476" alt="7FCBE2BB-1D52-4515-B3A6-B9E7C92D333A.png" src="https://qiita-image-store.s3.amazonaws.com/0/56445/7c4b3248-0a31-f4b1-5ef7-166e36570f99.png">

次の画面で有効にし，「認証情報に進む」．
<img width="932" alt="D1FDC965-8945-4970-91EC-EEDB9F2F7A45.png" src="https://qiita-image-store.s3.amazonaws.com/0/56445/aca99e24-17b0-7743-8b17-d525d0595414.png">

APIを呼び出す場所は「その他のUI」(「その他の非UI」だと，ユーザーデータを入手できない)
で，「アクセスするデータの種類」を「ユーザーデータ」に．クライアントID，OAuth2.0画面も適当に設定する．
最後まで進むとClient ID等が発行されるので，JSON形式で保存しておく．

### R上での作業
httrでOAuth2.0の認証をすすめる．

```r:sample.R
library(httr)
#前段階の最後で手に入れたIDとSecret等を使う．
youtube_app <- oauth_app('YouTube',key='<Client ID>',secret='<Client Secret>') 
youtube_ep <- oauth_endpoint(authorize='<Auth URI>',access='<Token URI>')
youtube_token <- oauth2.0_token(youtube_ep,youtube_app,scope='https://www.googleapis.com/auth/youtube.readonly',cache=T)
```
すると認証用のURLがコンソールに出力されるので，ここにブラウザでアクセスすると，よくある認証画面が表示される．
![D1676F2C-F2CB-430D-A798-CB7B8C2F2592.png](https://qiita-image-store.s3.amazonaws.com/0/56445/31815ddd-b677-66f4-7ce0-88ff0c20b0b9.png)

許可するとコードが出力されるので，Rのコンソールに貼り付けてEnter．
これでhttrパッケージからYouTubeの認証が必要なAPIを叩けるようになる．

## 新規動画の取得
ここが肝．
取得方法はStackOverflowの[Retrieving all the new subscription videos in YouTube v3 API](https://stackoverflow.com/questions/19640796/retrieving-all-the-new-subscription-videos-in-youtube-v3-api)に投稿されていたものを参考にする．
具体的には

1. 購読チャンネル一覧を取得
2. 購読チャンネルごとに，uploadプレイリストのプレイリストIDを取得
3. プレイリスト毎に，プレイリストに含まれる動画の情報(videoID,タイトルetc...)を取得

という3段階で取得する．
1-3とも取得件数は最大50件までに制限されている．そのため1,2では複数回繰り返して全件取得する．

Rで実装してみると，下のようになった．

手順1

```r:getChannelID.R
#=購読チャンネルIDのリストを取得
#=一度に50件までしか取れない．50件以上ある場合，nextPageTokenが付いてくるので，付いてこなくなるまで取得する

getChannelID <- function(){

	GET(url='https://www.googleapis.com/youtube/v3/subscriptions',config(token=youtube_token),query=list(part='snippet',mine='true',maxResults=50)) %>>%
		stop_for_status %>>%
		content -> res
	channelList <- res$items
	nextPage <- res$nextPageToken

	while(!is.null(nextPage)){
		GET(url='https://www.googleapis.com/youtube/v3/subscriptions',config(token=youtube_token),query=list(part='snippet',mine='true',maxResults=50,pageToken=nextPage)) %>>%
			stop_for_status %>>%
			content -> res
		channelList <- append(channelList,res$items)
		nextPage <- res$nextPageToken


	}

	channelList %>>% 
		list.map(snippet) %>>% 
		list.map(resourceId) %>>% 
				list.mapv(channelId) -> channelIDList

				return(channelIDList)
}
```

手順2

```r:getPlayListID.R
#=各チャンネルのuploadプレイリストのIDを取得
getPlaylistID <- function(channelIDList){

	#複数チャンネルのプレイリストを取れるが最大50件まで
	#50件ずつ分割して取得する
	max_itr <- length(channelIDList) %/% 50 + 1
	playlistID <- lapply(1:max_itr,function(i){
		       channelIDList_comb <- paste(channelIDList[((i-1)*50+1):min(i*50,length(channelIDList))],collapse=',')
		       GET(url='https://www.googleapis.com/youtube/v3/channels',config(token=youtube_token),query=list(part='contentDetails',id=channelIDList_comb,maxResults=50)) %>>%
			       stop_for_status %>>%
			       content %>>%
			       (items) %>>%
			       list.map(contentDetails) %>>%
			       list.map(relatedPlaylists) %>>%
			       list.mapv(uploads) -> res
		       return(res)
})
	playlistID <- do.call(playlistID,what='c')
	return(playlistID)
}
```

手順3

```r:getVideoInfo.R
#=各プレイリストに含まれる動画(最新50件)の情報をdata.frameにまとめ返す
#新規動画が50件以上存在した場合取り逃すが
#publishedTime(newest first)|channelTitle|videoTitle|videoUrl
getVideoInfo <- function(playlistID){

videoDf <- lapply(1:length(playlistID),function(i){

GET(url='https://www.googleapis.com/youtube/v3/playlistItems',config(token=youtube_token),query=list(part='snippet',playlistId=playlistID[i],maxResults=50)) %>>%
	stop_for_status %>>%
	content %>>%
	(items) %>>%
		list.map(snippet) %>>%
		(~list.mapv(.,channelTitle) -> channelTitle) %>>%
		(~list.mapv(.,publishedAt) -> publishedTime) %>>%
		(~list.mapv(.,title) -> videoTitle) %>>%
		list.map(resourceId) %>>%
		list.mapv(videoId) -> videoID
	df <- data_frame(publishedTime,channelTitle,videoTitle,videoID)
	return(df)
})
videoDf %>>%
	rbind_all %>>%
	mutate(publishedTime=as.POSIXct(strptime(publishedTime,'%Y-%m-%dT%H:%M:%S.000Z',tz='UTC'))) %>>%
	mutate(videoUrl=paste0('https://www.youtube.com/watch?v=',videoID)) %>>%
	arrange(desc(publishedTime)) %>>%
	select(-videoID) -> videoDf

return(videoDf)
}

```

後は↑の3つの関数を

```r:sampleGet.R
getNewSubscribedVideo <- function(){

channelIDList <- getChannelID()
playlistID <- getPlaylistID(channelIDList)
videoDf <- getVideoInfo(playlistID)

return(videoDf)

}

getNewSubscribedVideo()
```
という感じで叩けば新規投稿動画を取得出来る．


### この方法の問題点
とにかく重い．51チャンネルを購読している私の場合，取得に最大8秒程度掛かる．全てのチャンネル，全てのプレイリストを並列化せずに取得しているため当然だが...．
オンデマンドで実行するにはストレスになりそう．

スクリプトをcron登録して，新規投稿動画を定期的に取得するようにしても良いが，わざわざそんなことをする必要は無い気もする．
ただ後述のyoutube-dlと組み合わせ，全ての投稿動画を自動ダウンロードするシステム，とか作れば(そんなシステムを使う機会があるかは置いといて)，面白いかもしれない．




## その他
* [youtube-dl](https://rg3.github.io/youtube-dl/)を使えば，新規動画一覧を取得し，動画のダウンロードまで出来るらしい(詳細は[man](https://github.com/rg3/youtube-dl/blob/master/README.md))．Googleの二段階認証を上手く通れなかったので今回は使っていない．
* Rならrvestでスクレイピングしても良いし，多分その方がシンプルかつ高速．今回はAPIを使いたかったのでこの方法にしたが．