Skip to content

sasasin/sreader

Repository files navigation

SReader

SReader は、公開 RSS/Atom feed を取得し、記事 URL/タイトルと本文抽出結果を PostgreSQL に保存する lightweight feed reader です。

テックスタックは Java 25 + Spring Boot + jOOQ + Flyway + PostgreSQL 18.x です。

ディレクトリ構成

  • app/: Spring Boot application
  • app/src/generated/java/: Git-managed jOOQ generated sources
  • db/migration/: Flyway migration。fresh development DB 前提で feed_urlcontent_headercontent_full_texteft_rules だけを作成
  • docker-compose.yml: postgresflywaymavenapp
  • k8s/: Kubernetes manifests (Kustomize base/overlays)。詳細は k8s/README.md

build/runtime path は app/ module を対象にします。

Docker Compose 経由で操作可能です。ホスト OS に Java / Maven / PostgreSQL client / Flyway を入れる必要はありません。Kubernetes へのデプロイは k8s/README.md を参照してください。

セットアップ

docker compose config
docker compose down -v
docker compose up -d postgres
docker compose ps
docker compose exec postgres psql -U sreader -d sreader -c "SELECT version();"

docker compose down -v はローカル PostgreSQL volume を削除します。既存の開発 DB データが必要な場合は実行しないでください。

Flyway

docker compose run --rm flyway info
docker compose run --rm flyway migrate
docker compose run --rm flyway info
docker compose exec postgres psql -U sreader -d sreader -c "\dt"

Spring Boot app 起動時にも Flyway auto migration が有効です。

jOOQ generated sources policy

jOOQ generated sources は Git 管理します。通常の app 実装変更では generated sources を再生成・手編集しないでください。

