11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-04-21

テレビ離れが叫ばれて久しい今日この頃

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

そこのあなた!

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

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

サブスク動画配信サービスがテレビよりも優れている点は何か:thinking:

  • いつでも好きな時に好きな番組が見れる
  • 見れる作品数が多い

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

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

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

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

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

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

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

使用したもの

  • Node-RED(Railwayでデプロイ)
  • LINE Bot(Messaging API)
  • NHK番組表API(Program Genre API(Ver.2))
  • ChatGPT API

こんな感じの機能を作ろう(設計)

image.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
    • 画像の通り、数値のみ取得するのは難しそうということを考慮したうえで実装していきます
  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

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

Tokenにはご自身で取得したChatGPTのAPIキーを設定してください。
ChatGPT APIの使い方や料金体系については過去記事で解説しているので、お時間ある方はご参照ください。

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を取得するためのコードです。

サンプルコード
setGenreId
const userText = msg.payload;
let genreId = '';

// 解説1
// 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';
}

// 解説2
// 日付を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;

// 解説1

こんな感じの機能を作ろう(設計) パート の 2. ChatGPTが入力された番組をジャンル分けするでも紹介した通り、ChatGPTはジャンルIDの数値のみ返答することは難しいようです。
したがって、以下の条件分岐をJavaScriptで実現しました!

  • ChatGPTの回答に数値が含まれている=ジャンル分けに成功していたら、該当のジャンルIDを返す
  • ChatGPTの回答に数値が含まれていない=ジャンル分けに失敗していたら、9999を返す(ユーザが番組名でない単語を入力した場合などです。ジャンルIDと一致しない値ならなんでもOK:ok_hand:

// 解説2

formatDate関数は、先人が作ってくださった関数を参考に一部改変しています。
日付をYYYY-MM-DDの書式で返す関数 (JavaScript)

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

③入力値が番組名か?

switchノードを利用しています。
ジャンルIDが9999かどうかで条件分岐させます。
image.png

③-A番組名でない:replyGreeting

functionノードを利用しています。
ジャンルIDが取得できなかった場合はNHK番組表APIを叩くことができないため、LINE Botがデフォルトのあいさつ文を返すように設定します。

replyGreeting
msg.payload = '好きな番組名を教えてください。おすすめの番組を紹介します!'
return msg;

③-B-1番組名である:番組表APIにリクエスト

http requestノードを利用しています。
{{payload.genreId}}と{{payload.date}}には、②setGenreIdで設定した値が入ります。
今回、地域IDは130:東京、サービスIDはg1:NHK総合1で固定しています。
image.png

URL
https://api.nhk.or.jp/v2/pg/genre/130/g1/{{payload.genreId}}/{{payload.date}}.json?key=[ご自身で取得したNHK番組表APIキー]
取得できる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": "NHK総合1",
          "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": "なれそめの数だけいろんな人生の楽しみ方がある!今回は「元警察官&元消防士!同性カップル」田村淳、LiLiCo、那須雄登、アンジェリーナ1/3、語り:水瀬いのり",
        "content": "なれそめの数だけいろんな人生の楽しみ方がある!今回は「元警察官と元消防士の同性カップル」自分が男性のことが好きなことを認めれなかったKANE(カネ)さん、KOTFE(コッフェ)さんの気持ちをなかなか受け入れられなかった。しかし「彼を失うことの怖さ」に気づき…。★田村淳、LiLiCo、那須雄登(美 少年/ジャニーズJr.)、アンジェリーナ1/3(Gacharic Spin)★語り:水瀬いのり",
        "act": "【司会】田村淳,【語り】水瀬いのり,【ゲスト】LiLiCo,アンジェリーナ1/3,那須雄登,KANE,KOTFE",
        "genres": [
          "0502"
        ]
      }
    ]
  }
}

③-B-2:番組名である:replyProgram

functionノードを利用しています。

replyProgram
// 解説1
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;
// 解説2
var start_time = msg.payload.list.g1[i].start_time.split('T');
// 2-1
var start_date = start_time[0].split('-');
// 2-2
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;
// 解説1

JSONの要素数以下の乱数iを生成し、取得する番組情報をランダムで選択しています。

// 解説2

番組開始時間を○年○月○日○時○分の形に成形するためのコードです。
start_timeは2023-04-22T02:46:00+09:00の形になっているため、

  • 2-1:Tの前後で2023-04-2202:46:00+09:00に分ける
  • 2-2:年月日はハイフンで分け、時刻はコロンで分けて時分(02と4)のみ使用する

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

完成!

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

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

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

11
3
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
11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?