Hiratake Web ロゴ

GASを使って小説家になろうの小説が更新されたらDiscordに通知する

投稿した日
更新した日
書いたひと
icon
ひらたけ

利用するもの

  • なろう小説 API
  • Google Apps Script
  • Google スプレッドシート
  • Discord

スプレッドシートをつくる

スプレッドシートの情報を取得する

const main = () => {
  /** スプレッドシート */
  const spreadsheet = SpreadsheetApp.getActive()
  /** リストシート */
  const sheet = spreadsheet.getSheetByName('リスト')
  /** 取得するデータの範囲(2行目以降) */
  const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 3)
  /** リストシートのデータ */
  const data = range.getValues().map((item) => ({
    /** タイトル */
    title: item[0] || '',
    /** Nコード */
    ncode: (item[1] || '').toLowerCase(),
    /** エピソード数 */
    latest: Number(item[2] || 0),
  }))
}

小説の情報を取得する

/**
 * なろう小説APIから情報を取得する
 * @param ncodes Nコードの配列
 * @returns 小説情報の配列
 */
const getNarouInfo = (ncodes = []) => {
  /** APIのベースURL */
  const base = 'https://api.syosetu.com/novelapi/api/'
  /** 出力形式 */
  const output = 'json'
  /** 取得する項目 */
  const key = [
    'n',  // Nコード
    's',  // 作品のあらすじ
    'ga', // エピソード数
  ].join('-')
  /** 取得する件数 */
  const limit = ncodes.length
  /** Nコード */
  const ncode = ncodes.join('-')

  try {
    const response = UrlFetchApp
      .fetch(`${base}?out=${output}&of=${key}&lim=${limit}&ncode=${ncode}`)
    const data = JSON.parse(response.getContentText())
      .filter((item) => item.ncode)
      .map((item) => ({
        ncode: item.ncode.toLowerCase(),
        description: item.story.substring(0, 100),
        latest: Number(item.general_all_no),
      }))
    return data
  } catch (err) {
    Logger.log(err.toString())
  }
}
  • base - API の URL。
  • output - API の out パラメータに渡す値。Google Apps Script で扱いやすいよう JSON 形式を指定。
  • key - API の of パラメータに渡す値。転送量を減らすため、不要なデータは取得しないようにここで指定します。複数の項目を取得する場合は、ハイフン(-)で区切ります。
    • n - N コード
    • s - 作品のあらすじ
    • ga - エピソード数
  • limit - API の lim パラメータに渡す、取得する小説の件数。ここでは引数として渡された配列の要素数を指定しています。
  • ncode - API の ncode パラメータに渡す、取得する小説の N コード。複数の小説を指定する場合はハイフン(-)で区切ります。
const main = () => {
  /** スプレッドシート */
  const spreadsheet = SpreadsheetApp.getActive()
  /** リストシート */
  const sheet = spreadsheet.getSheetByName('リスト')
  /** 取得するデータの範囲(2行目以降) */
  const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 3)
  /** リストシートのデータ */
  const data = range.getValues().map((item) => ({
    /** タイトル */
    title: item[0] || '',
    /** Nコード */
    ncode: (item[1] || '').toLowerCase(),
    /** エピソード数 */
    latest: Number(item[2] || 0),
  }))

  /** なろう小説APIから取得した情報 */
  const response = getNarouInfo(
    data.filter((item) => item.ncode).map((item) => item.ncode)
  )
}

通知を送信する

/**
 * Discordへ通知を送信する
 * @params novels 通知する小説の情報
 */
