世はまさに大タイパ時代！LINE Botがあなた好みの番組を提案します📺

## テレビ離れが叫ばれて久しい今日この頃
子どもの頃、テレビを見ることに全力を注ぎ、
兄弟姉妹でチャンネル権を争ってケンカしたり、
怒った親にテレビを禁じられて大泣きしたりしていた

**そこのあなた！**

「最近面白いテレビないなぁ:unamused:」なんて、テレビから遠ざかってしまってはいないでしょうか。

一方で、今や多くの方が利用しているのが**Netflix**や**Amazon Prime Video**などの**サブスク動画配信サービス**。

**サブスク動画配信サービスがテレビよりも優れている点は何か:thinking:？**
- いつでも好きな時に好きな番組が見れる
- 見れる作品数が多い

などがありますが、私はある観点に着目しました。

**レコメンド機能がある:point_up:**

何事にも**タイパ**が重視されるこの時代:stopwatch:
忙しい私たちには、**新たな作品を能動的に探している暇などないのです！！**

実際私はAmazon Prime Videoを利用するとき、「あなたが興味のありそうな」カテゴリに表示されている動画から、新たなおもしろ作品に出会うことがほとんど。

したがって、**普段見ているテレビ番組から、おすすめのテレビ番組を提案してくれるツール**があれば、テレビを楽しむ人が増えるのではないでしょうか。

テレビを楽しむ人が増えれば番組制作の予算も増え、面白い番組が増えるでしょう！

**さあ、甦れ！！テレビの黄金時代:tv::sparkles:**

## 使用したもの
- Node-RED（Railwayでデプロイ）
- LINE Bot（Messaging API）
- [NHK番組表API](https://api-portal.nhk.or.jp/)（Program Genre API(Ver.2)）
- ChatGPT API

## こんな感じの機能を作ろう（設計）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958610/b2f5b513-8ec2-8b58-6ac9-e246ca02639e.png)
1. ユーザがLINE Botで好きな番組名を入力する

2. ChatGPTが入力された番組をジャンル分けする
    - NHK番組表APIのリクエストには、4桁の**ジャンルID**を引数として設定する必要があります
    ```:NHK番組表APIリクエスト
    https://api.nhk.or.jp/v2/pg/genre/{地域ID}/{サービスID(総合,Eテレetc)}/{ジャンルID}/{日付}.json?key={apikey}
    ```
    - ジャンル分けは**GhatGPT**先生にお願いします:pray:
    - ChatGPTはどんな回答をくれるのか、Web版でテスト
    - 引数として必要な**ジャンルID**の数値のみを回答してほしいが・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958610/ae75046e-a83a-d223-88c4-1b4f3123155e.png)
    - 画像の通り、**数値のみ取得するのは難しそう**ということを考慮したうえで実装していきます

3. **NHK番組表API**を叩き、該当ジャンルの番組情報を取得する

4. 取得した情報をいい感じに編集し、LINE Botに返す

※参考：ジャンルIDとジャンルの対応（NHK番組表API）
| ジャンルID | ジャンル |
| --- | --- |
| 0000 | ニュース |
| 0100 | スポーツ(スポーツニュース) |
| 0205 | 情報/ワイドショー(グルメ・料理) |
| 0300 | ドラマ(国内ドラマ) |
| 0409 | 音楽(童謡・キッズ) |
| 0502 | バラエティ(トークバラエティ) |
| 0600 | 映画(洋画) |
| 0700 | アニメ/特撮(国内アニメ) |
| 0800 | ドキュメンタリー/教養(社会・時事) |
| 0903 | 劇場/公演(落語・演芸) |
| 1000 | 趣味/教育(旅・釣り・アウトドア) |
| 1100 | 福祉(高齢者) |

## 機能を実装しよう
ノードに振った番号①～③の順に解説していきます！
（誰か教えてきれいにノードを配置する方法:joy:）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958610/30ebaebe-2055-53f8-5bf7-bd547f59a8c2.png)

