ApifyとGeminiを使ってYouTube動画情報を要約し、kintoneに自動登録しよう!

著者名:Mamoru Fujinoki( Fuji Business Software Inc. (External link)

目次

はじめに

ソーシャルメディアやWebサイトから情報を収集したい時、Webスクレイピングの技術を使って最新情報を取得できます。
しかし、情報が増えてくると、必要な情報を見つけるまでに内容の確認で時間がかかり、非効率になってしまいます。
この情報を生成AIによって要約して蓄積できれば、必要な情報を短時間で検索して作業時間の短縮が可能です。
今回は、 Apify (External link) というクラウドサービスを利用して、「kintone」のキーワードが入っているYouTube動画の情報を収集します。
Apifyとは、Webスクレイピングができるクラウドサービスです。
Apify上で「Actor」を作成し、この「Actor」を定期的に実行して特定のサイトから最新情報を取得できます。
Actorとはサーバーレスのクラウドプログラムで、Webスクレイピングやブラウザー自動化またはデータ処理等のタスクを実行します。
たとえば特定のWebページのスクレイピングやX、YouTubeなどのサービスから情報を取得するタスクを実行できます。
また、YouTube動画の字幕情報を利用して、生成AI Geminiによる動画内容の要約をGASを使って実現します。
最後にYouTube動画内容とその要約をkintoneに蓄積して管理し、のちほど結果を効率的に検索および分析できるようにします。

全体構成

① ApifyでActorを設定し、YouTubeから「kintone」についての情報を取得するよう設定します。
注:公式がメンテナンスしている YouTube Scraper (External link) を使用します。
② Actorを実行し、GASのエンドポイントにWebhookが飛ぶよう設定します。
③ GASでWebhook情報を受け取り、GeminiでYouTubeの字幕情報を要約し、kintoneへデータが入れやすい形に整形します。
④ GASからkintoneアプリに動画情報を登録します。

kintoneの設定

まずは情報の受け入れ口として、kintoneアプリの作成やWebhookの準備をします。

アプリの作成

下の画像を参考に新規でアプリを作成します。

フィールド名 フィールドの種類 フィールドコード 備考
動画タイトル 文字列(1行) video_title YouTube動画のタイトル
動画概要 文字列(複数行) video_description YouTube動画の概要テキスト
動画投稿日 日付 published_date YouTube動画の投稿日
動画投稿者 文字列(1行) channel_name YouTube動画のチャンネル名
動画の長さ 数値 duration_seconds YouTube動画の長さ(秒)
動画リンク リンク video_url YouTube動画のURL
ハッシュタグ 文字列(1行) hashtags YouTube動画のハッシュタグ
動画要約 文字列(複数行) ai_summary YouTube動画の生成AI要約
動画ID 文字列(1行) video_id YouTube動画の内部ID

「動画ID」は、レコードの重複を防ぐため、「値の重複を禁止する」をチェックします。

APIトークンの生成

「アプリの設定」ー「APIトークン」にて、APIトークンを作成します。
GASから、kintoneにデータを保存する際のアクセスを許可します。
「生成する」ボタンをクリックし、表示されたAPIトークンの「レコード閲覧」および「レコード追加」のアクセス権をチェックし、「保存」をクリックします。

Apifyの設定

次に、YouTube情報を定期的に取得してGASに情報を渡すためのApifyの準備をします。

Actorの設定

Apifyの Console (External link) より、ログインします。(Apifyのアカウントがない場合、アカウントを先に作成してください)
左側のメニューより「Actors」を選択し、「Go to Store」をクリックします。

検索テキストに「YouTube」と入力し、リストが表示されたら「YouTube Scraper」を選択します。

「Search Terms」に「kintone」、「Maximum Videos per search term」には、「5」videosと入力し、1回の実行で5動画まで検索する設定をします。
Apifyは、毎月$5のクレジットが付与されます。よって毎日1回程度検索しても無料枠範囲内で利用できます。
またGeminiはGemini Flash 2.5の利用で動画検索数を5に限定して無料枠で利用します。
今回のサンプルではApify、Geminiともに無料枠の範囲で実装していますが、課金することでさらに多くの動画情報を処理するスケールアップが実現できます。

YouTube Scraperの動作確認

YouTube Scraperの右上の「Start」ボタンをクリックして、実際に動作するかテストします。

下の画像のように検索結果のリストが「Output」表示されれば成功です。
左側の「Usage」欄において検索で課金された額が確認できます。(この記事を執筆している2026年7月時点では、毎月$5以内なら無料で使用できます。無料枠に関しては詳しくは Pricingページ (External link) を確認してください)

APIトークンの生成

左側メニューから、「Settings」を選択し、「API&Integration」タブをクリックします。
次に「+ Create a new token」をクリックします。

DescriptionにAPIトークンの説明を入力し、「Create」ボタンをクリックして、トークンを生成します。

生成されたAPIトークンはのちほどGASのコーディングで使用します。

GASの設定

次に、GASでApifyから取得した情報を整理して、kintoneアプリに情報を入れるための準備をします。

Googleスプレッドシートの作成

送信されたWebhookのデータは、GASの実行ログでは確認できませんので、実行ログをGoogleスプレッドシートに出力することで確認、デバッグが可能になります。
Googleスプレッドシート (External link) のサイトより、「空白のスプレッドシート」をクリックして新規作成します。

スプレッドシートに適当な名前を入力します。
表示されているURLより、スプレッドシートIDを取得します。「d」と「edit」の間の英数字混合の値がIDとなります。

Gemini APIキーの作成

Google AI Studio (External link) のAPI設置画面を開きます。
「APIキーを作成」をクリックします。

「新しいキーを作成する」画面にて、APIキー名を入力し、「プロジェクトを作成」をクリックします。

「新しいプロジェクトを作成」する画面にて、プロジェクト名を入力し、「プロジェクトを作成」をクリックします。

次に「新しいキーを作成する」画面にて、「キーを作成」をクリックします。

新しいキーが作成されましたので、リンクをクリックして詳細を確認します。

APIキーの情報が表示されますので、後述する「Google Apps Scriptによるプログラムの作成」にて使用します。

コーディング

Step 1

Google Apps Script (External link) のサイトにログインし、「新しいプロジェクト」をクリックします。

GASのエディターが表示されるので任意のProject名を入力します。

Step 2

プロジェクトの設定ボタンをクリックして、プロジェクトの設定画面を表示します。

画面下の「スクリプトプロパティ」の項より、「スクリプトプロパティを追加」をクリックします。

以下のプロパティを追加し、「スクリプトプロパティを保存」します。

プロパティ名
APIFY_TOKEN {Apifyトークンの値}
GEMINI_API_KEY {Gemini APIキーの値}
KINTONE_API_TOKEN {kintone APIトークン}
KINTONE_APP_ID {kintoneアプリID}
KINTONE_DOMAIN {kintoneサブドメイン}.cybozu.com
SPREADSHEET_ID {Google SheetのID}
WEBHOOK_SECRET {任意のランダム文字列}

information

{Apifyトークンの値}には、上記 Apify APIトークンの生成 で生成したトークンを入力します。
{Gemini APIキーの値}には、上記 「Gemini APIキーの作成」 で生成したAPIキーを入力してください。

{kintone APIトークン}には、上記 「APIトークンの生成」 で生成したAPIトークンを入力します。

{kintoneアプリID}には、作成したkintoneアプリのIDを入力します。(次のような画面から確認できます)

{kintoneサブドメイン}には、お使いのkintoneのサブドメイン名を入力してください。(https://example.cybozu.com/の場合、サブドメイン名はexampleです)

{Google SheetのID}には、上記 Googleスプレッドシートの作成 で確認した「スプレッドシートID」を入力します。

{任意のランダム文字列}には、GASのエディタで以下の関数を実行して任意の文字列を生成します。実行ログに出力された値を使用します。この値はスクリプトプロパティと後述する Webhookの設定 におけるWebhook URLの両方に同じものを設定します。

1
2
3
const generateWebhookSecret = () => {
  Logger.log(Utilities.getUuid());
};

Step 3

以下のコードを参考にコーディングします。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
/*
 * ApifyのYouTube Scraperを使って、kintone関連の動画の情報をWebhookで定期的にGASへ送信し、動画の内容をGeminiで要約して定期的にkintoneアプリに登録するサンプルプログラム
 * Copyright (c) 2026 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

// スクリプトパラメーターの値を取得
// Script Properties
const scriptProperties = PropertiesService.getScriptProperties();
const APIFY_TOKEN = scriptProperties.getProperty('APIFY_TOKEN');
const GEMINI_API_KEY = scriptProperties.getProperty('GEMINI_API_KEY');
const KINTONE_API_TOKEN = scriptProperties.getProperty('KINTONE_API_TOKEN');
const KINTONE_APP_ID = scriptProperties.getProperty('KINTONE_APP_ID');
const KINTONE_DOMAIN = scriptProperties.getProperty('KINTONE_DOMAIN');
const SPREADSHEET_ID = scriptProperties.getProperty('SPREADSHEET_ID');
const WEBHOOK_SECRET = scriptProperties.getProperty('WEBHOOK_SECRET');

const doPost = (e) => {

  // リクエスト元検証
  if (!e.parameter || e.parameter.secret !== WEBHOOK_SECRET) {
    return jsonResponse({status: 'error', message: 'Unauthorized'});
  }

  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);

  const logSheet = getOrCreateSheet(ss, 'Webhook_Log');
  const videoSheet = getOrCreateSheet(ss, 'Videos');

  try {
    const webhookData = JSON.parse(e.postData.contents);

    const datasetId =
      webhookData.resource.defaultDatasetId;

    const items =
      fetchApifyDatasetItems(datasetId);

    items.forEach(item => {

      // =========================
      // Duplicate check
      // =========================

      if (recordExists(item.id)) {

        logSheet.appendRow([
          new Date(),
          'SKIPPED_DUPLICATE',
          item.title
        ]);

        return;
      }

      // =========================
      // Gemini summarize
      // =========================

      const videoText =
        getVideoText(item).substring(0, 12000);

      const summary =
        summarizeWithGemini(videoText);

      // =========================
      // Save to Spreadsheet
      // =========================

      appendVideoToSheet(
        videoSheet,
        item,
        summary
      );

      // =========================
      // Save to kintone
      // =========================

      const result =
        createKintoneRecord(item, summary);

      logSheet.appendRow([
        new Date(),
        'KINTONE_RESPONSE',
        result.status,
        item.title,
        result.body
      ]);

      if (result.status < 200 || result.status >= 300) {
        throw new Error(
          `kintone API error: ${result.status}\n${result.body}`
        );
      }

    });

    return jsonResponse({
      status: 'ok',
      count: items.length
    });

  } catch (err) {

    logSheet.appendRow([
      new Date(),
      'ERROR',
      err.name,
      err.message
    ]);

    return jsonResponse({
      status: 'error',
      message: err.message
    });
  }
};


// ===================================
// Apify
// ===================================

const fetchApifyDatasetItems = (datasetId) => {
  const url =
    `https://api.apify.com/v2/datasets/${datasetId}/items?format=json&clean=true`;

  const response = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: {
      Authorization: `Bearer ${APIFY_TOKEN}`
    },
    muteHttpExceptions: true
  });

  const status =
    response.getResponseCode();

  const body =
    response.getContentText();

  if (status !== 200) {
    throw new Error(
      `Apify API error: ${status}\n${body}`
    );
  }

  return JSON.parse(body);
};


// ===================================
// Gemini
// ===================================

const summarizeWithGemini = (text) => {
  const url =
    'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='
    + GEMINI_API_KEY;

  const payload = {
    contents: [
      {
        parts: [
          {
            text:
              '次のYouTube動画を日本語で5行程度に要約してください。\n\n'
              + text
          }
        ]
      }
    ]
  };

  const response = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });

  const status =
    response.getResponseCode();

  const body =
    response.getContentText();

  if (status !== 200) {
    throw new Error(
      `Gemini API error: ${status}\n${body}`
    );
  }

  const json = JSON.parse(body);

  const summary =
    json.candidates?.[0]?.content?.parts?.[0]?.text;

  if (!summary) {
    throw new Error(
      'Gemini response does not contain summary text.'
    );
  }

  return summary;
};

const getVideoText = (item) => {

  const subtitleText =
    item.subtitles?.[0]?.plaintext || '';

  const descriptionText =
    item.text || '';

  return `
動画タイトル:
${item.title || ''}

動画説明:
${descriptionText}

字幕:
${subtitleText}
`;
};


// ===================================
// kintone
// ===================================

const createKintoneRecord = (item, summary) => {

  const url =
    `https://${KINTONE_DOMAIN}/k/v1/record.json`;

  const payload = {
    app: KINTONE_APP_ID,
    record: {
      video_title: {
        value: item.title || ''
      },
      video_description: {
        value: item.text || ''
      },
      published_date: {
        value: formatDate(item.date)
      },
      channel_name: {
        value: item.channelName || ''
      },
      duration_seconds: {
        value: durationToSeconds(item.duration)
      },
      video_url: {
        value: item.url || ''
      },
      hashtags: {
        value: (item.hashtags || []).join(' ')
      },
      ai_summary: {
        value: summary || ''
      },
      video_id: {
        value: item.id || ''
      }
    }
  };

  const response = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'X-Cybozu-API-Token': KINTONE_API_TOKEN
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });

  return {
    status: response.getResponseCode(),
    body: response.getContentText()
  };
};

const recordExists = (videoId) => {

  const query = `video_id in ("${videoId}")`;

  const url =
    `https://${KINTONE_DOMAIN}/k/v1/records.json`
    + `?app=${KINTONE_APP_ID}`
    + `&query=${encodeURIComponent(query)}`;

  const response = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: {
      'X-Cybozu-API-Token': KINTONE_API_TOKEN
    },
    muteHttpExceptions: true
  });

  const status =
    response.getResponseCode();

  const body =
    response.getContentText();

  if (status !== 200) {
    throw new Error(
      `kintone API error: ${status}\n${body}`
    );
  }

  const json =
    JSON.parse(body);
  return (
    json.records &&
    json.records.length > 0
  );
};


// ===================================
// Spreadsheet
// ===================================

const appendVideoToSheet = (
  sheet,
  item,
  summary
) => {

  sheet.appendRow([
    item.id || '',
    item.title || '',
    item.channelName || '',
    formatDate(item.date),
    item.duration || '',
    item.url || '',
    item.viewCount || 0,
    item.likes || 0,
    item.commentsCount || 0,
    (item.hashtags || []).join(' '),
    summary || ''
  ]);
};

const getOrCreateSheet = (ss, sheetName) => {

  return (
    ss.getSheetByName(sheetName)
    || ss.insertSheet(sheetName)
  );
};


// ===================================
// Utilities
// ===================================

const formatDate = (dateText) => {

  if (!dateText) return '';

  return dateText.substring(0, 10);
};

const durationToSeconds = (duration) => {

  if (!duration) return 0;

  const parts =
    duration.split(':').map(Number);

  if (parts.length === 3) {

    return (
      parts[0] * 3600
      + parts[1] * 60
      + parts[2]
    );
  }

  if (parts.length === 2) {

    return (
      parts[0] * 60
      + parts[1]
    );
  }

  return 0;
};

const jsonResponse = (obj) => {

  return ContentService
    .createTextOutput(JSON.stringify(obj))
    .setMimeType(
      ContentService.MimeType.JSON
    );
};

コードの解説

スクリプトパラメーターの値の取得

以下のAPI関数で、各スクリプトパラメーターの値を取得します。

PropertiesService.getScriptProperties()

1
2
3
4
5
6
7
8
const scriptProperties = PropertiesService.getScriptProperties();
const APIFY_TOKEN = scriptProperties.getProperty('APIFY_TOKEN');
const GEMINI_API_KEY = scriptProperties.getProperty('GEMINI_API_KEY');
const KINTONE_API_TOKEN = scriptProperties.getProperty('KINTONE_API_TOKEN');
const KINTONE_APP_ID = scriptProperties.getProperty('KINTONE_APP_ID');
const KINTONE_DOMAIN = scriptProperties.getProperty('KINTONE_DOMAIN');
const SPREADSHEET_ID = scriptProperties.getProperty('SPREADSHEET_ID');
const WEBHOOK_SECRET = scriptProperties.getProperty('WEBHOOK_SECRET');
WebhookのPOSTリクエストを受け付ける関数

以下の関数内でApifyからのWebhookのPOSTリクエストを受け付けます。
GAS WebアプリのURLは秘匿を前提とした設計ではなく、URLを知られると第三者から直接POSTされる可能性があります。そこで、Webhookの宛先URLに付与した共有シークレット(secretパラメーター)をe.parameter.secretで受け取り、スクリプトプロパティのWEBHOOK_SECRETと一致するかを検証します。一致しない場合は処理を中断します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// eslint-disable-next-line consistent-return
const doPost = (e) => {

  // リクエスト元検証
  if (!e.parameter || e.parameter.secret !== WEBHOOK_SECRET) {
    return jsonResponse({status: 'error', message: 'Unauthorized'});
  }

  // Webhooksから受け取ったデータの処理
};
WebhookのPOSTリクエスト受信ログ

Webhook受信では、リアルタイムでGAS内でのデバッグが困難なため、Googleスプレッドシートにデバッグ内容を出力します。
そのため、以下のようにスプレッドシートの情報を設定します。
SPREADSHEET_IDには、上記でデバッグ用に作成してGoogleスプレッドシートのIDを設定します。
またWebhookのデバッグの内容のログを出力するシートおよび、ApifyのYouTube Scraperの検索結果を出力するシートを設定します。

1
2
3
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const logSheet = getOrCreateSheet(ss, 'Webhook_Log');
const videoSheet = getOrCreateSheet(ss, 'Videos');
Webhookから結果を取得

以下の関数でWebhookの結果を取得できます。

const webhookData = JSON.parse(e.postData.contents);

ただ、Webhook受信の結果のデータに検索結果は含まれていないため、Apifyからさらにデータを取得する必要があるため、以下のようにDataset IDを取得します。

const datasetId = webhookData.resource.defaultDatasetId;

Apify Datasetから結果を取得

上記で取得したDataset IDを使って、ApifyのストレージからDatasetを取得します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const fetchApifyDatasetItems = (datasetId) => {
  const url =
    `https://api.apify.com/v2/datasets/${datasetId}/items?format=json&clean=true`;

  const response = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: {
      Authorization: `Bearer ${APIFY_TOKEN}`
    },
    muteHttpExceptions: true
  });

  const status =
    response.getResponseCode();

  const body =
    response.getContentText();

  if (status !== 200) {
    throw new Error(
      `Apify API error: ${status}\n${body}`
    );
  }

  return JSON.parse(body);
};

詳細はApifyの公式サイトのドキュメント内の Get dataset items (External link) を参照してください。

Datasetより各検索結果を取得

以下のように各YouTube動画の情報を処理します。

1
2
3
4
5
const items = fetchApifyDatasetItems(datasetId);

items.forEach(item => {
// 各YouTube動画情報の処理
});
Geminiで動画情報を要約

以下の関数で、Google Geminiに動画の内容を要約してもらいます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const summarizeWithGemini = (text) => {
  const url =
    'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='
    + GEMINI_API_KEY;

  const payload = {
    contents: [
      {
        parts: [
          {
            text:
              '次のYouTube動画を日本語で5行程度に要約してください。\n\n'
              + text
          }
        ]
      }
    ]
  };

  const response = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });

  const status =
    response.getResponseCode();

  const body =
    response.getContentText();

  if (status !== 200) {
    throw new Error(
      `Gemini API error: ${status}\n${body}`
    );
  }

  const json = JSON.parse(body);

  const summary =
    json.candidates?.[0]?.content?.parts?.[0]?.text;

  if (!summary) {
    throw new Error(
      'Gemini response does not contain summary text.'
    );
  }

  return summary;
};

「url」には、Gemini APIのサービスエンドポイントのURLを指定します。

1
const url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' + GEMINI_API_KEY;

詳細は API リファレンス (External link) を参照してください。
今回は、https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContentを使用しています。

以下のデータ形式でリクエストのデータを設定します。「次のYouTube動画を日本語で5行程度に要約してください」と指示しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const payload = {
  contents: [
    {
      parts: [
        {
          text:
            '次のYouTube動画を日本語で5行程度に要約してください。\n\n'
            + text
        }
      ]
    }
  ]
};

以下の関数で、Gemini APIにリクエストを送信します。

const response = UrlFetchApp.fetch(gemini_end_point, options);

1
2
3
4
5
6
const response = UrlFetchApp.fetch(url, {
  method: 'post',
  contentType: 'application/json',
  payload: JSON.stringify(payload),
  muteHttpExceptions: true
});

Gemini APIの関数の詳細については、 「Gemini API ドキュメント」-「テキスト生成」 (External link) を参照してください。

Googleスプレッドシートにデバッグ情報および動画情報を保存

次の関数にてGoogleスプレッドシートにスプレッドシートの作成または、既存のスプレッドシートの情報を取得します。

1
2
3
4
5
6
7
const getOrCreateSheet = (ss, sheetName) => {

  return (
    ss.getSheetByName(sheetName)
    || ss.insertSheet(sheetName)
  );
};

Webhook受信のログ情報と動画情報を保存するスプレッドシートを作成しています。

1
2
const logSheet = getOrCreateSheet(ss, 'Webhook_Log');
const videoSheet = getOrCreateSheet(ss, 'Videos');

以下の関数で動画情報をスプレッドシートに保存します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const appendVideoToSheet = (
  sheet,
  item,
  summary
) => {

  sheet.appendRow([
    item.id || '',
    item.title || '',
    item.channelName || '',
    formatDate(item.date),
    item.duration || '',
    item.url || '',
    item.viewCount || 0,
    item.likes || 0,
    item.commentsCount || 0,
    (item.hashtags || []).join(' '),
    summary || ''
  ]);
};
kintone REST APIでレコード登録

次の関数で動画情報をkintoneに登録します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const createKintoneRecord = (item, summary) => {

  const url =
    `https://${KINTONE_DOMAIN}/k/v1/record.json`;

  const payload = {
    app: KINTONE_APP_ID,
    record: {
      video_title: {
        value: item.title || ''
      },
      video_description: {
        value: item.text || ''
      },
      published_date: {
        value: formatDate(item.date)
      },
      channel_name: {
        value: item.channelName || ''
      },
      duration_seconds: {
        value: durationToSeconds(item.duration)
      },
      video_url: {
        value: item.url || ''
      },
      hashtags: {
        value: (item.hashtags || []).join(' ')
      },
      ai_summary: {
        value: summary || ''
      },
      video_id: {
        value: item.id || ''
      }
    }
  };

  const response = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'X-Cybozu-API-Token': KINTONE_API_TOKEN
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });

  return {
    status: response.getResponseCode(),
    body: response.getContentText()
  };
};

次のAPI関数でkintoneへデータを送信しています。

UrlFetchApp.fetch(url, options);

1
2
3
4
5
6
7
8
9
const response = UrlFetchApp.fetch(url, {
  method: 'post',
  contentType: 'application/json',
  headers: {
    'X-Cybozu-API-Token': KINTONE_API_TOKEN
  },
  payload: JSON.stringify(payload),
  muteHttpExceptions: true
});
重複データのチェック

以下の関数ですでに同じ動画のレコードが存在しないかチェックします。「動画ID」をチェックしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const recordExists = (videoId) => {

  const query = `video_id in ("${videoId}")`;

  const url =
    `https://${KINTONE_DOMAIN}/k/v1/records.json`
    + `?app=${KINTONE_APP_ID}`
    + `&query=${encodeURIComponent(query)}`;

  const response = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: {
      'X-Cybozu-API-Token': KINTONE_API_TOKEN
    },
    muteHttpExceptions: true
  });

  const status =
    response.getResponseCode();

  const body =
    response.getContentText();

  if (status !== 200) {
    throw new Error(
      `kintone API error: ${status}\n${body}`
    );
  }

  const json =
    JSON.parse(body);
  return (
    json.records &&
    json.records.length > 0
  );
};

スコープ設定

再び「プロジェクトの設定」をクリックして、設定画面を表示します。

「appsscript.json」マニフェストファイルをエディタで表示するの項にチェックをいれます。

するとエディタ側に「appsscript.json」が表示されますので、以下のようにスコープを設定します。外部APIからのリクエストおよびスプレッドシートへのアクセスを許可しています。

1
2
3
4
"oauthScopes": [
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/spreadsheets"
  ]

Webアプリのデプロイ

コーディングが完成したら、Webhookの受信の動作確認の前にGASをWebアプリとしてデプロイします。
「デプロイ」メニューから、「新しいデプロイ」を選択します。

「種類の選択」で「Webアプリ」を選択し、「デプロイ」ボタンをクリックします。

「アクセスを承認」ボタンをクリックします。

承認許可画面が表示されるので、「Select all」をチェックし、「Continue」をクリックしてアクセスを許可します。

新しいデプロイ情報が表示されますので、「Webアプリ」の「URL」をコピーします。

コピーした「URL」を下記 Webhookの設定 時に「URL」欄に入力します。

Webhookの設定

「Integrations」のタブをクリックし、検索テキストに「Webhook」と入力します。 表示されたリストから、「HTTP Webhook」を選択します。

「Start When」イベントに「Run Succeeded」を選択します。

「Save」をクリックして保存します。

こちらのURLの欄には、上記で設定されたGASのエンドポイントおよび「secret」の値を入力し、保存します。
末尾に?secret={スクリプトプロパティに設定した値}を追加してください。

以上で動作確認の準備が整いました。

動作確認

上記の YouTube Scraperの動作確認 を参照してYouTube Scraperを実行します。

次にGASのコンソールに戻り、「実行数」メニューを選択すると実行中のスクリプトのログが表示されますので、リスト最上部のスクリプトの「ステータス」が「完了」になっていることを確認します。

右上のメニューより「スプレッドシート」をクリックして、Googleスプレッドシートを開きます。

「Webhook_Log」のシートに「200」のステータスが表示されていれば成功です。
「Error」が表示されている場合、メッセージにしたがってデバッグしてください。

「Videos」のシートには、検索取得したYouTube動画の情報のリストが表示されます。

次にkintoneアプリを開き、YouTube動画のリストが登録されているか確認します。

動画が新規登録されていれば、成功です。動画IDを確認して、重複動画がないことも確認します。

以上で動作確認は終了です。

Apify Actorのスケジューリング

実際にWebスクレイピングを運用する際には、スケジューリング機能を利用するのが便利です。
今回は、毎日一定の時間にYouTube Scraperを起動することで、自動的に動画情報を収集できます。

下記画像のように今回作成したActorより、右上の「...」メニューをクリックして「Schedule Actor」を選択します。

「Daily」タブを選択し、「Custom」を選びます。
次にActorを実行したい時間を選んで、「Timezone」を選択します。
「Create」ボタンで新規スケジュールを作成します。

まとめ

今回のTipsでは、ApifyのYouTube Scraperを使って、kintone関連の動画検索してその動画情報をGeminiで要約し、kintoneに登録するサンプルを作成しました。
Geminiで動画内容を要約することで、動画の内容を効率よくチェックできるので、必要な動画をすばやく見つけることが可能になります。
また、kintoneで管理することによって動画のナレッジを蓄積でき、動画情報をフィルターリングできるので管理が容易になります。
さらに他のメンバーとも動画のナレッジを容易に共有できるメリットがあります。
サンプルではApify、Geminiともに無料枠の範囲で実装していますが、課金することでさらに多くの動画情報を取得したり、取得頻度を増やしたり、要約の精度をあげるなどスケールアップできます。

注意点

ApifyはYouTubeだけでなくソーシャルメディアやWebサイトから情報を取得できます。
Webサイトやサービスによっては、APIを使わない方法で情報収集することが禁止されているためWebサイトやサービスの利用規約をよく読んだうえで実装してください。

参照サイト

information

このTipsは、2026年7月版kintoneで動作を確認しています。

公式コミュニティ

kintone開発者同士で質問や知見を共有し、学び合うことができます。