const postDiscord = (novels = []) => {
  /** 送信先URL */
  const webhookUrl = 'https://discord.com/api/webhooks/xxxxxxxxxxxxxxxxxxxx'
  const items = novels.reduce((prev, current) => {
    if (!prev.length || prev[prev.length - 1].length === 10) {
      return [...prev, [current]]
    } else {
      const [last, ...rest] = prev.reverse()
      return [...rest.reverse(), [...last, current]]
    }
  }, [])

  try {
    items.forEach((item) => {
      UrlFetchApp.fetch(webhookUrl, {
        'method': 'post',
        'contentType': 'application/json',
        'payload': JSON.stringify({
          content: '小説家になろうの作品に更新がありました。',
          embeds: item,
        })
      })
    })
  } catch (err) {
    Logger.log(err.toString())
  }
}
const main = () => {
  /** スプレッドシート */
  const spreadsheet = SpreadsheetApp.getActive()
  /** リストシート */
  const sheet = spreadsheet.getSheetByName('リスト')
  /** 取得するデータの範囲(2行目以降) */
  const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 3)
  /** リストシートのデータ */
  const data = range.getValues().map((item) => ({
    /** タイトル */
    title: item[0] || '',
    /** Nコード */
    ncode: (item[1] || '').toLowerCase(),
    /** エピソード数 */
    latest: Number(item[2] || 0),
  }))

  /** なろう小説APIから取得した情報 */
  const response = getNarouInfo(
    data.filter((item) => item.ncode).map((item) => item.ncode)
  )

  /** 通知する小説の情報 */
  const postItems = []

  data.forEach((item, index) => {
    try {
      const current = response.find((novel) => novel.ncode === item.ncode)
      // 見つからない場合はエラー
      if (!current) {
        throw new Error('小説が見つかりません')
      }
      if (Number(item.latest) < current.latest) {
        // スプレッドシートのエピソード数を更新
        sheet.getRange(`C${index + 2}`).setValue(current.latest)
        // 通知する項目として配列に追加
        postItems.push({
          title: item.title,
          description: current.description,
          url: `https://ncode.syosetu.com/${item.ncode}/`,
        })
      }
    } catch (err) {
      Logger.log(err.toString())
    }
  })

  // Discordへ通知を送信する
  postDiscord(postItems)
}

最終的なコード

const main = () => {
  /** スプレッドシート */
  const spreadsheet = SpreadsheetApp.getActive()
  /** リストシート */
  const sheet = spreadsheet.getSheetByName('リスト')
  /** 取得するデータの範囲(2行目以降) */
  const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 3)
  /** リストシートのデータ */
  const data = range.getValues().map((item) => ({
    /** タイトル */
    title: item[0] || '',
    /** Nコード */
    ncode: (item[1] || '').toLowerCase(),
    /** エピソード数 */
    latest: Number(item[2] || 0),
  }))

  /** なろう小説APIから取得した情報 */
  const response = getNarouInfo(
    data.filter((item) => item.ncode).map((item) => item.ncode)
  )

  /** 通知する小説の情報 */
  const postItems = []

  data.forEach((item, index) => {
    try {
      const current = response.find((novel) => novel.ncode === item.ncode)
      // 見つからない場合はエラー
      if (!current) {
        throw new Error('小説が見つかりません')
      }
      if (Number(item.latest) < current.latest) {
        // スプレッドシートのエピソード数を更新
        sheet.getRange(`C${index + 2}`).setValue(current.latest)
        // 通知する項目として配列に追加
        postItems.push({
          title: item.title,
          description: current.description,
          url: `https://ncode.syosetu.com/${item.ncode}/`,
        })
      }
    } catch (err) {
      Logger.log(err.toString())
    }
  })

  // Discordへ通知を送信する
  postDiscord(postItems)
}

/**
 * なろう小説APIから情報を取得する
 * @param ncodes Nコードの配列
 * @returns 小説情報の配列
 */
const getNarouInfo = (ncodes = []) => {
  /** APIのベースURL */
  const base = 'https://api.syosetu.com/novelapi/api/'
  /** 出力形式 */
  const output = 'json'
  /** 取得する項目 */
  const key = [
    'n',  // Nコード
    's',  // 作品のあらすじ
    'ga', // エピソード数
  ].join('-')
  /** 取得する件数 */
  const limit = ncodes.length
  /** Nコード */
  const ncode = ncodes.join('-')

  try {
    const response = UrlFetchApp
      .fetch(`${base}?out=${output}&of=${key}&lim=${limit}&ncode=${ncode}`)
    const data = JSON.parse(response.getContentText())
      .filter((item) => item.ncode)
      .map((item) => ({
        ncode: item.ncode.toLowerCase(),
        description: item.story.substring(0, 100),
        latest: Number(item.general_all_no),
      }))
    return data
  } catch (err) {
    Logger.log(err.toString())
  }
}

/**
 * Discordへ通知を送信する
 * @params novels 通知する小説の情報
 */
const postDiscord = (novels = []) => {
  /** 送信先URL */
  const webhookUrl = 'https://discord.com/api/webhooks/xxxxxxxxxxxxxxxxxxxx'
  const items = novels.reduce((prev, current) => {
    if (!prev.length || prev[prev.length - 1].length === 10) {
      return [...prev, [current]]
    } else {
      const [last, ...rest] = prev.reverse()
      return [...rest.reverse(), [...last, current]]
    }
  }, [])

  try {
    items.forEach((item) => {
      UrlFetchApp.fetch(webhookUrl, {
        'method': 'post',
        'contentType': 'application/json',
        'payload': JSON.stringify({
          content: '小説家になろうの作品に更新がありました。',
          embeds: item,
        })
      })
    })
  } catch (err) {
    Logger.log(err.toString())
  }
}

定期的に実行する