🌊
Picker APIを使ってGoでGoogle Photosへアクセス
はじめに
2025年3月31日に、Google Photos APIが仕様変更しました。この変更により、従来のLibrary APIの多くの機能が制限され、新しいPicker APIへの移行が必要となります。今回は、この変更の詳細と実際の移行方法について、Go言語で作成したCLIツールの実装例と共に解説したいと思います。
※内容に齟齬があれば、ぜひ教えてください。
2025年3月31日の変更内容
Library APIの機能制限
- アプリが作成していないコンテンツへのアクセス不可
- 共有アルバム機能(share, unshare, get, join, leave, list)の利用不可
- ユーザーの全写真ライブラリへの自由なアクセス不可
この変更により、Library APIからGoogle Photosへのアクセスができなくなりました。
従来通りの操作を行う対象は、アプリケーション側からアップロードした画像・動画のみとなりました。
廃止されたスコープ
以下スコープが完全に廃止されました
<https://www.googleapis.com/auth/photoslibrary.readonly>
<https://www.googleapis.com/auth/photoslibrary.sharing>
<https://www.googleapis.com/auth/photoslibrary>
継続利用可能なスコープ
<https://www.googleapis.com/auth/photoslibrary.appendonly>
<https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata>
<https://www.googleapis.com/auth/photoslibrary.edit.appcreateddata>
Picker APIの導入
Picker APIとは
Google Photos Picker APIは、ユーザーが自分の写真ライブラリから安全に写真を選択するための新しいAPIです。
- セキュリティ重視: ユーザーが明示的に選択した写真のみアクセス可能
-
ブラウザベース: ユーザーはGoogleの公式インターフェースで写真を選択
- Googleが用意した画面UIからのみしか、写真選択ができない
- セッション管理: セッションベースで動作
Picker APIの制限
- アルバム一覧の表示
- 日付やコンテンツによるフィルタリング
- アルバム内写真の動的更新
実装例
Picker APIを利用した画像選択・ダウンロード機能を実装したCLIツール「gphoto-cli」を例に実装例を紹介します。
この実装では、Claude Codeを活用してコード開発を行いました。 (すごく便利です!!)
OAuth2.0設定
Picker API専用のスコープを設定します
config := &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURI,
Scopes: []string{"<https://www.googleapis.com/auth/photospicker.mediaitems.readonly>"},
Endpoint: oauth2.Endpoint{
AuthURL: "<https://accounts.google.com/o/oauth2/auth>",
TokenURL: "<https://oauth2.googleapis.com/token>",
},
}
セッションの作成と管理
type PickerSession struct {
Name string `json:"name"`
PickerUri string `json:"pickerUri"`
MediaItemsSet bool `json:"mediaItemsSet"`
ID string `json:"id"`
}
func (pc *PickerClient) CreateSession(ctx context.Context) (*PickerSession, error) {
url := "<https://photospicker.googleapis.com/v1/sessions>"
reqBody := map[string]interface{}{}
jsonData, _ := json.Marshal(reqBody)
req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer "+pc.accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := pc.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var session PickerSession
json.NewDecoder(resp.Body).Decode(&session)
return &session, nil
}
ユーザーの選択待ちとポーリング
func (pc *PickerClient) WaitForSelection(ctx context.Context, sessionName string) error {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
timeout := time.After(10 * time.Minute)
for {
select {
case <-timeout:
return fmt.Errorf("写真選択がタイムアウトしました")
case <-ticker.C:
session, err := pc.GetSession(ctx, sessionName)
if err != nil {
return err
}
if session.MediaItemsSet {
return nil // 選択完了
}
}
}
}
選択された写真の取得
type MediaItem struct {
ID string `json:"id"`
CreateTime string `json:"createTime"`
Type string `json:"type"`
MediaFile MediaFile `json:"mediaFile"`
}
func (pc *PickerClient) ListMediaItems(ctx context.Context, sessionName string) ([]MediaItem, error) {
sessionId := strings.TrimPrefix(sessionName, "sessions/")
url := fmt.Sprintf("<https://photospicker.googleapis.com/v1/mediaItems?sessionId=%s>", sessionId)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+pc.accessToken)
resp, _ := pc.httpClient.Do(req)
defer resp.Body.Close()
var response struct {
MediaItems []MediaItem `json:"mediaItems"`
}
json.NewDecoder(resp.Body).Decode(&response)
return response.MediaItems, nil
}
Discussion