```json:JSON
[{"id":"088145566f68e247","type":"tab","label":"フロー 4","disabled":false,"info":"","env":[]},{"id":"05197443aab6d823","type":"debug","z":"088145566f68e247","name":"デバッグ1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":60,"wires":[]},{"id":"88932518a5be1a13","type":"function","z":"088145566f68e247","name":"②setGenreId","func":"const userText = msg.payload;\nlet genreId = '';\n\n// 0000: ニュース\nif (userText.indexOf('0000') > -1) {\n    genreId = '0000';\n// 0100: スポーツ(スポーツニュース)\n} else if (userText.indexOf('0100') > -1) {\n    genreId = '0100';\n// 0205: 情報 / ワイドショー(グルメ・料理)\n} else if (userText.indexOf('0205') > -1) {\n    genreId = '0205';\n// 0300: ドラマ(国内ドラマ)\n} else if (userText.indexOf('0300') > -1) {\n    genreId = '0300';\n// 0409: 音楽(童謡・キッズ)\n} else if (userText.indexOf('0409') > -1) {\n    genreId = '0409';\n// 0502: バラエティ(トークバラエティ)\n} else if (userText.indexOf('0502') > -1) {\n    genreId = '0502';\n// 0600: 映画(洋画)\n} else if (userText.indexOf('0600') > -1) {\n    genreId = '0600';\n// 0700: アニメ / 特撮(国内アニメ)\n} else if (userText.indexOf('0700') > -1) {\n    genreId = '0700';\n// 0800: ドキュメンタリー / 教養(社会・時事)\n} else if (userText.indexOf('0800') > -1) {\n    genreId = '0800';\n// 0903: 劇場 / 公演(落語・演芸)\n} else if (userText.indexOf('0903') > -1) {\n    genreId = '0903';\n// 1000: 趣味 / 教育(旅・釣り・アウトドア)\n} else if (userText.indexOf('1000') > -1) {\n    genreId = '1000';\n// 1100: 福祉(高齢者)\n} else if (userText.indexOf('1100') > -1) {\n    genreId = '1100';\n} else {\n    genreId = '9999';\n}\n\n// 日付をYYYY-MM-DDの書式で返すメソッド\nfunction formatDate(dt) {\n    dt.setHours(dt.getHours() + 9);\n    var y = dt.getFullYear();\n    var m = ('00' + (dt.getMonth() + 1)).slice(-2);\n    var d = ('00' + dt.getDate()).slice(-2);\n    return (y + '-' + m + '-' + d);\n}\n\nmsg.payload = {\n    \"genreId\": genreId,\n    \"date\": formatDate(new Date())\n};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":110,"y":120,"wires":[["e47ded1cd7965421","57a71f4aec71c79d"]]},{"id":"e47ded1cd7965421","type":"debug","z":"088145566f68e247","name":"デバッグ2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":120,"wires":[]},{"id":"b334e2bf2d2da7e4","type":"debug","z":"088145566f68e247","name":"デバッグ3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":180,"wires":[]},{"id":"80eb97197b3ae0f0","type":"http request","z":"088145566f68e247","name":"③-B-1番組名である：番組表APIにリクエスト","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://api.nhk.or.jp/v2/pg/genre/130/g1/{{payload.genreId}}/{{payload.date}}.json?key=[APIキーを入力]","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":320,"y":260,"wires":[["bf937e80adaac941","782b6edf78f7de01"]]},{"id":"bf937e80adaac941","type":"function","z":"088145566f68e247","name":"③-B-2：番組名である：replyProgram","func":"var count = Object.keys(msg.payload.list.g1).length;\nvar i = Math.floor((Math.random() * (count + 1)));\nvar programTitle = msg.payload.list.g1[i].title;\nvar programContent = msg.payload.list.g1[i].content;\nvar start_time = msg.payload.list.g1[i].start_time.split('T');\nvar start_date = start_time[0].split('-');\nvar start_hour = start_time[1].split(':');\nmsg.payload = 'あなたにおすすめの番組は・・・\\n「' \n    + programTitle + '」\\n\\n' \n    + programContent + '\\n\\n' \n    + start_date[0] + '年' + start_date[1] + '月' + start_date[2] + '日'\n    + start_hour[0] + '時' + start_hour[1] + '分スタート！';\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":320,"wires":[["a2bec31e483b2ae4","b334e2bf2d2da7e4"]]},{"id":"650ce299cc0ac60f","type":"Webhook","z":"088145566f68e247","name":"","url":"/webhook","x":100,"y":60,"wires":[["6fcf7009580661ba"]]},{"id":"a2bec31e483b2ae4","type":"ReplyMessage","z":"088145566f68e247","name":"","replyMessage":"","x":680,"y":320,"wires":[]},{"id":"6fcf7009580661ba","type":"simple-chatgpt","z":"088145566f68e247","name":"①ChatGPT APIにリクエスト","Token":"Tokenを入力","SystemSetting":"以下のジャンルに分類してください。「ID: ジャンル」となっているので、IDの4桁の数値のみ回答お願いします。 0000: ニュース 0100: スポーツ(スポーツニュース) 0205: 情報/ワイドショー(グルメ・料理) 0300: ドラマ(国内ドラマ) 0409: 音楽(童謡・キッズ) 0502: バラエティ(トークバラエティ) 0600: 映画(洋画) 0700: アニメ/特撮(国内アニメ) 0800: ドキュメンタリー/教養(社会・時事) 0903: 劇場/公演(落語・演芸) 1000: 趣味/教育(旅・釣り・アウトドア) 1100: 福祉(高齢者)","x":300,"y":60,"wires":[["05197443aab6d823","88932518a5be1a13"]]},{"id":"57a71f4aec71c79d","type":"switch","z":"088145566f68e247","name":"③入力値が番組名か？","property":"payload.genreId","propertyType":"msg","rules":[{"t":"eq","v":"9999","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":140,"y":180,"wires":[["af6dfdd9aaa450a7"],["80eb97197b3ae0f0"]]},{"id":"af6dfdd9aaa450a7","type":"function","z":"088145566f68e247","name":"③-A番組名でない：replyGreeting","func":"msg.payload = '好きな番組名を教えてください。おすすめの番組を紹介します！'\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":180,"wires":[["a2bec31e483b2ae4","b334e2bf2d2da7e4"]]},{"id":"782b6edf78f7de01","type":"debug","z":"088145566f68e247","name":"デバッグ4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":260,"wires":[]}]
```

