YouTube動画を検索するサイトを作ります(自分専用のためエラー処理は省略^^;)
GASを使えばAPIキーの取得やサーバーが不要です
スクリプトの準備
スタンドアロン(GoogleドライブからGASを新規ファイルとして開きます)で開きます
拡張サービス(YouTube Data API)を追加します
「サービスの+ボタン」をクリックし「YouTube Data API」を選択し追加します
HTMLファイルを追加します
「ファイルの+ボタン」をクリックし「HTML」を選択「index」と入力します
YouTube.Searchで検索
YouTube Data APIの「Search: list」メソッドを利用します
「Search: list」はリクエストで指定したクエリパラメータに一致する検索結果を返します
検索結果の構造(itemsにidやタイトルなどの情報が含まれます)
{
"kind": "youtube#searchListResponse",
"etag": etag,
"nextPageToken": string,
"prevPageToken": string,
"pageInfo": {
"totalResults": integer,
"resultsPerPage": integer
},
"items": [
検索リソース
]
}
Search.list(part, option)
メソッドの引数 part
:必須でレスポンスに含めるリソースのプロパティ'id,snippet'
を指定します(YouTube Data APIのパラメータ)
*snippetも指定することで検索結果(items)に動画のIDだけでなくタイトルや説明などが含まれますmaxResults
:結果の最大数を0以上50以下で指定します(デフォルト値は5)order
:並べ替え方法date(作成日の新しい順)・rating (評価の高い順)・viewCount(再生回数の多い順)などq
:検索クエリを指定します
function searchWord() {
const results = YouTube.Search.list('id,snippet', {
maxResults:2,
q:'Google App Script',
//再生回数の多い順
order:'viewCount'
});
//itemsをログで確認してみる
results.items.forEach(item=>{
console.log(item.id);
console.log(item.snippet);
})
}
searchWord関数を実行します(保存→実行)
アクセス権限の承認をします(初回のみ)
権限を確認→アカウントを選択して→詳細→プロジェクト名(安全ではないページ)に移動→許可をクリック
実行ログの抜粋
---id---
{ kind: '・・・', videoId: '・・・' }
---snippet---
{ description: '・・・',
publishTime: '2019-01-10T23:13:15Z',
channelId: '・・・',
title: '・・・',
liveBroadcastContent: 'none',
channelTitle: '・・・',
publishedAt: '2019-01-10T23:13:15Z',
thumbnails:
{ medium:
{ width: 320,
url: 'https://・・・',
height: 180 },
high:
{ url: 'https://・・・',
height: 360,
width: 480 },
default:
{ url: 'https://・・・',
height: 90,
width: 120 }
}
}
Webサイトとして公開
1:doGet()関数
でindex.htmlファイルをHtmlOutputオブジェクトにして返します
HtmlService.createHtmlOutputFromFile(‘index’)
doGetは「公開したURLにアクセスがあったとき」というトリガーです
2:searchWord(keyword)関数
でクライアント側から検索キーワードを受け取り、結果をクライアント側に返却します(後でクライアント側スクリプトでgoogle.script.runを実装します)
*検索クエリはエンコードされたキーワードが渡ってくるのでデコードします
function doGet(){
return HtmlService.createHtmlOutputFromFile('index');
}
function searchWord(keyword) {
const results = YouTube.Search.list('id,snippet', {
maxResults:2,
//エンコードされたキーワードを受け取るのデコードする
q:decodeURIComponent(keyword),
order:'viewCount'
});
return results;
}
とりあえず公開する
- 右上「デプロイ」から「新しいデプロイ」をクリック「アクセスできるユーザー」を選択
- 左上「歯車アイコン」のをクリック「ウェブアプリ」を選択して「デプロイ」をクリック
- 右上「デプロイ」から「デプロイをテスト」を選択し「URL」をコピー「完了」をクリック
コピーしたURLにアクセスしたときの画面表示
備考:「デプロイをテスト(常に最後に保存されたコードを実行)」にするとリロードするだけで変更が反映されます( コードと同期されます)
「新しいデプロイ」では、変更するたびにテプロイする必要があります(バージョン付きデプロイ)
*開発中は「デプロイをテスト」で取得したURLで変更を確認します
*本番では、完成したコードを「新しいデプロイ」で公開したURLを使います
*バージョン付きデプロイはアーカイブできますが、削除はできません
クライアント側の実装
「index.html」を編集してサーバー側に検索キーワードを送り、サーバー側から返却された結果を表示します
サーバー側に検索キーワードを送る準備をします
*スタイルはBootstrapを使って見た目を整えました
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<!-- Bootstrapの読み込み -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<div class="container my-4">
<div class="col-auto d-flex justify-content-center" >
<input type="text" class="form-control w-75" placeholder="検索キーワード">
<button type="submit" class="btn btn-primary">検索する</button>
</div>
<!-- 検索結果を表示 -->
<ul id="contents" class="d-flex flex-wrap justify-content-around mt-3" style="padding:0"></ul>
</div>
<script>
const contents = document.querySelector('#contents');
const searchTerm = document.querySelector('input');
const btn = document.querySelector('button');
//検索ボタンをクリックした時
btn.addEventListener('click', (e)=>{
e.preventDefault();
//入力された値を取得してURLエンコードします
let search = encodeURIComponent(searchTerm.value)
console.log(search);
});
</script>
</body>
</html>
余談:URL全体をエンコードする場合はencodeURI
パラメータなどの部分的な文字列をエンコードする場合はencodeURIComponent
日本語で検索してコンソールで確認
URLエンコーディングされていればOK
GAS関数とやり取りできるように実装します
google.script.run.withSuccessHandler(callback).GAS関数
callbackはGAS関数(searchWord)が応答したときに実行するクライアント側の関数
callbackの引数にGAS関数(searchWord)が返す値が入ります
btn.addEventListener('click', (e)=>{
e.preventDefault();
let search = encodeURIComponent(searchTerm.value)
//Gas関数searchWordに検索ワードを渡し、YouTubeAPIで取得した結果はonSuccessで処理
google.script.run.withSuccessHandler(onSuccess).searchWord(search);
});
//onSuccess関数
function onSuccess(results) {
//必要な情報をまとめたオブジェクトの配列(data)を作る
let data = results.items.map(result => {
return {
id: result.id.videoId,
title: result.snippet.title,
date: result.snippet.publishTime,
description: result.snippet.description,
img: result.snippet.thumbnails.medium.url
}
})
console.log(data)
//ここで取得したdataからHTMLをレンダリングする関数を呼び出す render(data)
}
備考:google.script.run
は「HTMLサービスページ」が「GAS関数」を呼び出せるようにする非同期のクライアント側 「JavaScript API 」ですwithSuccessHandler(callback)
はクライアント側のコードは「GAS関数」の完了を待たずに次に進むため、引数に「GAS関数が応答したときに実行するクライアント側のコールバック関数」を指定できます
「GAS関数」が値を返す場合、その値を引数として新しい関数に渡します
YouTubeAPIで取得した結果からHTMLをレンダリングする関数
function render(data) {
contents.innerHTML = '';
let html = data.reduce((accu, str) => {
return accu + `
<li class="card mb-2" style="width: 18rem;">
<a href="http://www.youtube.com/watch?v=${str.id}" target="_blank">
<img src="${str.img}" class="card-img-top"></a>
<div class="card-body">
<h5 class="card-title">${str.title}</h5>
<p class="card-text">${str.description}</p>
<p class="card-text">${str.date.split('T')[0]}</p>
</div></li>
`
}, '')
contents.innerHTML = html
}
2023年11月追記:Google Apps Scriptのセキュリティポリシーが更新されたようで、以前は許可されていた innerHTMLの使用が、CSPによってブロックされているようなので、コードを変更しました
*注意:innerHTMLを使っているコードはエラーになります
全てのコードを見る
function doGet(){
return HtmlService.createHtmlOutputFromFile('index');
}
function searchWord(keyword) {
const results = YouTube.Search.list('id,snippet', {
maxResults:30,
q:decodeURIComponent(keyword),
//再生回数の多い順
order:'viewCount'
});
return results;
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<!-- Bootstrapの読み込み -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<div class="container my-4">
<div class="col-auto d-flex justify-content-center" >
<input type="text" class="form-control w-75" placeholder="検索キーワード">
<button type="submit" class="btn btn-primary">検索する</button>
</div>
<!-- 検索結果を表示 -->
<ul id="contents" class="d-flex flex-wrap justify-content-around mt-3" style="padding:0"></ul>
</div>
<script>
const searchTerm = document.querySelector('input');
const btn = document.querySelector('button');
//クリックしたときの処理
btn.addEventListener('click', (e)=>{
e.preventDefault();
//入力された値を取得してURLエンコードします
let search = encodeURIComponent(searchTerm.value)
//Gas関数searchWordに検索ワードを渡し、YouTubeAPIの検索結果はonSuccessで処理
google.script.run.withSuccessHandler(onSuccess).searchWord(search);
});
//onSuccess関数
function onSuccess(results) {
let data = results.items.map(result => {
return {
id: result.id.videoId,
title: result.snippet.title,
date: result.snippet.publishTime,
description: result.snippet.description,
img: result.snippet.thumbnails.medium.url
}
})
render(data)
}
function render(data) {
contents.innerHTML = '';
let html = data.reduce((accu, str) => {
return accu + `
<li class="card mb-2" style="width: 18rem;">
<a href="http://www.youtube.com/watch?v=${str.id}" target="_blank">
<img src="${str.img}" class="card-img-top"></a>
<div class="card-body">
<h5 class="card-title">${str.title}</h5>
<p class="card-text">${str.description}</p>
<p class="card-text">${str.date.split('T')[0]}</p>
</div></li>
`
}, '')
contents.innerHTML = html
}
</script>
</body>
</html>
変更後のコード
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<!-- Bootstrapの読み込み -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<div class="container my-4">
<div class="col-auto d-flex justify-content-center" >
<input type="text" class="form-control w-75" placeholder="検索キーワード">
<button type="submit" class="btn btn-primary">検索する</button>
</div>
<!-- 検索結果を表示 -->
<ul id="contents" class="d-flex flex-wrap justify-content-around mt-3" style="padding:0"></ul>
</div>
<script>
const contents = document.querySelector('#contents');
const searchTerm = document.querySelector('input');
const btn = document.querySelector('button');
//クリックしたときの処理
btn.addEventListener('click', (e)=>{
e.preventDefault();
//入力された値を取得してURLエンコードします
let search = encodeURIComponent(searchTerm.value)
//Gas関数searchWordに検索ワードを渡し、YouTubeAPIの検索結果はonSuccessで処理
google.script.run.withSuccessHandler(onSuccess).searchWord(search);
google.script.run.withFailureHandler(onFailure).searchWord(search);
});
//onFailure
function onFailure(error) {
while (contents.firstChild) {
contents.removeChild(contents.firstChild);
}
const errorMsg = document.createElement('p');
errorMsg.textContent = `ERROR: ${error.message}`;
contents.appendChild(errorMsg);
}
//onSuccess関数
function onSuccess(results) {
while (contents.firstChild) {
contents.removeChild(contents.firstChild);
}
results.forEach(item => {
const id = item.items[0].id;
const title = item.items[0].snippet.title;
const date = item.items[0].snippet.publishedAt;
const description = item.items[0].snippet.description.slice(0, 100);
const imgURL = item.items[0].snippet.thumbnails.medium.url;
const duration = item.items[0].contentDetails.duration.replace('PT', '').replace('S','秒').replace('M', '分').replace('H', '時間');
const viewCount = item.items[0].statistics.viewCount;
const likeCount = item.items[0].statistics.likeCount;
const commentCount =item.items[0].statistics.commentCount;
const listItem = document.createElement('li');
listItem.className = "card mb-2";
listItem.style.width = "18rem";
const anchor = document.createElement('a');
anchor.href = 'http://www.youtube.com/watch?v=' + id;
anchor.target = "_blank";
const image = document.createElement('img');
image.src = imgURL;
image.className = "card-img-top";
anchor.appendChild(image);
listItem.appendChild(anchor);
const cardBody = document.createElement('div');
cardBody.className = "card-body";
const dateSpan = document.createElement('span');
dateSpan.textContent = date.split('T')[0];
cardBody.appendChild(dateSpan);
const titleHeading = document.createElement('h5');
titleHeading.className = "card-title";
titleHeading.textContent = title;
cardBody.appendChild(titleHeading);
const descriptionParagraph = document.createElement('p');
descriptionParagraph.className = "card-text";
descriptionParagraph.textContent = description;
cardBody.appendChild(descriptionParagraph);
const detailsParagraph = document.createElement('p');
detailsParagraph.className = "card-text";
const durationSpan = document.createElement('span');
durationSpan.textContent = duration;
detailsParagraph.appendChild(durationSpan);
detailsParagraph.appendChild(document.createElement('br'));
const viewCountSpan = document.createElement('span');
viewCountSpan.textContent = `再生回数:${viewCount}`;
detailsParagraph.appendChild(viewCountSpan);
detailsParagraph.appendChild(document.createElement('br'));
const likeCountSpan = document.createElement('span');
likeCountSpan.textContent = `いいね:${likeCount}`;
detailsParagraph.appendChild(likeCountSpan);
detailsParagraph.appendChild(document.createElement('br'));
const commentCountSpan = document.createElement('span');
commentCountSpan.textContent = `コメントの数:${commentCount}`;
detailsParagraph.appendChild(commentCountSpan);
detailsParagraph.appendChild(document.createElement('br'));
cardBody.appendChild(detailsParagraph);
listItem.appendChild(cardBody);
contents.appendChild(listItem);
});
}
</script>
</body>
</html>
Videos: list
ビデオの詳細データを取得
動画の長さも表示したかったのでその場合は
Videos: listで各動画の詳細データを取得する必要があります
ついでに、再生された回数・高く評価したユーザーの数・コメントの数も表示しますYouTube.Videos.list('snippet, statistics, contentDetails', {id:result.id.videoId})
詳細情報も表示する場合
function doGet(){
return HtmlService.createHtmlOutputFromFile('index');
}
function searchWord(keyword) {
const results = YouTube.Search.list('id', {
maxResults:30,
q:decodeURIComponent(keyword),
//再生回数の多い順
order:'viewCount'
});
const newResults = results.items.map(result=>{
if(result.id.videoId){
return YouTube.Videos.list('snippet, statistics, contentDetails', {id:result.id.videoId})
}
})
//nullは除く
return newResults.filter(Boolean)
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<!-- Bootstrapの読み込み -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<div class="container my-4">
<div class="col-auto d-flex justify-content-center" >
<input type="text" class="form-control w-75" placeholder="検索キーワード">
<button type="submit" class="btn btn-primary">検索する</button>
</div>
<!-- 検索結果を表示 -->
<ul id="contents" class="d-flex flex-wrap justify-content-around mt-3" style="padding:0"></ul>
</div>
<script>
const searchTerm = document.querySelector('input');
const btn = document.querySelector('button');
//クリックしたときの処理
btn.addEventListener('click', (e)=>{
e.preventDefault();
//入力された値を取得してURLエンコードします
let search = encodeURIComponent(searchTerm.value)
//Gas関数searchWordに検索ワードを渡し、YouTubeAPIの検索結果はonSuccessで処理
google.script.run.withSuccessHandler(onSuccess).searchWord(search);
});
//onSuccess関数
function onSuccess(results) {
let data = results.map(result => {
// console.log(result)
return {
id: result.items[0].id,
title: result.items[0].snippet.title,
date: result.items[0].snippet.publishedAt,
//全文表示されるので100文字だけ表示
description: result.items[0].snippet.description.slice(0, 100),
img: result.items[0].snippet.thumbnails.medium.url,
duration:result.items[0].contentDetails.duration.replace('PT', '').replace('S','秒').replace('M', '分').replace('H', '時間'),
viewCount:result.items[0].statistics.viewCount,
likeCount:result.items[0].statistics.likeCount,
commentCount:result.items[0].statistics.commentCount,
}
})
render(data)
}
function render(data) {
contents.innerHTML = '';
let html = data.reduce((accu, str) => {
return accu + `
<li class="card mb-2" style="width: 18rem;">
<a href="http://www.youtube.com/watch?v=${str.id}" target="_blank">
<img src="${str.img}" class="card-img-top"></a>
<div class="card-body">
<h5 class="card-title">${str.title}</h5>
<p class="card-text">${str.description}</p>
<p class="card-text">${str.date.split('T')[0]}</p>
<p class="card-text">
<span>${str.duration}</span></br>
<span>再生回数:${str.viewCount}</span></br>
<span>いいね:${str.likeCount}</span></br>
<span>コメントの数:${str.commentCount}</span></br>
</p>
</div></li>
`
}, '')
contents.innerHTML = html
}
</script>
</body>
</html>
「Videos: list」で各動画の詳細データを取得するとキーワードを絞らないとエラーで検索できないことがあり
ごちょごちょしていたら1日の上限を超えてしまいました〜
「videoIdがないもの」が含まれることが原因のようなので、とりあえずは「videoId」があればという条件をつけたりしています
備考:YouTube Data APIを使う場合、1日に使えるクォータの量は10,000のようなのですが(ちなみにsearchは100・videosは1(1回の検索で30件)で計算するみたいなので75回くらいが目安なのかな…?)
太平洋時間午前0時(日本時間では午後4時~5時)にリセットされるようです