db/migration/*.sql を変更した場合:

  1. build-time PostgreSQL を起動する
  2. Flyway migration を適用する
  3. mvn -Pgenerate-jooq -pl app -am generate-sources を実行する
  4. generated source の diff を同じ commit に含める

jOOQ の version または codegen 設定を変更した場合:

  1. mvn -Pgenerate-jooq -pl app -am generate-sources を実行する
  2. generated source の diff を確認して commit する

generated sources の配置先は app/src/generated/java/ です。

jOOQ / build / test

通常 build は checked-in generated sources を compile し、jOOQ code generation は実行しません。 package -DskipTests は build-time PostgreSQL なしで実行できます。

docker compose run --rm maven mvn -version
docker compose run --rm maven java -version
docker compose run --rm maven mvn -pl app -am package -DskipTests

test は PostgreSQL を使うため、先に DB を起動してから実行してください。

docker compose up -d postgres
docker compose run --rm maven mvn test

カバレッジを含む正式確認では verify を実行します。JaCoCo は HTML、XML、CSV の 各形式でレポートを生成し、アプリケーション全体の BUNDLE 分岐カバレッジが 90% 未満の場合はビルドを失敗させます。

docker compose up -d postgres
docker compose run --rm maven mvn clean verify

HTML レポートは app/target/site/jacoco/index.html に出力されます。master の CI が 成功した最新レポートは GitHub Pages でも公開されます。

jOOQ generated sources(net/sasasin/sreader/jooq/**)は集計対象外で、手書き プロダクションコードは原則としてすべて集計します。このゲートはローカルと GitHub Actions の双方で mvn clean verify により実行されます。

ゲート導入前の初回計測値は docs/testing/coverage-baseline.md を参照してください。ゲート有効化直前の参考値は、BUNDLE 分岐カバレッジ 622/667(93.25%)です。

jOOQ sources を再生成する場合は、先に fresh DB へ Flyway migration を適用してください。

docker compose up -d postgres
docker compose run --rm flyway migrate
docker compose run --rm maven mvn -Pgenerate-jooq -pl app -am generate-sources

同等の手順は scripts/generate-jooq.sh でも実行できます。

maven service は jOOQ codegen 用に SREADER_JOOQ_JDBC_* 環境変数経由で sreader DB へ接続し、test 実行時は sreadertest DB に接続します。docker/postgres/init/00-create-test-database.sql が test DB/role を作成します。

Renovate

依存関係と Docker image の更新検知には Renovate を使います。設定は repository root の renovate.json です。

現在の設定:

  • config:recommended を継承します。
  • release 直後の不安定な更新を避けるため、minimumReleaseAge3 days にしています。
  • Kubernetes manifest、target k3s version(k8s/ops/versions.env)、Kubernetes CI 補助ツールも更新検出の対象です。互換性の判定は Kubernetes manifests CI が担当します。

Renovate GitHub App を有効化する手順:

  1. GitHub の sasasin/sreader repository を開きます。
  2. Settings -> Integrations -> GitHub Apps を開きます。
  3. Renovate が未インストールの場合は Renovate GitHub App を開き、Install または Configure から sasasin/sreader を対象 repository に追加します。
  4. 初回 onboarding PR が作られた場合は、PR の内容が root の renovate.json と矛盾しないことを確認して close するか、必要に応じて取り込みます。
  5. 以後の Renovate PR では、この README の Docker Compose コマンドで検証します。

app 起動

docker compose build app
docker compose up app

app container は /app/app.jar を実行します。

Container image publishing (GHCR)

master への push ごとに、GitHub Actions が application container image を build して GitHub Container Registry (GHCR) へ publish します。手動実行は workflow_dispatch でも可能です。

  • Workflow: .github/workflows/publish-container.yml
  • Image: ghcr.io/sasasin/sreader
  • Tags:
    • sha-<full-commit-sha> (immutable。40 文字の full commit SHA。home-server への deploy に推奨)
    • master (mutable。動作確認用)

sha-<full-commit-sha>sha- に続けて 40 文字の full Git commit SHA を付けた tag です。例: sha-412ddade0cf36492fb52ec2060f5afce81c0de10。deploy 対象の SHA は git rev-parse HEAD や GitHub commit ページの full SHA と対応させてください。rollback と再現性のため、home-server の k3s Deployment ではこの tag を使ってください。master tag は毎回上書きされるため、本番相当の運用では使わないでください。

GHCR package は public 想定です。初回 publish 後、GitHub の repository Packages から sreader container package を開き、visibility が public であることを確認してください。public image の pull に追加 credential は不要です。

GitHub Actions から home-server への自動 deploy は行いません。deploy は手動です。

Home-server deployment (Ubuntu + k3s)

通常の home-server deploy では、以下は不要です。

  • home-server 上での Maven package
  • home-server 上での Docker image build
  • ローカル build した tarball の k3s containerd image import

代わりに、GHCR から publish 済み image を pull し、k3s Deployment を更新します。deploy する full commit SHA を確認し、sha-<full-commit-sha> tag を指定してください。

kubectl -n sreader scale deployment/sreader --replicas=0

kubectl -n sreader set image deployment/sreader \
  sreader=ghcr.io/sasasin/sreader:sha-<full-commit-sha>

kubectl -n sreader scale deployment/sreader --replicas=1

kubectl -n sreader rollout status deployment/sreader --timeout=300s
kubectl -n sreader logs deployment/sreader --tail=120

Deployment manifest では、通常は SHA-based tag を使います。

containers:
  - name: sreader
    image: ghcr.io/sasasin/sreader:sha-<full-commit-sha>
    imagePullPolicy: IfNotPresent

master tag で試す場合は imagePullPolicy: Always が必要になることがありますが、本番相当の home-server 運用では master tag は使わないでください。

DB migration と backup

app 起動時の Flyway auto migration は、従来どおり有効です。

db/migration/*.sql を含む更新は、通常の app image 更新より慎重に扱ってください。home-server へ deploy する前に、PostgreSQL backup を取ることを検討してください。例:

mkdir -p /srv/sreader/backups

pg_dump -Fc \
  -h 127.0.0.1 \
  -U sreader \
  -d sreader \
  -f "/srv/sreader/backups/sreader-$(date +%Y%m%d-%H%M%S).dump"

host、user、database 名は環境に合わせて読み替えてください。

Scheduler

Spring Scheduler がアプリケーションコンテナ内で job を実行します。

主な環境変数:

  • SREADER_SCHEDULER_ENABLED=true
  • SREADER_SCHEDULER_CRON=0 */15 * * * *
  • SREADER_HTTP_USER_AGENT=SReader/0.1
  • SREADER_HTTP_CONNECT_TIMEOUT=5s
  • SREADER_HTTP_READ_TIMEOUT=20s
  • SREADER_HTTP_RETRY_COUNT=1
  • SREADER_PLAYWRIGHT_ENABLED=false
  • SREADER_SEED_FEED_URLS= comma separated feed URLs

無効化起動:

docker compose run --rm -e SREADER_SCHEDULER_ENABLED=false app

1 回だけ job を実行:

docker compose run --rm app --sreader.scheduler.enabled=false run-once

既存の --sreader.job.run-once=true も後方互換のため利用できます。

全文取得 method

feed_url.full_text_method は以下を指定できます。省略時は従来通り http です。

  • feed: RSS/Atom entry 本文を保存します。
  • http: 記事 URL を HTTP GET し、XPath rule、失敗時 body text で抽出します。
  • http_readability: 記事 URL を HTTP GET し、取得した HTML を Readability4J で抽出します。
  • playwright: Chromium で JS 実行後の DOM HTML から XPath rule、失敗時 body text で抽出します。
  • playwright_readability: Chromium で JS 実行後の DOM HTML を Readability4J で抽出します。
  • playwright_infy_scroll: Infy Scroll extension を読み込んだ Chromium で scroll 後の DOM HTML から XPath rule、失敗時 body text で抽出します。
  • playwright_infy_scroll_readability: Infy Scroll 後の DOM HTML を Readability4J で抽出します。

