ジャパンサーチ( https://jpsearch.go.jp )のWeb APIを使い、日本の文化資源を探索するiOS/Androidアプリ「JPS Explorer」を開発しました。API調査からアプリ実装、App Storeリリースの自動化までの過程を記録します。
ジャパンサーチのAPI
ジャパンサーチは国立国会図書館が運営する、3,200万件以上のデジタル文化資源のメタデータを横断検索できるサービスです。簡易Web APIが公開されており、以下のような検索が可能です。
| パラメータ | 機能 |
|---|---|
keyword | キーワード検索 |
text2image | テキストでモチーフを指定して画像検索 |
image | 既存アイテムIDで類似画像検索 |
g-coordinates | 緯度・経度・半径で場所検索 |
r-tempo | 年代範囲で時代検索 |
API調査で気づいた点
座標フィールドのキー名
位置情報検索(g-coordinates)のレスポンスで座標データは common.coordinates に格納されています。経度のキーは lon で、lng や longitude ではありません。
"coordinates": {
"lat": 35.669,
"lon": 139.764
}
ギャラリーAPIの多言語フィールド
ギャラリー検索(/api/curation/search)のレスポンスでは、title と summary が文字列ではなくオブジェクトになっています。
"title": {"ja": "耳鳥斎", "en": "Jichosai"},
"image": {"url": "https://...", "thumbnailUrl": "https://..."}
単純に .toString() すると {ja: 耳鳥斎, en: Jichosai} のような文字列がUIに表示されてしまうため、言語キーでアクセスする必要があります。
ギャラリー詳細のアイテム構造
ギャラリー詳細(/api/curation/{id})のアイテムは contents ではなく parts 配列にネストされています。type: "jps-curation-list-item" を再帰的に探索してIDを収集する必要がありました。一部のギャラリーでは subPages にもアイテムが含まれているようです。
画像アップロードによる類似検索
公式APIガイドには記載されていませんが、Web UIのネットワーク通信を調査したところ、画像アップロードによる類似検索が3段階のAPIで実現されていることがわかりました。
POST /dl/api/imagefeatures/で画像のBase64データから64次元の特徴量ベクトルを取得POST /api/item/create-image-featureで特徴量から一時的な検索IDを生成GET /api/item/search/jps-cross?image={ID}で通常の類似検索を実行
Step 2では X-Requested-With: XmlHttpRequest ヘッダーが必要で、レスポンスはプレーンテキストでIDが返ります。
この仕組みを利用して、カメラで撮影した画像から直接ジャパンサーチの類似画像を検索できるようにしています。
利用条件・データベース名のID問題
検索結果の common.contentsRightsType は pdm、ccby などのコード値です。common.database や common.provider もIDで返ってきます。人間が読めるラベルを表示するには、別途 /api/database/{id} や /api/organization/{id} を呼び出してキャッシュする必要があります。
アプリの実装
技術スタック
- Flutter + Riverpod(状態管理)
- flutter_map + OpenStreetMap(地図表示)
- CachedNetworkImage(画像キャッシュ)
- 日英ローカライゼーション
モバイル固有の機能
ブラウザ版ジャパンサーチとの差別化として、以下のモバイル固有の機能を実装しました。
| 機能 | 使用API | モバイル技術 |
|---|---|---|
| 撮影画像で類似検索 | 非公開API(imagefeatures) | カメラ / image_picker |
| 周辺文化資源の通知 | g-coordinates | バックグラウンド位置監視 + ローカル通知 |
| マップ探索 | g-coordinates | flutter_map + Geolocator |
| オフラインお気に入り | — | ローカルストレージ + 画像キャッシュ |
| Spotlight連携 | — | CoreSpotlight(iOS MethodChannel) |
| 触覚フィードバック | — | HapticFeedback |
| 本日の一品 | keyword(ランダムオフセット) | SharedPreferences で日替わりキャッシュ |
周辺通知の仕組み
バックグラウンドで位置情報を監視し、500m以上移動したらJPS APIで周辺を検索して、新しい文化資源が見つかったらローカル通知を送信します。バッテリー消費を抑えるため LocationAccuracy.low と distanceFilter: 500 を使用しています。同じアイテムの重複通知は直近100件のIDを SharedPreferences に保持して防止しています。
Spotlight連携
iOSのCoreSpotlightにアイテムをインデックスするため、MethodChannel でSwiftのネイティブコードを呼び出しています。AppDelegateに直接実装する方法を採用しました。別ファイル(SpotlightPlugin.swift)にするとXcodeプロジェクトファイル(.pbxproj)への登録が必要で、CLIからの自動化が困難なためです。
App Storeリリースの自動化
自動化できた作業
scripts/release.py で以下をコマンドラインから実行できるようにしています。
python3 scripts/release.py build # ビルド & アップロード
python3 scripts/release.py screenshots # スクリーンショット撮影
python3 scripts/release.py submit # メタデータ設定 → 審査提出
| 作業 | 方法 |
|---|---|
| Bundle ID登録 | POST /v1/bundleIds |
| ビルド & アップロード | flutter build ipa → xcrun altool --upload-app |
| メタデータ設定 | PATCH /v1/appStoreVersionLocalizations/{id} |
| スクリーンショット撮影 | xcrun simctl io screenshot + Pillowでマーケティング画像生成 |
| スクリーンショットアップロード | 3段階(予約→バイナリ→コミット) |
| 年齢レーティング | PATCH /v1/ageRatingDeclarations/{id} |
| カテゴリ・著作権 | PATCH /v1/appInfos/{id}, PATCH /v1/appStoreVersions/{id} |
| ビルド紐付け | PATCH /v1/appStoreVersions/{id}/relationships/build |
| 暗号化コンプライアンス | PATCH /v1/builds/{id} |
| レビュー詳細 | POST /v1/appStoreReviewDetails |
| 審査提出 | POST /v1/reviewSubmissions + reviewSubmissionItems |
自動化できなかった作業
調査した限り、以下の作業はAPIからの実行ができないようでした。
| 作業 | 理由 |
|---|---|
| アプリの新規作成 | /v1/apps はGETのみで、POSTは存在しない |
| App Privacy(データ使用状況の宣言) | OpenAPI仕様を網羅的に検索したところ、appDataUsages 等の関連エンドポイントがv1/v2いずれにも見つからなかった |
| In-App Purchase商品の登録 | 初回のみブラウザから作成が必要 |
App Privacyについては、Appleが配布している公式OpenAPI JSON(openapi.oas.json)の全パスを privacy, dataUsage, consent, declaration 等のキーワードで検索し、該当するエンドポイントが存在しないことを確認しています。
.envによる設定管理
APIキーや連絡先情報をハードコードせず、.env ファイルから読み込む設計にしています。.env.example にテンプレートを用意しており、各自の環境に合わせて設定します。
リリース作業で気づいた注意点
漏れやすい設定項目
APIで全項目を設定したつもりでも、以下の項目が漏れやすいようです。提出前にチェックスクリプトで確認するのがよさそうです。
| 項目 | API | 備考 |
|---|---|---|
| Promotional Text | appStoreVersionLocalizations の promotionalText | 任意だが設定推奨。審査なしでいつでも変更可能 |
| Marketing URL | appStoreVersionLocalizations の marketingUrl | 任意 |
| Privacy URL(英語) | appInfoLocalizations | 日本語版と同じURLを設定しがち。英語版パスへの修正が必要 |
| 価格設定 | appPriceSchedules | 無料アプリでも明示的に設定が必要。appPricePoints でFREE(price=0)のIDを取得して使う |
初回バージョンでは whatsNew が設定不可
初回リリース時に whatsNew(新機能の説明)を設定すると 409 STATE_ERROR になります。このフィールドは2回目以降のバージョンアップ時のみ設定可能なようです。
電話番号のフォーマット
レビュー詳細の contactPhone は +国番号-番号 形式が必要です。ダミーの番号は無効と判定されるため、既存アプリのレビュー詳細から GET /v1/appStoreVersions/{id}/appStoreReviewDetail で取得して流用するのが確実でした。
スクリーンショット撮影とマーケティング画像生成
Flutterの integration_test を使い、各画面のスクリーンショットを自動撮影しています。テスト内で ActionChip のタップや NavigationBar のアイコンタップを行い、探索→検索結果→詳細→マップ→ギャラリーの5画面を自動遷移します。
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ... app.main() → pumpAndSettle → takeScreenshot
シミュレータの言語を切り替えて日本語・英語それぞれで撮影し、iPhoneとiPadの2デバイスで実行することで、5画面 x 2言語 x 2デバイス = 20枚のスクリーンショットを自動で取得しています。
マーケティング画像の生成では、以下のような工夫をしています。
SCREENSHOT_PRIORITYでどの画面のスクリーンショットをどのテーマに割り当てるかを制御。ファイル名のプレフィックス(02_search,05_gallery等)でマッチングするfind_best_screenshots(input_dir, count=5)のcountをテーマ数と一致させる必要がある。デフォルトのcount=3だとテーマと画面の対応がずれるbleed_fraction = 0.35でデバイスの下部35%を画面外にはみ出させる「見切れレイアウト」にしている- iPhone(アスペクト比 ~0.46)とiPad(~0.75)でフォントサイズやスクリーンショットの拡大率を自動調整
- 出力ファイル名は
marketing_01_iphone.png,marketing_01_ipad.pngのようにデバイス種別を含む形式
審査提出後のスクリーンショット変更
審査に提出した後(Waiting For Review状態)は、スクリーンショットの削除・追加ができません。スクリーンショットの品質は提出前に確認しておく必要があります。
スクリーンショットの言語設定
Flutterアプリのスクリーンショットを自動撮影する際、シミュレータの言語設定がアプリのUI言語に直結します。日本語のスクリーンショットを撮るにはシミュレータの言語を日本語に切り替える必要があります。また、同じスクリーンショットを複数枚コピーして使い回すと、審査でリジェクトされる可能性があるようです。ファイルのハッシュ値を比較して重複を検出するチェック機構を入れておくのがよさそうです。
プロジェクト構成
jps_explorer/
├── lib/
│ ├── models/jps_item.dart
│ ├── services/
│ │ ├── jps_api_service.dart # JPS API クライアント
│ │ ├── label_service.dart # DB/org/rights ラベル解決
│ │ ├── daily_pick_service.dart # 日替わりアイテム
│ │ ├── nearby_notification_service.dart
│ │ ├── image_cache_service.dart # オフライン画像
│ │ ├── spotlight_service.dart # iOS Spotlight
│ │ ├── favorites_service.dart
│ │ └── tip_jar_service.dart # チップ
│ ├── views/ # 各画面
│ └── providers/app_providers.dart # Riverpod
├── scripts/
│ ├── release.py # ビルド・提出の自動化
│ ├── capture_screenshots.sh # スクリーンショット撮影
│ └── generate_marketing_screenshots.py # マーケティング画像生成
├── .env # 設定(gitignore)
└── .env.example # 設定テンプレート