ニューヨークタイムズのベストセラー情報をメール通知する

2024.01.31

はじめに

読みたい書籍を探すのに、各種レビューサイトや書評系ブログ、YouTubeを参考にしますが、ニューヨークタイムズ社のベストセラーリストも主要な情報源の一つです。しかし、ベストセラーリストを見るにはニューヨークタイムズ社のWebサイトを見に行く必要があり、ふと気づくと何週間も見ていないことがあります。

そのようなことを防ぐために、ベストセラーリストを毎週メールで通知してくれるような仕組みを作ろうと思いました。

アカウント登録

ベストセラー情報を取得するためにニューヨークタイムズ社によって提供されているBooks APIを使用します。

アカウント登録が必要になるので、早速行います。

The New York Times Developer Networkにアクセスします。

「Sign In」をクリックします。

「Create an account」をクリックします。

必要事項を入力し、「Create Account」をクリックします。

下記画面が表示され、確認メールが送信されます。

メール内のリンクをクリックすると認証され、アカウント登録が完了します。

アクセスキー発行

The New York Times Developer Network右上のメールアドレス部分をクリックし、「Apps」をクリックします。

「NEW APP」をクリックします。

「Overview」にアプリ情報を入力し、「APIs」では「Books API」のEnableをクリックし有効にします。

右下の「Save」をクリックすると、APIキーが発行されます。

APIを試してみる

APIの仕様はBooks APIのページに記載があります。

また、Books APIにアクセスすると右上に2つのボタンがあります。

「DOWNLOAD SPEC」をクリックするとSwagger 2.0形式のAPI仕様がダウンロードできます。

「AUTHORIZE」をクリックすると、Web上でAPIの動作確認をするための認証情報を登録できます。

どのアプリケーションの認証情報を使うかが選択できるので、先ほど登録した「WeeklyInformation」を選択します。(「Manually enter key」を選択すると手動でAPI KEYを入力することもできます)

選択したら「AUTHORIZE」をクリックし、「OK」で画面を閉じます。

APIをWeb上で試しに動かしてみます。

左側メニューから、ベストセラーリストの名前(ジャンル名)を一覧で取得できる「names.json」をクリックします。

APIの説明の右側に「Try this API」があるので「EXECUTE」をクリックします。

API KEYは登録済みなので、結果が表示されます。

メールで通知されるようにする

このAPIを使って自分のメールにベストセラーリストを通知する仕組みを作ります。CDKで簡単に作ります。

まずCDKプロジェクトを作成します。

cdk init app --language typescript

フォルダ構成は以下の通りです。(デフォルトで作成されるファイルは省略しています)

┌ bin
│  └ nyt-best-seller.ts
├ lambda
│  └ weekly-checker.ts
├ lib
│  ├ resoutces
│  │  ├ event-rule.ts
│  │  ├ lambda.ts
│  │  └ sns-topic.ts
│  └ nyt-best-seller-stack.ts
└ .env

以下のモジュールをインストールしておきます。

  • dotenv
  • @aws-sdk/client-sns

.envファイルには送信先のメールアドレスと、APIキーを環境変数を登録します。

SNS_MAIL=hoge@example.com
API_KEY=xxxxxxxx

APIを呼び出し、メール通知を行うLambda関数のハンドラは以下の通りです。

  • weekly-checker.ts
import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";

const TOPIC_ARN = process.env.TOPIC_ARN || "";
const API_KEY = process.env.API_KEY || "";
const END_POINT = "https://api.nytimes.com/svc/books/v3/lists.json";
const LISTS = [
  "childrens-middle-grade",
  "young-adult",
  "trade-fiction-paperback",
];

type Book = {
  title: string;
  description: string;
  author: string;
};

type APIResponse = {
  copyright: string;
  num_results: number;
  results: [
    {
      amazon_product_url: string;
      book_details: Book[];
    }
  ];
};

export const handler = async (): Promise<any> => {
  let mailBody = "";
  for (const list of LISTS) {
    mailBody += "";
    mailBody +=
      "+-----------------------------------------------------------+" + "\r\n";
    mailBody += "■" + list + "\r\n";
    mailBody +=
      "+-----------------------------------------------------------+" + "\r\n";

    const response = await fetch(
      `${END_POINT}?list=${list}&api-key=${API_KEY}`
    );
    const apiResponse: APIResponse = await response.json();
    for (const result of apiResponse.results) {
      mailBody += `『${result.book_details[0].title}』by ${result.book_details[0].author}\r\n`;
      mailBody += `${result.book_details[0].description}\r\n`;
      mailBody += `${result.amazon_product_url}\r\n\r\n`;
    }
    await new Promise((resolve) => setTimeout(resolve, 12000));
  }

  // メール送信
  const client = new SNSClient({});
  const input = {
    TopicArn: TOPIC_ARN,
    Subject: "!!new!!【自動配信】The New York Times Best Sellers",
    Message: mailBody,
  };
  const command = new PublishCommand(input);
  await client.send(command);
};

