ジャパンサーチ( 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 で、lnglongitude ではありません。

"coordinates": {
  "lat": 35.669,
  "lon": 139.764
}

ギャラリーAPIの多言語フィールド

ギャラリー検索(/api/curation/search)のレスポンスでは、titlesummary が文字列ではなくオブジェクトになっています。

"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で実現されていることがわかりました。

  1. POST /dl/api/imagefeatures/ で画像のBase64データから64次元の特徴量ベクトルを取得
  2. POST /api/item/create-image-feature で特徴量から一時的な検索IDを生成
  3. GET /api/item/search/jps-cross?image={ID} で通常の類似検索を実行

Step 2では X-Requested-With: XmlHttpRequest ヘッダーが必要で、レスポンスはプレーンテキストでIDが返ります。

この仕組みを利用して、カメラで撮影した画像から直接ジャパンサーチの類似画像を検索できるようにしています。

利用条件・データベース名のID問題

検索結果の common.contentsRightsTypepdmccby などのコード値です。common.databasecommon.provider もIDで返ってきます。人間が読めるラベルを表示するには、別途 /api/database/{id}/api/organization/{id} を呼び出してキャッシュする必要があります。

アプリの実装

技術スタック

  • Flutter + Riverpod(状態管理)
  • flutter_map + OpenStreetMap(地図表示)
  • CachedNetworkImage(画像キャッシュ)
  • 日英ローカライゼーション

モバイル固有の機能

ブラウザ版ジャパンサーチとの差別化として、以下のモバイル固有の機能を実装しました。

機能使用APIモバイル技術
撮影画像で類似検索非公開API(imagefeatures)カメラ / image_picker
周辺文化資源の通知g-coordinatesバックグラウンド位置監視 + ローカル通知
マップ探索g-coordinatesflutter_map + Geolocator
オフラインお気に入りローカルストレージ + 画像キャッシュ
Spotlight連携CoreSpotlight(iOS MethodChannel)
触覚フィードバックHapticFeedback
本日の一品keyword(ランダムオフセット)SharedPreferences で日替わりキャッシュ

周辺通知の仕組み

バックグラウンドで位置情報を監視し、500m以上移動したらJPS APIで周辺を検索して、新しい文化資源が見つかったらローカル通知を送信します。バッテリー消費を抑えるため LocationAccuracy.lowdistanceFilter: 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 ipaxcrun 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 TextappStoreVersionLocalizationspromotionalText任意だが設定推奨。審査なしでいつでも変更可能
Marketing URLappStoreVersionLocalizationsmarketingUrl任意
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                      # 設定テンプレート