Playwright 系 method はデフォルトでは無効です。使う場合は SREADER_PLAYWRIGHT_ENABLED=true を設定してください。無効時に Playwright 系 method の feed がある場合、その記事の全文取得は warning log を出して skip します。

Playwright / Infy Scroll 用の主な環境変数:

  • SREADER_PLAYWRIGHT_HEADLESS=true
  • SREADER_PLAYWRIGHT_VIEWPORT_WIDTH=1280
  • SREADER_PLAYWRIGHT_VIEWPORT_HEIGHT=1600
  • SREADER_PLAYWRIGHT_NAVIGATION_TIMEOUT=60s
  • SREADER_PLAYWRIGHT_NETWORK_IDLE_TIMEOUT=5s
  • SREADER_PLAYWRIGHT_INFY_EXTENSION_DIR=/opt/sreader/extensions/infy-scroll
  • SREADER_PLAYWRIGHT_INFY_USER_DATA_DIR=/var/lib/sreader/playwright-infy-profile
  • SREADER_PLAYWRIGHT_INFY_MAX_SCROLLS=20
  • SREADER_PLAYWRIGHT_INFY_STABLE_ROUNDS=3
  • SREADER_PLAYWRIGHT_INFY_SCROLL_WAIT=2700ms

Docker app image は Playwright の bundled Chromium と、その Chromium 専用の OS 依存ライブラリを含みます。ブラウザは /ms-playwright に固定して配置され、実行時のブラウザ自動ダウンロードは無効です。Playwright のバージョンを更新した場合は、app image を再ビルドしてください。

Maven service で Playwright の bundled browser を使う場合は、Docker Compose 経由で以下を実行してください。

docker compose run --rm maven mvn -pl app exec:java -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install chromium"

Infy Scroll は実行時に Chrome Web Store からインストールしません。Chromium が load できる unpacked extension directory を用意し、その directory を SREADER_PLAYWRIGHT_INFY_EXTENSION_DIR に指定してください。GitHub source tree をそのまま指定して必ず動くとは限らないため、manifest.json を含む extension artifact を配置してください。

一度 content_full_text に保存された記事は insertIfAbsent のため、full_text_method を変更しても自動再抽出されません。再抽出が必要な場合は対象行の扱いを別途検討してください。

Content text export

全文取得済みの記事をテキストファイルへ出力できます。デフォルトでは無効です。

有効化する場合は Docker Compose の環境変数で指定します。

SREADER_TEXT_EXPORT_ENABLED=true
SREADER_TEXT_EXPORT_HOST_DIR=./var/sreader/content-text
SREADER_TEXT_EXPORT_BATCH_SIZE=100

app container 内の出力先は以下です。Docker Compose ではこの path が host directory に bind mount されます。

/var/lib/sreader/content-text

ファイルはディレクトリ分散せず、content_header.id を使った相対ファイル名で作成されます。

/var/lib/sreader/content-text/<content_header.id>.txt

各ファイルの内容は UTF-8 で以下の形式です。

URL: <content_header.canonical_url>
TITLE: <content_header.title>

<content_full_text.full_text>

出力対象は、content_full_text.full_text が空でなく、まだ content_text_file_export に記録されていない content_header + content_full_text です。ファイル書き込みに成功したものだけ content_text_file_export に記録され、記録済みの記事は再出力されません。書き込みに失敗した記事は出力済み扱いにせず、次回 job で再試行されます。

記事 URL は 3 種類を保存します。source_url は RSS/Atom の link、fetch_url はリダイレクト解決後に本文を取得する URL、canonical_url は記事の同一性判定とテキスト出力に使う正規化済み URL です。

関連する環境変数:

  • SREADER_TEXT_EXPORT_ENABLED=false
  • SREADER_TEXT_EXPORT_OUTPUT_DIR=/var/lib/sreader/content-text
  • SREADER_TEXT_EXPORT_HOST_DIR=./var/sreader/content-text
  • SREADER_TEXT_EXPORT_BATCH_SIZE=100

feed URL TOML import / export

feed_url は購読状態を持つ購読レコードです。

  • active: 記事取得対象です。
  • unsubscribed: 購読停止済みです。DELETE ではなく tombstone として残し、過去に停止した feed が別環境や古い TOML import で誤って復活しにくいようにします。

export はデフォルトで activeunsubscribed の両方を URL 昇順で出力します。

docker compose run --rm app --sreader.scheduler.enabled=false feeds export --output /tmp/feeds.toml
docker compose run --rm app --sreader.scheduler.enabled=false feeds export --active-only --output /tmp/active-feeds.toml

