LoginSignup
14
2

More than 3 years have passed since last update.

LINE BOTでYouTubeの検索をしてみたよ

Last updated at Posted at 2020-10-11

実装結果

実行結果は以下の通り、上手くデータを取ることができました。
テキストメッセージ以外は、固定メッセージを返すようにしています。
tgtns-zwkp0.gif

概要

先日、LINE BOT の開発を教わったので、実際に作成してみました。
教わったのは簡単なテキストを返すものでしたが、LINE Messaging API でテンプレートメッセージのカルーセルを使ってみました。
LINE BOT の初期設定(事前準備)は以下を参考にしています。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest

目的

私はよくYouTubeで動画を見るので、簡単なYouTube検索ボットを作ってみました。
単純メッセージのレスポンスだとつまらないと思ったので、カルーセル形式に挑戦してみました。

環境

Visual Studio Code v1.49.0
node.js v14.9.0
ngrok v2.3.35

概要

自PCを(Node.js)サーバに見立て、LINEサーバからWebhookを受け取る為のトンネリングサービスはngrokを利用しています。
データの流れについては、公式ページ上記の参考サイトをみると何となく分かります。

今回、YouTubeのデータ検索にはYouTube API DATA v3 を利用し、LINE BOT に送ったメッセージでキーワード検索をしています。APIのレスポンスはaxiosで受け取り、関連する上位3件の動画情報から、サムネイル表示、タイトル表示、動画URLへ飛ぶようにしてみようと思いました。

コード

.js
'use strict'; // おまじない
// ########################################
//               初期設定など
// ########################################
// パッケージを使用します
const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');

const PORT = process.env.PORT || 3000;  // ローカル(自分のPC)でサーバーを公開するときのポート番号
const YoutubeURL = 'https://www.youtube.com/watch?v=';   // YouTubeURL
const YoutubeAPIKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; //YouTubeAPIKey

// Messaging APIで利用するクレデンシャル(秘匿情報)です。
const config = {
    channelSecret: '99999999999999999999999999999999',  //作成したBotのチャネルシークレット
    channelAccessToken: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' //作成したBotのチャネルアクセストークン
};

// ########## ▼▼▼ サンプル関数 ▼▼▼ ##########
const MainFunction = async (event) => {

    const userText = event.message.text;    // ユーザーメッセージ
    axios.defaults.baseURL = 'https://www.googleapis.com/youtube/v3';    // YouTube Data API

    // 「リプライ」を使って先に返事しておきます
    await client.replyMessage(event.replyToken, {
        type: 'text',
        text: '調べています……'
    });

    let Title = [];
    let IdUrl = [];
    let ImageUrl = [];
    try {
        // axiosでAPIを叩きます(少し時間がかかる・ブロッキングする)
        const res = await axios.get('/search?part=snippet&q=' + encodeURIComponent(userText) + '&key=' + YoutubeAPIKey);

        // 関連の高い動画を3件返す
        for (let i=0; i<3; i++) {
            Title.push(res.data.items[i].snippet.title);    // タイトル
            // 文字数が50より大きかったら末尾に...を付けたす ※MessagingApiの制限60文字を超えるとエラーになるので。
            if (Title[i].length > 50) {
                Title[i] = Title[i].substr(0, 50) + '...';
            }
            IdUrl.push(YoutubeURL + res.data.items[i].id.videoId);  // 動画URL
            ImageUrl.push(res.data.items[i].snippet.thumbnails.medium.url);    // サムネ画像
        }

    } catch (error) {
        // APIからエラーが返ってきたらターミナルに表示する
        return client.pushMessage(event.source.userId, {
            type: 'text',
            text: '検索中にエラーが発生しました。ごめんね。',
        });
        console.error(error);
    }

    // 「プッシュ」で後からユーザーに通知(カルーセル形式ガリ書き。)
    return client.pushMessage(event.source.userId, {
        "type": "template",
        "altText": "this is a carousel template",
        "template": {
            "type": "carousel",
            "columns": [
                {
                  "thumbnailImageUrl": ImageUrl[0],
                  "text": Title[0],
                  "defaultAction": {
                      "type": "uri",
                      "label": "動画を見に行く",
                      "uri": IdUrl[0]
                  },
                  "actions": [
                      {
                          "type": "uri",
                          "label": "動画を見に行く",
                          "uri": IdUrl[0]
                      }
                  ]
                },
                {
                  "thumbnailImageUrl": ImageUrl[1],
                  "text": Title[1],
                  "defaultAction": {
                      "type": "uri",
                      "label": "動画を見に行く",
                      "uri": IdUrl[1]
                  },
                  "actions": [
                      {
                          "type": "uri",
                          "label": "動画を見に行く",
                          "uri": IdUrl[1]
                      }
                  ]
                },
                {
                    "thumbnailImageUrl": ImageUrl[2],
                    "text": Title[2],
                    "defaultAction": {
                        "type": "uri",
                        "label": "動画を見に行く",
                        "uri": IdUrl[2]
                    },
                    "actions": [
                        {
                            "type": "uri",
                            "label": "動画を見に行く",
                            "uri": IdUrl[2]
                        }
                    ]
                  }
            ],
            "imageAspectRatio": "rectangle",
            "imageSize": "cover"
        }
      });
};
// ########## ▲▲▲ サンプル関数 ▲▲▲ ##########


// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################
// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
    // 受信したWebhookが「テキストメッセージ以外」であれば固定メッセージを返す
    if (event.type !== 'message' || event.message.type !== 'text') {
        //return Promise.resolve(null);
        return client.replyMessage(event.replyToken, {
            type: 'text',
            text: 'YouTubeで検索しようか?\n検索キーワードは?'
        });
    }
    // サンプル関数を実行します
    return MainFunction(event);
}


// ########################################
//          Expressによるサーバー部分
// ########################################
// expressを初期化します
const app = express();

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
    // Webhookの中身を確認用にターミナルに表示します
    console.log(req.body.events);

    // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行
    if (req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff') {
        res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します
        console.log('検証イベントを受信しました!'); // ターミナルに表示します
        return; // これより下は実行されません
    }

    // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
    // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
    Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// (環境によってはローカルネットワーク内にも公開されます)
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);

苦戦した箇所

・YouTube APIの仕様理解
以下サイトを参考に、APIの有効化と仕様を確認しましたが、上手くデータを取れず少し苦戦しました。
原因は余計なパラメータを渡していた為でした。(APIの仕様書を読み解くのが、なかなか慣れない、、、)
 Youtube Data API Key の取得手順
 YouTube Data API | Google Developers
 動画の検索 | YouTube Data API v2

・LINE Messaging APIのカルーセル表示
テンプレートメッセージのカルーセル表示もかなり苦戦しました。YouTube APIからのデータは取れているのにエラーで上手くいかず、原因箇所特定に苦戦しました。
原因は、タイトルの文字数制限があり、超えていた為でした。なので、文字数判定を入れて無理やり切るように工夫してます。
    Title.push(res.data.items[i].snippet.title);    // タイトル
       // 文字数が50より大きかったら末尾に...を付けたす ※MessagingApiの制限60文字を超えるとエラーになるので。
       if (Title[i].length > 50) {
            Title[i] = Title[i].substr(0, 50) + '...';
       }

振り返り

課題はAPIの利用方法がまだ慣れない点だと強く感じました。
JavaScript、JSONがまだ良く分かっていない、自分の課題が見えました。
これからも書き続けてコツを掴めたら、もっとできることが増えていって楽しいだろうなぁと感じました。
(おすすめ提案とかもさせてみたかったけど、時間的に厳しかった。)

14
2
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
14
2