[追記あり] Google Photos APIsでアルバム作成と写真のアップロード

Picasa(デスクトップ版)の提供が2016年に終了し、インストール済みのデスクトップ版PicasaのWeb同期機能も2018年3月で完全に(?)停止してしまい[^1]、趣味で撮ってる写真のGoogleサービスとの同期に困っていた[^2]のですが、2018年5月に[Google Photos APIs](https://developers.google.com/photos/)が公開されたので、アルバムの作成とアップロードができるか試してみました。

[^1]: [Picasa Web Albums Data API](https://developers.google.com/picasa-web/)自体はまだ使える様子。ログを見た感じではPicasaが使用しているトークンが無効化されてるみたい。
[^2]: PicasaではPC上でフォルダ分類した画像がそのままアルバムとしてアップロードされたが、代替ソフトの「バックアップと同期」にはその機能がなかった

----

- 2018.08.12 追記(2): ファイルサイズと、重複ファイル・ファイル名の指定方法について追記
- 2018.05.14 追記(1): 日本語のアルバムタイトルの作り方について記述

# TL;DR

1. APIの有効化は[Get started](https://developers.google.com/photos/library/guides/get-started)で。
2. OAuthの要領は[Google APIのAccess Tokenをお手軽に取得する](https://qiita.com/shin1ogawa/items/49a076f62e5f17f18fe5)から。
3. アルバムアップロードをするには、
   4. [アルバムを作成する](https://developers.google.com/photos/library/guides/create-albums)
   5. [画像アップロード時にアルバムを指定する](https://developers.google.com/photos/library/guides/upload-media)
      - Uploading bytes
      - Creating a media item の2つのAPI実行が必要
6. 画像の更新や、既にアップロードされている画像をアルバムに追加するAPIはなさそう？


# APIの有効化

[Get started  |  Google Photos APIs  |  Google Developers](https://developers.google.com/photos/library/guides/get-started)の Enable the API を見て行けばOK

たぶん1.の[ENABLE THE GOOGLE PHOTOS LIBRARY API]押下が早いと思うけど、2.の手順で設定。

## プロジェクトの作成

1. [Google API Console](https://console.developers.google.com/apis/library)にアクセス
2. [プロジェクトを選択]から新しくプロジェクトを作成![2018-05-10_21h48_44.png](https://qiita-image-store.s3.amazonaws.com/0/256859/0b4e17e2-b714-274d-d9a0-4736278258e4.png)
3. 今回は[photo-practice01]と入力

## APIの有効化

1. [APIとサービスを検索]に[photo]と入れて検索
2. [Photos Library API]が出てくるので選択
3. [有効にする]ボタン押下
4. プロジェクトの選択画面になるので、[photo-practice01]を選択

これでダッシュボードから使用状況が見えるようになる。

# 認証とトークンの作成

[Google APIのAccess Tokenをお手軽に取得する](https://qiita.com/shin1ogawa/items/49a076f62e5f17f18fe5)
今回はWebアプリケーションでなくシェル上からコマンドを叩くだけの予定なので、こちらの記事が大変参考になった。

## 認証情報の作成

1. APIコンソールの[認証情報]を開く
2. 中央のダイアログの[認証情報を作成 ▼]押下し[OAuthクライアントID]を選択
3. [同意画面を設定]押下
4. [ユーザーに表示するサービス名]に[GooglePhoto practice]を入力(暫定 / 他は未入力)
5. [アプリケーションの種類]を[その他]にし、[名前]に[console app]と入力し[作成]

すると、client ID と client secret が作成されるのでメモする。
(後からでも[認証情報]メニューから確認できる)

## OAuth2する

### スコープって？

使用するトークンに許可するAPIの対象のこと。
平たく言うとアプリの権限に相当。

[Authentication and authorization scopes  |  Google Photos APIs  |  Google Developers](https://developers.google.com/photos/library/guides/authentication-authorization)

今回は読み書きをする予定なので`https://www.googleapis.com/auth/photoslibrary`を使用。


### 認証用URLに組み立て

シェル上で以下のコマンドを実行する。
URL自体は
`https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=<認証情報の作成で作成したクライアントID>&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/photoslibrary&access_type=offline`

```
zaki@wensley%  CLIENT_ID=<認証情報の作成で作成した文字列>
zaki@wensley%  CLIENT_SECRET=<認証情報の作成で作成した文字列>
zaki@wensley%  REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
zaki@wensley%  SCOPE=https://www.googleapis.com/auth/photoslibrary
zaki@wensley% echo "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=$SCOPE&access_type=offline"
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=****&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/photoslibrary&access_type=offline
```

### 認証コードの取得

OAuth用のURLにブラウザでアクセスし、アプリケーションへアクセス要求を許可すると認証コードが表示されるのでメモする。

![2018-05-12_11h48_57-mask.png](https://qiita-image-store.s3.amazonaws.com/0/256859/cae5f4bd-c3a4-e48c-5d99-a6c38d1472a2.png)

### Access Token / Refresh Tokenの取得

認証コードをパラメタにOAuthを実行する。

```
zaki@wensley%  AUTHORIZATION_CODE=<ブラウザアクセスで取得した認証コード>
zaki@wensley% curl -s --data "code=$AUTHORIZATION_CODE" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "redirect_uri=$REDIRECT_URI" --data "grant_type=authorization_code" --data "access_type=offline" https://www.googleapis.com/oauth2/v4/token
{
 "access_token": "***",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "***"
}

```

ここで取得した`access_token`と`refresh_token`をメモする。

以降、Web APIを使用する際にアクセストークンを使用する。
(HTTPリクエストヘッダに`Authorization: Bearer $ACCESS_TOKEN`を加える)
ただしレスポンスの`expires_in`の通り、3600秒(1時間)しか有効期限がないため、リフレッシュトークンを使用して都度再作成を行う必要がある。

### Refresh Tokenから再度Access Tokenを取得

```
zaki@wensley%  REFRESH_TOKEN=<取得済みのrefresh_token>
zaki@wensley% curl -s --data "refresh_token=$REFRESH_TOKEN" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token
{
 "access_token": "***",
 "token_type": "Bearer",
 "expires_in": 3600
}
```

# 参照系APIを使ってみる

以下の環境変数を設定しているものとする。

| 変数名           | 値 |
|:----------------|:--|
|$ACCESS_TOKEN | アクセストークン |

## アルバム一覧の取得

- リファレンス: [Method: albums.list](https://developers.google.com/photos/library/reference/rest/v1/albums/list)
- ガイド: [List library contents, albums, and media items](https://developers.google.com/photos/library/guides/list)

```
zaki@wensley% curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://photoslibrary.googleapis.com/v1/albums
{
  "albums": [
    {
      "id": "...",
      "title": "20180508 鶏すき丼",
      "productUrl": "https://photos.google.com/lr/album/...",
      "totalMediaItems": "1",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/..."
    },
    {
      "id": "...",
      "title": "20180503 03.試合＠千葉ロッテマリーンズ・ホークス4回戦",
      "productUrl": "https://photos.google.com/lr/album/...",
      "totalMediaItems": "4",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/..."
    },
    {
      "id": "...",
      "title": "20180503 02.キャラクター＆M☆Splash!!ダンスショー＠千葉ロッテマリーンズ・ホークス4回戦",
      "productUrl": "https://photos.google.com/lr/album/A...",
      "totalMediaItems": "8",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/..."
    },
:
:
  ],
  "nextPageToken": "..."
}
```

デフォルトは20件出力されるとのこと。
(でも16件だった。端数の処理を1件目にやってるのかもしれない)

`productUrl`のリンクをブラウザでアクセスすれば、Google Photosの該当のアルバムを開ける。
`id`の値はアルバム内画像一覧を取得するときに使用する(後述)。

### アルバム一覧(pageSize指定)

`https://photoslibrary.googleapis.com/v1/albums?pageSize=5`みたいにquery_stringで与える。

```
zaki@wensley% curl -s -H "Authorization: Bearer $ACCESS_TOKEN" 'https://photoslibrary.googleapis.com/v1/albums?pageSize=5'
{
  "albums": [
    {
      "id": "...",
      "title": "20180508 鶏すき丼",
      "productUrl": "https://photos.google.com/lr/album/...",
      "totalMediaItems": "1",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/..."
    },
    {
      "id": "...",
      "title": "20180503 03.試合＠千葉ロッテマリーンズ・ホークス4回戦",
      "productUrl": "https://photos.google.com/lr/album/...",
      "totalMediaItems": "4",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/..."
    },
    {
      "id": "...",
      "title": "20180503 02.キャラクター＆M☆Splash!!ダンスショー＠千葉ロッテマリーンズ・ホークス4回戦",
      "productUrl": "...",
      "totalMediaItems": "8",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/..."
    }
  ],
  "nextPageToken": "..."
}
zaki@wensley%
```

### アルバム一覧(pageSizeと[次のページ取得])

レスポンスにある`nextPageToken`を`pageToken`に使用することでページ処理ができる。
(ということは、最初からnページ目とかを表示することはできない感じかもしれない)

使用するURLは
`https://photoslibrary.googleapis.com/v1/albums?pageToken=<前のレスポンスのnextPageTokenの文字列>`

```
zaki@wensley% curl -s -H "Authorization: Bearer $ACCESS_TOKEN" 'https://photoslibrary.googleapis.com/v1/albums?pageSize=5&pageToken=***'
{
  "albums": [
    {
      "id": "***",
      "title": "20180503 01.ボールパークステージオープニング＠千葉ロッテマリーンズ・ホークス4回戦",
      "productUrl": "https://photos.google.com/lr/album/***",
      "totalMediaItems": "5",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/***"
    },
    {
      "id": "***",
      "title": "ツイッターアイコン",
      "productUrl": "https://photos.google.com/lr/album/***",
      "totalMediaItems": "24",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/***"
    },
    {
      "id": "***",
      "title": "20180501 チキンカレー＠パリカール",
      "productUrl": "https://photos.google.com/lr/album/***",
      "totalMediaItems": "1",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/***"
    },
    {
      "id": "***",
      "title": "20180429 GX7 mk3",
      "productUrl": "https://photos.google.com/lr/album/***",
      "totalMediaItems": "1",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/***"
    },
    {
      "id": "***",
      "title": "20180429 ホルモン ふたご",
      "productUrl": "https://photos.google.com/lr/album/***",
      "totalMediaItems": "14",
      "coverPhotoBaseUrl": "https://lh3.googleusercontent.com/photos-library/***"
    }
  ],
  "nextPageToken": "***"
}
zaki@wensley%
```

## アルバム内の画像一覧の取得

アルバム一覧の取得で得た`id`の値を`albumId`に指定する。
curlのコマンドはこんな感じ
`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "pageSize":"100", "albumId": "<アルバム一覧で取得したidの値>" }' https://photoslibrary.googleapis.com/v1/mediaItems:search`

- ガイド: [Listing album contents](https://developers.google.com/photos/library/guides/list#listing-album-contents)
- リファレンス: [Method: mediaItems.search](https://developers.google.com/photos/library/reference/rest/v1/mediaItems/search)


```
zaki@wensley% curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "pageSize":"100", "albumId": "***" }' https://photoslibrary.googleapis.com/v1/mediaItems:search
{
  "mediaItems": [
    {
      "id": "...",
      "productUrl": "https://photos.google.com/lr/album/.../photo/...",
      "baseUrl": "https://lh3.googleusercontent.com/photos-library/...",
      "mimeType": "image/jpeg",
      "mediaMetadata": {
        "creationTime": "2018-05-03T13:17:54Z",
        "width": "3464",
        "height": "4618",
        "photo": {
          "cameraMake": "Panasonic",
          "cameraModel": "DC-GX7MK3",
          "focalLength": 127,
          "apertureFNumber": 5.6,
          "isoEquivalent": 200
        }
      }
    },
:
:
}
```

`pageSize`や`pageToken`についてはアルバム一覧と同じ。


## 画像一覧の取得

普段Google Photosにアクセスしたときに表示される、全画像一覧の取得。
※ アルバム内の画像一覧取得と同じ(`albumId`の指定を削る)

curlのコマンドはこんな感じ。
`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "pageSize":"5" }' https://photoslibrary.googleapis.com/v1/mediaItems:search`

- ガイド: [Listing library contents](https://developers.google.com/photos/library/guides/list#listing-library-contents)
- リファレンス [Method: mediaItems.search](https://developers.google.com/photos/library/reference/rest/v1/mediaItems/search)


```
zaki@wensley% curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "pageSize":"5" }' https://photoslibrary.googleapis.com/v1/mediaItems:search 
{
  "mediaItems": [
    {
      "id": "***",
      "productUrl": "https://photos.google.com/lr/photo/***",
      "baseUrl": "https://lh3.googleusercontent.com/photos-library/***",
      "mimeType": "image/jpeg",
      "mediaMetadata": {
        "creationTime": "2018-05-11T11:11:16Z",
        "width": "3840",
        "height": "2160",
        "photo": {
          "cameraMake": "Sony",
          "cameraModel": "601SO",
          "focalLength": 4.23,
          "apertureFNumber": 2,
          "isoEquivalent": 100
        }
      }
    },
:
:
  ],
  "nextPageToken": "***"
}
```

`pageSize`や`pageToken`についてはアルバム一覧等と同じ。


# アルバム作成と画像アップロード

ようやく本題(笑)
APIをざっと見た感じだと、以下のようなこんな感じです。

- 空のアルバムの作成: ある
- アルバム作成時に格納する画像ファイルを指定: なさそう
- 画像のアップロード: ある
- 画像のアップロード時にアルバム指定: ある
- アップロード済みの画像を作成済みアルバムに格納: 不明


## アルバムの作成

- ガイド: [Create albums](https://developers.google.com/photos/library/guides/create-albums)
- リファレンス: [Method: albums.create](https://developers.google.com/photos/library/reference/rest/v1/albums/create)

curlでのコマンド概要
`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "album": { "title":"<アルバムのタイトル>" } }' https://photoslibrary.googleapis.com/v1/albums`

~~※ アルバム名の日本語指定方法は不明~~ (後述)

```
zaki@wensley% curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "album": { "title":"create by api" } }' https://photoslibrary.googleapis.com/v1/albums
{
  "id": "***",
  "title": "create by api",
  "productUrl": "https://photos.google.com/lr/album/***"
}
zaki@wensley%
```

`id`の値はアルバムへの追加等で使用するのでメモする。

日本語でアルバムタイトルを付ける場合は、Unicodeのコードポイントで指定する。
`日本語のアルバム(test)`であれば、パラメタに指定するアルバムタイトルは`\u65e5\u672c\u8a9e\u306e\u30a2\u30eb\u30d0\u30e0(test)`となる。

```
zaki@wensley% curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "album": { "title":"\u65e5\u672c\u8a9e\u306e\u30a2\u30eb\u30d0\u30e0(test)" } }' https://photoslibrary.googleapis.com/v1/albums
{
  "id": "...",
  "title": "日本語のアルバム(test)",
  "productUrl": "https://photos.google.com/lr/album/..."
}
```


## 画像のアップロード

- ガイド: [Upload media](https://developers.google.com/photos/library/guides/upload-media)
- リファレンス: なぜか無い

ちょっと面倒だけど、以下の2段階の処理が必要

1. アップロードURLへPOST
2. POSTできると`upload token`が得られるので、そのtokenを使って[メディアアイテムの作成]を実行

また、以下の点に注意

- ~~Google Photos上に同じファイル名のファイルがすでに存在している場合はアップロードに失敗する(`upload token`が得られない)という仕様がある~~ (8月追記: この仕様はなくなった模様)
- アップロードする画像はそのままの状態でアップロードされるため、Google Photosアプリケーションの設定的には「容量無制限」でなく「元のサイズ(品質)」と同等になっている。ストレージ保存したくない場合はあらかじめサイズ削減等しておく必要がある(以下引用)

> Note: All media items uploaded to Google Photos through the API are stored in full resolution at original quality. If your uploads exceed 25MB per user, your application should remind the user that these uploads will count towards storage in their Google Account.

(25MB per user と書かれていてて、よくわからない。。1ファイル1600万画素とかに削ればいいのとは違うのかな)

ちなみに、約11MBのJPGファイルをアップロードすると、ストレージが消費された。

### 画像データのバイトデータをアップロード

アップロード時に`X-Goog-Upload-File-Name`リクエストヘッダで、Google Photos上でのファイル名を指定する(元のファイル名と異なっていて構わない。~~重複を避けるために実行時の時刻を入れたりすると良いのかも~~)

[2018.08.12追記] また、6月下旬頃から動作が変わっていており、現在は`Content-type: application/octet-stream`と、`X-Goog-Upload-Protocol: raw`の指定が必要になっている(指定する文字列はどちらも固定)。
このヘッダがない場合は`X-Goog-Upload-File-Name`の指定が無視され、アップロードを行った日付がファイル名になる。
また、同名のファイル名があった場合でもデータが異なればアップロードに成功するようになっている。

※以下のコマンド例は`X-Goog-Upload-File-Name`しか指定していないが、現在は`Content-type`と、`X-Goog-Upload-Protocol`の指定も必要

```
zaki@wensley% curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/octet-stream" -H "X-Goog-Upload-File-Name: ***.jpg" --data-binary '@/path/to/***.jpg' https://photoslibrary.googleapis.com/v1/uploads 
CAIS6QIApKFirX......
```

アップロードに成功すると、`upload token`の文字列(のみ)が返ってくる(~~もしファイル名重複でtokenが得られない場合は、何も出力されない~~)

なお、アップロードトークンの有効期限は1日

> An upload token is valid for one day after being created.

### メディアアイテムの作成

アップロード時に得られる`upload token`をパラメタにAPIを実行すると、Google Photosに画像が反映される

```
zaki@wensley% curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "newMediaItems":[ { "simpleMediaItem": { "uploadToken": "CAIS6QIApKFirX......" }} ] }' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate
{
  "newMediaItemResults": [
    {
      "uploadToken": "CAIS6QIApKFirX...",
      "status": {
        "message": "OK"
      },
      "mediaItem": {
        "id": "...",
        "productUrl": "https://photos.google.com/lr/photo/...",
        "mimeType": "image/jpeg",
        "mediaMetadata": {
          "creationTime": "2018-05-03T20:00:47Z",
          "width": "5184",
          "height": "3888"
        }
      }
    }
  ]
}
zaki@wensley%
```

### メディアアイテムの作成(アルバム指定)

メディアアイテムの作成のパラメタには、追加するアルバムIDをオプションで指定することができる。
アルバム作成時のレスポンスや、アルバム一覧で取得した`id`を`albumId`に指定すると、指定したアルバムに追加された状態でGoogle Photosに反映される。

```
zaki@wensley% curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "albumId": "...", "newMediaItems":[ { "simpleMediaItem": { "uploadToken": "CAIS6QIApKFirf3Zl..." }} ] }' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate
{
  "newMediaItemResults": [
    {
      "uploadToken": "CAIS6QIApKFirf3Zl...",
      "status": {
        "message": "OK"
      },
      "mediaItem": {
:
}
```

# 課題

- 日本語のアルバム名はどうすればよいのか？
  - [追記] Unicodeのコードポイントを指定すればOK
- 画像の削除あるいは更新(replace)はないのか？
  - スマホのGoogle Photosアプリ上でトリミングとかするとそのままreplaceされるけど…
- 画像のサイズはどれくらいに抑えれば容量無制限内で利用できるのか？
- 一覧のAPIは検索条件の指定があり、日付指定なんかもできそうなので試したい

# まとめ

Google Photos APIsを使うことで、「バックアップと同期」でできなかった「フォルダ毎に分類した画像をそのままアルバム単位にまとめてアップロード」はできるようになる。(というか、この機能を標準で実装してほしいな、Googleさん…)