最近、ストレージサーバーが起動しなくなったり(ストレージ自体は無事)、Android買ったりした関係で、自宅サーバーの整理をしていた。
その中で今迄雑にcalibreに突っ込んでNAS共有だけしていた電子書籍を、汎用的に管理できる仕組みに移行しようと色々調査していた。
しかし、既存の何もかもが自分にとって中途半端というか日本人に噛み合わない作りになっていて我慢ならなかったので、Claudeを使って自分で作ることにした。
大抵の場合は、kavitaかstumpで良いとは思うが、自分には合わなかった。
既存のものが気に入らなかった理由
filenameに規約がある場合が多い
kavitaなどが顕著だが、ファイル名やディレクトリ名を勝手に解釈してメタデータに割り当てる。規約で決まってるので自分に都合の良いファイル名を付けられない。勝手に加工されて意図しないメタデータが入ることの方が多くてむしろ邪魔だった。
Web Readerの挙動
CBZを開いた時の見開き表示のオフセットコントロールが大抵上手くいかない。物によっては右から左に読むマンガスタイルの読み方が出来ない場合すらある。
EPUB readerも横書きしか考慮されていないケースが多い。縦書き + ルビを正しく表示できるものは非常に少ない。
kavita, stumpはどうだったか
kavitaはreaderが比較的良くできていて、自分が調べた限りでは、WebUIで縦書きとルビを正しく表示できるほぼ唯一の実装だった。しかし、CBZの見開き表示を変に自動でコントロールしようとしてオフセットのズレを手動で調整できないし、ファイル名規約に思想が強くメタデータのマッピングがアメコミに依っている。日本人向きではない。
stumpはメタデータ管理に余計なことをしないし、ディレクトリ名をシリーズに割り当てる挙動もある程度カスタムできる。しかし、CBZ readerはkavitaと同じ問題を持っていて、EPUB readerは横書きオンリーだった。縦書きのEPUBを開こうとすると、そもそも表示できないかフォントがおかしくなる。そして、自分がiOSで利用しているOPDS対応の電子書籍リーダーを経由してアクセスするとアプリが落ちる。
こんな感じで、自分にとってはとにかく噛み合わない。そもそも日本人が作ってないので当然ではある。CJK文化圏は大体こういう方面ではスルーされる。
という訳でAIパワーで開発に踏み切った。
この類のシステムは、UIは大分決まり切っていてやりたいことも明確で、ただ面倒なだけだったので実にAI向きな開発ネタだと言える。
Bookwallについて
システム構成は以下の様になっている。
| Directory | Role | Stack |
|---|---|---|
server/ |
API + OPDS delivery, SQLite + Active Storage, book scanner, authentication | Rails 8.1 / Ruby 4.0 / Falcon / Thruster / SQLite (FTS5) / SolidQueue |
client/ |
SPA mounted under /ui. Book list / detail / search / favorites / settings / Web Reader |
Vite + React 19 + TypeScript / Tailwind v4 / shadcn/ui / React Router / TanStack Query / Zustand / foliate-js / PDF.js |
久しぶりにRailsで新しくなんか作るかーってので、FalctonとThrusterとSolidQueueという新しめの構成を採用。基本的にVPN内部でセルフホストする用途なので認証も組込みの機能を利用してシンプルに。
UIは独立したSPAで、Vite + React構成。UIコンポーネントにはshadcnを利用した。自分はUI構成の面倒臭さに割と耐えられないタイプなので、claudeがゴリゴリ作ってくれて本当に助かる。
Readerの実装はCBZ readerは普通にRails側でzipを読んで画像を返したりキャッシュしたりする仕組みを用意して画像レンダリングするだけ。
EPUBの実装にはfoliate-jsを利用した。これは直近でLinuxで利用できるEPUB Readerを探していた時に見つけたやつのバックエンドで、自分が知る限りでは、JSでまともに縦書きとルビ表示が出来る唯一の代物だった。
PDFはPDF.jsを利用してcanvasにtextマッピングする形。これでテキストの選択やコピーが出来る。
Falconによる並列リクエストサポートとReact SPAのおかげで結構キビキビと動いてくれる。ライブラリスキャンは高速なSSDに保持していない限りはディスクI/Oネックになるので、CPU並列は余り効果が無いからRubyでもそんなに困らない。
基本的な機能
サポートフォーマット
- CBZ (ComicInfo.xml 対応)
- EPUB
- 画像ディレクトリ
スキャン・メタデータ
- EPUB 拡張メタ:
dc:subjectをタグ、Calibre のcalibre:series/calibre:series_indexをシリーズ・巻数として取り込む - シリーズ フォールバック: メタデータにシリーズが無い書籍は親ディレクトリ名を採用
- 表紙画像: フォーマットごとに 1 ページ目を抽出して Active Storageに保存
- 全文検索: SQLite FTS5 によるタイトル / シリーズ / 著者の AND 検索
- タクソノミー閲覧: シリーズ・著者・タグ別の一覧 + サムネ表示。ライブラリ詳細はブック / シリーズビューをトグル可能
- ソート: 新着順・登録日順・タイトル順・シリーズ順・著者順 (昇降)
Web Reader
- CBZ / PDF / 画像ディレクトリ: ページ画像配信 + クライアント側で表示。見開き / 1 ページ表示、LTR / RTL、4 種類のスケールモード
- EPUB: foliate-js ベース。目次、フォントサイズ、テーマ (light / dark / sepia)、書字方向 (auto 自動判定 / horizontal / vertical) を per-book で保存
- 読書進捗: ページ位置 (CBZ) と CFI + fraction (EPUB) を自動保存。表紙の下端に進捗バーをオーバーレイ、ホーム画面の最近読んだ本カルーセル、書籍詳細ページに反映
- ホバーで現れるスクラバー: 下端から引き出して任意ページに直接ジャンプ。CBZ はホバー位置のサムネを、EPUB は対応する章ラベルをプレビュー表示
- キーボードナビ + ヒント: 矢印 / Space / Backspace / Esc に加え、CBZ では
2で見開きトグル、Shift + 矢印で 1 ページだけ送り。?でショートカット一覧 - クリック領域とホバーフィードバック: 左右 12% のみクリッカブル (中央の文字選択を妨げない)、ホバーティント付き
配信・連携
- OPDS / OPDS-PSE: Atom フィードで他のリーダーアプリに配信
- 認証: Cookie セッション (UI 用) と Bearer トークン (OPDS / Reader 用) を併存
- モバイル対応: 390px〜の viewport で動作
デモ
大体こんな感じで動く。
開発の流れと所感
と言っても、要件を書き出してclaudeにぶん投げて細かく調整指示出しただけだが。
しかし、やっぱ細かいところはおざなりになるもんで、重複した実装が増えたりReactの使い方が雑で画面がちらついたりとかそういうことは多々あるので、ある程度動いたら怪しいところを指定してリファクタリングさせたりとかはしている。
playwright-cliを使って動作確認させていて、基本シナリオはe2eテストとして再利用可能にすることで、挙動のバグとかはclaude自身がかなり自主的に判断してくれる様になった。playwrightはデモ動画を作るのにも利用しているし、後からsnapshotで挙動が追えるのでやはりとても便利。自分はUIの挙動確認がとにかくダルいので、こういうことを高速でやってくれるのは良いと思う。
Railsのコードについては、まあそこそこって感じ。何も指示しないと結構ファットコントローラーな実装をしてくる。後サービスクラス作ってくるw まあ止める程ではないし、用途的にも妥当だと思うが、ファットコントロールと実装重複は見逃せなかったのでざっくりコード読んでチクチク直させた。
一応rubydexのmcpサーバーとかも入れてあるんだけど、効果あんのかどうかは何も分からんw
ちょっとハマった点
FalconとSQLiteとActiveStorageを同時に使って、画像のthumbに並列でアクセスをかけると、そのタイミングで画像生成とActiveStorageの更新が並列で走りまくりSQLiteがめちゃくちゃ書き込み競合を起こして処理が停止する。
これについてはActiveStorageの動きを細かく制御しないとどうにもならんので、事前にライブラリスキャン時にシーケンシャルにvariantを作成する様にして並列リクエスト時に書き込みが走らない様にした。
まとめ
まだちょこちょこ改善するポイントはあるが、自分が使う分には特に困らない電子書籍サーバーが完成した。 一旦外から参照してみたが、場所を選ばずに一元管理されたサーバーからCBZ, EPUB, PDFが滞りなく読めることを確認した。
こういう既存のものが存在するが、自分の利用には中途半端で噛み合わないというものがあった時に、今迄は面倒臭さでかなりスイッチ入れないとやる気にならなかったので、とりあえずざっくり要件だけ書いて大体どうにかなる様になったのはライフチェンジングだなーと思う。viteとかshadcnとか普段全然触ってないんで、チマチマAPI調べるの面倒臭い。
なんにせよ、久しぶりにRailsで自分用のシステムが作れて良かった。
補足: OPDSとEPUBのCFIについて
Open Publication Distribution Systemの略で、書籍などの出版物の配信に関するオープンな規格がある。
また、CBZなどのページストリーミングに対する拡張規格である、OPDS-PSEが存在する。OPDS-PSEは基本的にCBZ用で、EPUBの場合は、CFIというコンテンツのフラグメントを表現するインターフェースがあるため、EPUBを読み込んだ後にfoliate-jsに保存していたCFIを読み込ませることで読書途中のページを復帰できるので、OPDS-PSEには頼らない。PDFの場合はPDF.jsがRangeリクエストに対応していて、段階的にファイルをリクエストすることで読み込みデータ量をコントロールしている。
この規格自体は、書籍のレンタルや販売にも対応できる様なメタデータを持てる様になっているが、自分用のサーバーなのでカタログとページリクエストにのみ利用している。
主要な電子書籍管理サーバーはこの規格を実装しており、OPDSのフィードが配信できれば、複数の電子書籍リーダーでそのアプリのUIを利用して書籍の一覧やページストリーミングが出来る様になり潰しが効く。とは言え、それなりに実装はあるがそこまで豊富ではない。この辺りも国産のアプリは非常に弱くて、自分が知る限りで日本語の書籍を含めてちゃんと動くのはiOSのComicShareぐらい。
カタログをどの様に配信するかはサーバー側の実装の自由なので、この辺の柔軟さもKavitaやStumpで満足いかなかった要因の一つだ。自作していればいくらでも自分の好みで軸を追加できる。
補足2: ComicInfo.xml
CBZファイルにメタデータを埋め込むための規約。
CBZファイルのルートにXMLでメタデータを記述することで、ライブラリスキャン時にメタデータを解釈することができる。書式を決めておけば潰しが効く。ちなみにcalibreにはcalibreのメタデータをComicInfo.xmlとして埋め込んでくれるプラグインがある。 スキーマはこんな感じ。結構色々入れられる様になってる。
これをどの様にマッピングするかはサーバー側の自由なので、この取り扱いが噛み合うかどうかも電子書籍管理サーバーの選択に影響すると思う。
今後
とりあえずWebブラウザ経由で利用する分には、PC・スマホ・タブレットそれぞれで困らないことは確認できた。
iOSに関してはComicShareで大体何とかなるので、後はAndroidのアプリを作るかなーと考えている。用途を限定してOPDSに特化したリーダーアプリならすぐに出来そうな気がする。
AIにロゴとか作らせようかどうしようか……。でも別にそういうオシャレ感要らんしなー……。