### ①ChatGPT APIにリクエスト
**node-red-contrib-simple-chatgpt**パレットの**simple-chatgpt**ノードを利用しています。
![chatgpt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958610/1ced7ee0-3394-9a57-78f9-802ded015092.png)

**Token**にはご自身で取得したChatGPTのAPIキーを設定してください。
ChatGPT APIの使い方や料金体系については[過去記事](https://qiita.com/iwakatakagundan/items/8292cc1bb215cecea304)で解説しているので、お時間ある方はご参照ください。

**SystemSetting**には以下の値を設定しています。
ユーザが入力した番組を、ChatGPT先生がNHK番組表APIのジャンルに分類します。
```:SystemSetting
以下のジャンルに分類してください。「ID: ジャンル」となっているので、IDの4桁の数値のみ回答お願いします。 0000: ニュース 0100: スポーツ(スポーツニュース) 0205: 情報/ワイドショー(グルメ・料理) 0300: ドラマ(国内ドラマ) 0409: 音楽(童謡・キッズ) 0502: バラエティ(トークバラエティ) 0600: 映画(洋画) 0700: アニメ/特撮(国内アニメ) 0800: ドキュメンタリー/教養(社会・時事) 0903: 劇場/公演(落語・演芸) 1000: 趣味/教育(旅・釣り・アウトドア) 1100: 福祉(高齢者)
```

### ②setGenreId
**function**ノードを利用しています。
NHK番組表APIリクエストの引数である**ジャンルID**を取得するためのコードです。
<details><summary>サンプルコード</summary>

```JavaScript:setGenreId
const userText = msg.payload;
let genreId = '';

// 解説１
// 0000: ニュース
if (userText.indexOf('0000') > -1) {
    genreId = '0000';
// 0100: スポーツ(スポーツニュース)
} else if (userText.indexOf('0100') > -1) {
    genreId = '0100';
// 0205: 情報 / ワイドショー(グルメ・料理)
} else if (userText.indexOf('0205') > -1) {
    genreId = '0205';
// 0300: ドラマ(国内ドラマ)
} else if (userText.indexOf('0300') > -1) {
    genreId = '0300';
// 0409: 音楽(童謡・キッズ)
} else if (userText.indexOf('0409') > -1) {
    genreId = '0409';
// 0502: バラエティ(トークバラエティ)
} else if (userText.indexOf('0502') > -1) {
    genreId = '0502';
// 0600: 映画(洋画)
} else if (userText.indexOf('0600') > -1) {
    genreId = '0600';
// 0700: アニメ / 特撮(国内アニメ)
} else if (userText.indexOf('0700') > -1) {
    genreId = '0700';
// 0800: ドキュメンタリー / 教養(社会・時事)
} else if (userText.indexOf('0800') > -1) {
    genreId = '0800';
// 0903: 劇場 / 公演(落語・演芸)
} else if (userText.indexOf('0903') > -1) {
    genreId = '0903';
// 1000: 趣味 / 教育(旅・釣り・アウトドア)
} else if (userText.indexOf('1000') > -1) {
    genreId = '1000';
// 1100: 福祉(高齢者)
} else if (userText.indexOf('1100') > -1) {
    genreId = '1100';
} else {
    genreId = '9999';
}

// 解説２
// 日付をYYYY-MM-DDの書式で返すメソッド
function formatDate(dt) {
    dt.setHours(dt.getHours() + 9);
    var y = dt.getFullYear();
    var m = ('00' + (dt.getMonth() + 1)).slice(-2);
    var d = ('00' + dt.getDate()).slice(-2);
    return (y + '-' + m + '-' + d);
}

msg.payload = {
    "genreId": genreId,
    "date": formatDate(new Date())
};
return msg;
```
</details>

#### // 解説１
**こんな感じの機能を作ろう(設計)** パート の **2. ChatGPTが入力された番組をジャンル分けする**でも紹介した通り、ChatGPTはジャンルIDの数値のみ返答することは難しいようです。
したがって、以下の条件分岐を**JavaScript**で実現しました！
- ChatGPTの回答に数値が含まれている＝**ジャンル分けに成功**していたら、**該当のジャンルID**を返す
- ChatGPTの回答に数値が含まれていない＝**ジャンル分けに失敗**していたら、**9999**を返す（ユーザが番組名でない単語を入力した場合などです。ジャンルIDと一致しない値ならなんでもOK:ok_hand:）

#### // 解説２
**formatDate**関数は、先人が作ってくださった関数を参考に一部改変しています。
[日付をYYYY-MM-DDの書式で返す関数 (JavaScript)](https://qiita.com/toshimin/items/5f13c3b4c28825219231)

Node-REDの環境では、時刻が世界標準時で取得されてしまうようです:earth_africa:
そのためこの行を追加し、**世界標準時を日本標準時に変更**しています:flag_jp:
`dt.setHours(dt.getHours() + 9);`

### ③入力値が番組名か？
**switch**ノードを利用しています。
ジャンルIDが**9999**かどうかで条件分岐させます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958610/5aa893a9-ab72-861c-4c0d-95eb3155c630.png)

#### ③-A番組名でない：replyGreeting
**function**ノードを利用しています。
ジャンルIDが取得できなかった場合はNHK番組表APIを叩くことができないため、**LINE Botがデフォルトのあいさつ文を返すように設定**します。
```JavaScript:replyGreeting
msg.payload = '好きな番組名を教えてください。おすすめの番組を紹介します！'
return msg;
```
#### ③-B-1番組名である：番組表APIにリクエスト
**http request**ノードを利用しています。
{{**payload.genreId**}}と{{**payload.date**}}には、**②setGenreId**で設定した値が入ります。
今回、地域IDは**130:東京**、サービスIDは**g1:NHK総合1**で固定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958610/e12b76d2-2976-9b68-9eca-ec1a754c0289.png)
```:URL
https://api.nhk.or.jp/v2/pg/genre/130/g1/{{payload.genreId}}/{{payload.date}}.json?key=[ご自身で取得したNHK番組表APIキー]
```
<details><summary>取得できるJSONデータサンプル</summary>

