🌊

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://developers.google.com/photos/support/updates?hl=ja

廃止されたスコープ

以下スコープが完全に廃止されました

  • <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を活用してコード開発を行いました。 (すごく便利です!!)
https://github.com/ChikaKakazu/gphoto-cli

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