通知されるのは自分の好きなジャンルだけで良いので、"childrens-middle-grade"と"young-adult”と"trade-fiction-paperback"に絞って取得しています。

また、APIの使用制限があるため、念のためリクエストとリクエストの間に12秒のスリープを入れています。

Lambda関数のリソースでは、SNSトピックのARNとBooks APIのAPIキーを環境変数として登録しています。(環境変数だとマネジメントコンソール上から見えてしまうので、Secrets Managerなどを利用する方がよりセキュアです)

  • lambda.ts
import { Construct } from "constructs";
import { aws_lambda_nodejs as lambda, Duration } from "aws-cdk-lib";
import * as dotenv from 'dotenv'

dotenv.config();

export class Lambda {
  private readonly _scope: Construct;
  private readonly _topicArn: string;

  constructor(scope: Construct, topicArn: string) {
    this._scope = scope;
    this._topicArn = topicArn;
  }

  public create(): lambda.NodejsFunction {
    return new lambda.NodejsFunction(this._scope, "lambda", {
      entry: "lambda/weekly-checker.ts",
      timeout: Duration.minutes(3),
      environment: {
        TOPIC_ARN: this._topicArn,
        API_KEY: process.env.API_KEY || "",
      },
    });
  }
}

週1回でスケジューリングするEvent Bridgeのルールは以下のようにしました。

  • event-rules.ts
import { Construct } from 'constructs';
import * as events from 'aws-cdk-lib/aws-events';

export class EventRule {
  private readonly _scope:Construct;

  constructor(scope:Construct) {
    this._scope = scope;
  }

  public create():events.Rule{
    return new events.Rule(this._scope, 'event', {
      schedule: events.Schedule.cron({
        minute: '0',
        hour: '23',
        weekDay: '6',
        month: '*',
        year: '*',
      })
    });
  }
}

SNSトピックは特別なことはしていません。

  • sns-topic.ts
import { Construct } from 'constructs';
import * as sns from 'aws-cdk-lib/aws-sns';

export class SnsTopic {
  private readonly _scope:Construct;

  constructor(scope:Construct) {
    this._scope = scope;
  }

  public create():sns.Topic{
    return new sns.Topic(this._scope, 'topic', { });
  }
}

これらのリソースを元にスタックを作成します。

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { EventRule } from "./resources/event-rule";
import { Lambda } from "./resources/lambda";
import { SnsTopic } from "./resources/sns-topic";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
import * as iam from "aws-cdk-lib/aws-iam";
import * as dotenv from "dotenv";

export class NytBestSellerStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    /**
     * Create Event Rule Resource
     */
    const eventRule = new EventRule(this);
    const rule = eventRule.create();

    /**
     * Create SNS Topic Resource
     */
    const snsTopie = new SnsTopic(this);
    const topic = snsTopie.create();

    /**
     * Create Lambda Resources
     */
    const lambda = new Lambda(this, topic.topicArn);
    const fn = lambda.create();

    /**
     * Set up integrations
     */
    // EventBridge -> Lambda
    const eventTarget = new targets.LambdaFunction(fn);
    rule.addTarget(eventTarget);

    // SNS Topic -> Subscription
    dotenv.config();
    let mailaddress = "";
    if (typeof process.env.SNS_MAIL === "string") {
      mailaddress = process.env.SNS_MAIL;
    }
    const sub = new subscriptions.EmailSubscription(mailaddress);
    topic.addSubscription(sub);

    // IAM
    const policy = new iam.PolicyStatement({
      actions: ["sns:*"],
      resources: [topic.topicArn],
    });
    fn.addToRolePolicy(policy);
  }
}

CDKをデプロイします。デプロイの過程でSNSトピックのサブスクリプション確認メールが送信されるので、「Confirm subscription」をクリックします。

cdk deploy

スタックが作成されました。

作成されたLambda関数を開き、「テスト」をクリックします。

無事にベストセラーリストが記載されたメールが届きました。

おわりに

非常に簡単にベストセラーリストを取得できてとてもありがたいです。今回使用したAPIの他にもいくつかAPIがあるようなので、欲しい情報を見つけたら使ってみようと思います。また、メール本文内に書影も含めることができたらもっと見やすくなりそうです。

ひとまずこの記事ではここまでとしようと思います。最後まで見て頂きありがとうございました。