```json:JSON
{
  "list": {
    "g1": [
      {
        "id": "2023042108414",
        "event_id": "08414",
        "start_time": "2023-04-22T02:46:00+09:00",
        "end_time": "2023-04-22T03:16:00+09:00",
        "area": {
          "id": "130",
          "name": "東京"
        },
        "service": {
          "id": "g1",
          "name": "ＮＨＫ総合１",
          "logo_s": {
            "url": "//www.nhk.or.jp/common/img/media/gtv-100x50.png",
            "width": "100",
            "height": "50"
          },
          "logo_m": {
            "url": "//www.nhk.or.jp/common/img/media/gtv-200x100.png",
            "width": "200",
            "height": "100"
          },
          "logo_l": {
            "url": "//www.nhk.or.jp/common/img/media/gtv-200x200.png",
            "width": "200",
            "height": "200"
          }
        },
        "title": "超多様性トークショー！なれそめ▽元警察官＆元消防士！同性カップル",
        "subtitle": "なれそめの数だけいろんな人生の楽しみ方がある！今回は「元警察官＆元消防士！同性カップル」田村淳、ＬｉＬｉＣｏ、那須雄登、アンジェリーナ１／３、語り：水瀬いのり",
        "content": "なれそめの数だけいろんな人生の楽しみ方がある！今回は「元警察官と元消防士の同性カップル」自分が男性のことが好きなことを認めれなかったＫＡＮＥ（カネ）さん、ＫＯＴＦＥ（コッフェ）さんの気持ちをなかなか受け入れられなかった。しかし「彼を失うことの怖さ」に気づき…。★田村淳、ＬｉＬｉＣｏ、那須雄登（美　少年／ジャニーズＪｒ．）、アンジェリーナ１／３（Ｇａｃｈａｒｉｃ　Ｓｐｉｎ）★語り：水瀬いのり",
        "act": "【司会】田村淳，【語り】水瀬いのり，【ゲスト】ＬｉＬｉＣｏ，アンジェリーナ１／３，那須雄登，ＫＡＮＥ，ＫＯＴＦＥ",
        "genres": [
          "0502"
        ]
      }
    ]
  }
}
```
</details>