import は safe merge です。TOML に存在する feed だけを insert/update し、TOML に存在しない DB 側 feed はデフォルトでは変更しません。dry-run で件数と conflict を確認できます。

docker compose run --rm -v ./feeds.toml:/tmp/feeds.toml:ro app --sreader.scheduler.enabled=false feeds import --input /tmp/feeds.toml --dry-run
docker compose run --rm -v ./feeds.toml:/tmp/feeds.toml:ro app --sreader.scheduler.enabled=false feeds import --input /tmp/feeds.toml

DB 側が unsubscribed、TOML 側が active の場合、デフォルトでは復活させず conflict として報告します。明示的に復活させる場合だけ --resubscribe を付けます。

docker compose run --rm -v ./feeds.toml:/tmp/feeds.toml:ro app --sreader.scheduler.enabled=false feeds import --input /tmp/feeds.toml --resubscribe

TOML import 前に全文取得方式を試す

feeds importfull_text_method を指定する前に、probe コマンドで実際の記事/フィードに対して方式を試せます。DB へは何も書き込みません。

docker compose run --rm app --sreader.scheduler.enabled=false probe article \
  --url https://example.com/article \
  --method http_readability
docker compose run -e SREADER_PLAYWRIGHT_ENABLED=true --rm app --sreader.scheduler.enabled=false probe feed \
  --feed-url https://example.com/feed.xml \
  --method playwright_readability
docker compose run --rm app --sreader.scheduler.enabled=false feeds discover \
  --site-url https://example.com/
  • probe article / probe feed は本文を STDOUT へ(--output 指定時はファイルへ)。--verbose 時は診断情報を STDERR へ。
  • --xpath で XPath 抽出を明示的にテストできます(DB rule / Readability をバイパス)。
  • feeds discover はページから RSS/Atom リンクを検出。--format toml で import 用の雛形を出力。
  • Playwright 系 method を使う場合は SREADER_PLAYWRIGHT_ENABLED=true(および関連設定)が必要です。

TOML schema:

schema_version = 2
generated_at = "2026-06-14T12:00:00+09:00"

[[feeds]]
url = "https://example.com/feed.xml"
status = "active"
full_text_method = "http"

[[feeds]]
url = "https://closed.example/rss.xml"
status = "unsubscribed"
unsubscribe_reason = "site_closed"
unsubscribed_at = "2026-06-14T12:00:00+09:00"
note = "サイト閉鎖を確認したため"
full_text_method = "http"

schema_version = 2(エクスポート時の現在のバージョン)と feeds[].url は必須です。import では schema_version = 1(後方互換)も受け付けます(full_text_method 未指定時は "http" として扱います)。status は省略時 active です。full_text_method は省略時 "http" です。

URL は trim され、http / https の absolute URI のみ許可します。userinfo を含む URL は拒否します。同一 TOML 内で正規化後 URL が重複した場合も validation error です。

unsubscribe_reasonunsubscribed 用の任意項目です。省略時は other として扱います。

  • not_interested: もう関心がない。
  • site_closed: サイト閉鎖。
  • feed_dead: feed が取得不能・壊れている。
  • moved: 移転済み。
  • other: その他。

既存 canonical URL 重複の統合

先行する canonical URL 導入後も、過去に保存された URL(たとえば追跡用 query parameter が異なる記事)が重複している場合があります。content canonicalize はそれらを canonical URL ごとに1件へ統合する明示的なメンテナンスコマンドです。通常の scheduler と同時に実行しないでください。

実行前に PostgreSQL のバックアップを取得し、まず対象 host に限定した dry-run を確認します。--apply を付けない場合も dry-run です。

docker compose run --rm app --sreader.scheduler.enabled=false \
  content canonicalize --host <target-host> --dry-run

summary と対象件数を確認したら、初回は小さい --limit または --batch-size で適用します。

docker compose run --rm app --sreader.scheduler.enabled=false \
  content canonicalize --host <target-host> --limit 10 --apply

統合では canonical URL の MD5 を survivor ID とし、空でない最長の本文を保持します。export history は削除され、旧 ID と survivor ID の .txt は削除されるため、次の text exporter 実行で survivor のファイルが再生成されます。DB 統合は group ごとに commit してからファイルを削除します。ファイル削除エラーは failed_files として表示され、コマンドは non-zero で終了します。

apply 後は同じ dry-run を再実行して候補がなくなったことを確認し、text exporter を実行して survivor のファイルが生成されたことと orphan file が残っていないことを確認してください。

残課題

  • 本格 Web UI
  • production monitoring / secret manager 設計
  • distributed scheduler lock

配布条件

本プログラムはフリーソフトウェアです。LGPL (the GNU Lesser General Public License) バージョン 3、またはそれ以降のバージョンに示す条件で本プログラムを再配布できます。LGPL については LICENSE ファイルを参照してください。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages