📑

「小説家になろう」の追っかけ読書ツールを作った話

2023/05/05に公開

何を作ったか

これを作りました。

https://okkake.shuttleapp.rs/

解決しようとした課題

「小説家になろう」は公式に「小説Atom」というフィードを提供しており、小説の最新話を読むにはこれで事足ります。

しかし、すでに多くの話数が投稿されているストーリーを一気に読むのは大変なことがあります。

このような場合に、一度にまとめて読み進めるのではなく、一定のペースで少しずつ読み進めていく仕組みがあると便利だと考えました。

仕様

配信方法

やりたいことは「一定のペースで、過去のエピソードを配信する」ということです。

このような更新情報を提供するには古典的な仕組みとしてRSS/Atomがあります。RSS/Atomによる最新情報の取得は以前ほどは広くは使われていないように思えますが、今でもフィードリーダーは生き残っています。特定のSNSに連携するよりは汎用的な形式のほうが応用の幅は広くとれます。また、RSS/Atomであればpush通知や未読状態の管理はフィードリーダーの機能に任せることができるため、自分で実装するよりははるかに合理的です。

そのようなわけで、「一定のペースで、過去のエピソードを配信する」という要求は、RSS/Atomという形で解決することにしました。また、最新話に関しては公式のAtomがあるため、今回作るフィードもこの形式に寄せることにしました。

ドロップした仕様

配信ペース

まず、配信ペースは1日1話で固定にしました。この部分の一般化はあとからでもできることを踏まえると、まず1日1話で実装することで十分な範囲の需要をカバーできると考えました。

各話タイトル

また、各話タイトルは配信しないことにしました。これは公式のAtomの仕様に揃えたものですが、結果として実装を大幅に簡略化することに貢献しました。

最新話以降の挙動

最後に問題になったのが、配信が追いついたときにどうするかです。

最も自然な実装は、「最新話に追いついたらそれ以降は配信しない」です。ただ、これはやや特殊なフィードなので、必要なくなったら購読を解除してもらうのがベストだと考えました。そのためには、最新話に追いついても何かの情報を配信し続けるという方法があります。たとえば以下のような方法が考えられます。

  • (a) 最新話に追いついたら、また1話から再配信する
  • (b) 最新話に追いついたら、最新話を繰り返し配信する
  • (c) 最新話に追いついたら、存在しない話を配信する

上の (a) のほうが便利そうにも思えますが、これを実現するには「いつ折り返しが発生したか」をどこかに状態として残しておく必要があります (さもなくば話数が変動したときに変な挙動になる)

一方 (b) であれば話数が変動しても自然に次の話が配信されるようになるだけで、2週目以降での挙動のズレなどは気にしなくてよくなります。端的に言うとステートレスな配信ができます。

…… というところまでは最初から考えていたのですが、実装してみて (c) というアイデアが出てきました。これは「小説家になろう」のURLの特性と関係があります。

「小説家になろう」ではエピソードは連番になっていて、URLでも連番で参照されます。 /1/, /2/, /3/, ... のようなURLが割り振られます。

ということは、最新話以降についてもURLだけは形式的につけることができることになります。もちろん、実際にリンク先に飛ぼうとすれば404になるはずです。

この方法は一見するとおかしな仕様のように思えますが、この方法には大きな利点があります。それは、エピソード情報を取得しなくてもよくなるという点です。

「小説家になろう」ではいくつかのAPIが提供されていて、それを使うと小説の検索やメタデータの取得を行うことができますが、エピソード数・エピソードタイトル・内容などは取得できません。これらが必要であれば、いくつかのアクセス制限を回避して自力でスクレイピングを行う必要が出てきます。

最新話を通り越しているかどうかを判定するにはエピソード数を知る必要がありますが、形式的な連番にもとづいて存在しない話を配信することを許容すれば、この情報取得は不要になります。しかも、もともとエピソードタイトルは配信しないことにしているので、これらのスクレイピング自体が不要ということになります。

小説タイトルの取得方法

ここまでの整理により、Atom配信に必要なのは小説タイトルのみということになりました。おまけで作者情報くらいは入れることにしますが、それでも主要なメタデータさえあればOKということになります。

今回は思い切って、これらをAtomのURLに埋め込むことにしました。これにより、Atom生成のエンドポイントは完全にステートレスに実装できるようになりました。

実装技術

作る必要があるのは以下の2つです。

  • AtomのURLを発行するためのUI
  • Atomの配信

サーバー

上記の設計により本サービスはデータストアを持つ必要がなくなったのでサーバーはほぼ何で実装してもよいのですが、せっかくなので今回はRustで実装することにしました。

  • ホスティングにはshuttleというサービスを使ってみました。検索したら出てきたやつで、Rustのアプリケーションをゼロコンフィグでデプロイできるというものです。現在は実験段階ということで無料で使えるようです。
    • RDBなどもかなり手軽にセットアップできて有望そうでした (が、今回は結局データストアを持たない設計になったので不要になりました)
    • アクセスが少なくてscale to zeroしてしまうとダウンタイムがあるようで、そこだけ少し困るところです。
  • shuttleでは複数のフレームワークがサポートされていますが、今回はtokio-rs/axumを使ってみました。

フロントエンド

「AtomのURLを発行するためのUI」はそれほどリッチではないものの、Vanilla JSでは書きたくないな……と思ったので、単一のHTML + Reactで書くことにしました。ざっくり言うとこんな感じです。

<script type="module">
  import React from "https://jspm.dev/react";
  import ReactDOMClient from "https://jspm.dev/react-dom/client";
  // ...
</script>

jspmはnpmのパッケージをESM化して配信してくれるCDN (unpkgのESM版的な感じ) で、これとWebブラウザのESMサポートを組み合わせることでバンドラー無しで全部動かせます。ちょっとした開発だとこれで済むのは良い時代ですね。

個人プロジェクトなので互換性を気にせず最新の構文をガンガン使っていますが、上の方法だとTypeScriptとJSXだけは使えません。さすがにbabel-standaloneを入れるほどではないので、JSXは使わずに React.createElement でひたすら書く感じで実装しました。

なんだかんだで一貫性のあるUIをサクッと実装できるのでReactは便利ですね。

まとめと感想

「小説家になろう」の追っかけ読書という課題を以前から感じていたので、自分でそれを解決するツールを作ってみました。

筆者自身の悩みとして、趣味プロジェクトを完遂せずに放棄してしまうことが多いというのがありました。趣味なのでそれ自体が悪いことはないのですが、このように最小限のプロダクトを自分で完遂まで持っていけたのは久しぶりでかなりよい達成感を得ることができました。

本稿ではどのように仕様と実装を決定したかという観点に注力して説明してみたので、何かこういったツールを作ってみようとしている人のヒントになればさいわいです。

Discussion