#### ③-B-2：番組名である：replyProgram
**function**ノードを利用しています。
```JavaScript:replyProgram
// 解説１
var count = Object.keys(msg.payload.list.g1).length;
var i = Math.floor((Math.random() * (count + 1)));
var programTitle = msg.payload.list.g1[i].title;
var programContent = msg.payload.list.g1[i].content;
// 解説２
var start_time = msg.payload.list.g1[i].start_time.split('T');
// ２－１
var start_date = start_time[0].split('-');
// ２－２
var start_hour = start_time[1].split(':');
msg.payload = 'あなたにおすすめの番組は・・・\n「' 
    + programTitle + '」\n' 
    + programContent + '\n' 
    + start_date[0] + '年' + start_date[1] + '月' + start_date[2] + '日'
    + start_hour[0] + '時' + start_hour[1] + '分スタート！';
return msg;
```
##### // 解説１
JSONの要素数以下の**乱数i**を生成し、取得する番組情報をランダムで選択しています。

##### // 解説２
番組開始時間を**○年○月○日○時○分**の形に成形するためのコードです。
start_timeは`2023-04-22T02:46:00+09:00`の形になっているため、
- ２－１：**T**の前後で`2023-04-22`と`02:46:00+09:00`に分ける
- ２－２：年月日は**ハイフン**で分け、時刻は**コロン**で分けて時分(02と4)のみ使用する

実装パートは以上です！お疲れさまでした:grin:

## 完成！
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">新たな作品を開拓する暇のないあなたへ。<br>LINE Botがあなた好みのテレビ番組を提案します📺 <a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://twitter.com/hashtag/LINEBot?src=hash&amp;ref_src=twsrc%5Etfw">#LINEBot</a> <a href="https://twitter.com/hashtag/ChatGPT?src=hash&amp;ref_src=twsrc%5Etfw">#ChatGPT</a> <a href="https://t.co/vW7U99V7kz">pic.twitter.com/vW7U99V7kz</a></p>&mdash; 新米SEちーろってぃ@新婚💍 (@chiiirotty) <a href="https://twitter.com/chiiirotty/status/1649370014204715008?ref_src=twsrc%5Etfw">April 21, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

最初に「こんにちは」と入力すると、デフォルトのあいさつ文が返ってきました！
続いて「ぐるナイ」と入力すると、「きょうの料理」を紹介されました:grinning:
ごはんつながりかな？:rice_ball:
最後に「news zero」と入力すると、「NHKニュース　おはよう日本」が紹介されました:sunny:

無事、大タイパ時代にふさわしいテレビ版のレコメンドツールが完成！
このLINE Botを使えば、自分で探しに行かずとも新たな**好き**に出会えそうですね:relaxed:

以上！最後までお読みいただきありがとうございました:hugging: