u-ryo's blog

various information for coding...

Category: Google photos

Google Photo Api

| Comments

一日千枚とか写真撮る人だと写真がすぐ溜まっちゃうんですよね。 backupは無限のGoogle Photosに、ということで、前はPicasaのAPI、upload_gphotsを使ってたんですけど、もう無くなっちゃっていて。どうしよう、途方に暮れていました。暫くぶりに探すと、丁度1年程前からGoogle Photo APIが整備されたようで、良かったです。ずっと待っていました。 [追記あり] Google Photos APIsでアルバム作成と写真のアップロードGoogle Photoを業務システムのクラウドストレージとして使った結果本家API Documentを参考に早速使ってみます。

ACCESS_TOKENの取得

  • APIの有効化
  • Google Developer Consoleから「認証情報」→「OAuth2.0クライアントID」無ければ上の「認証情報を作成」pulldown menuから「OAuthクライアントID」(「ウェブアプリケーションの種類」は「その他」)で作成
  • 上記「クライアントID」「クライアント シークレット」をメモ
  • 次のURLに$CLIENT_IDを入れてbrowserでaccess、AUTHORIZATION_CODEを取得 (https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=$CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/photoslibrary&access_type=offline (SCOPEはGoogle PhotoでのR/W accessの場合はhttps://www.googleapis.com/auth/photoslibrary)
  • 以下のようにして、ACCESS_TOKEN及びREFRESH_TOKENを得る
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    $ AUTHORIZATION_CODE=4/wnmGpTh__1zdrgdjmPWyetUI7C1mvsjRrA_IyZmwY7aSeYppD9X_9iB
    $ CLIENT_ID=952391557281-s8b8ditnocfu590fi0ntsfk76rbmkm80.apps.googleusercontent.com
    $ CLIENT_SECRET=k6XPLuryMWUtKDKmS1cYgW0r
    $ REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
    
    $ curl --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": "ya29.GlsOB-ebr6NrI78UemOPHcm1-jdw0XkxD8iiSqE-Bh5xB_Sx8bhKsRhRyz7gqJy45A-HIF6s6GF0j5wz0dmNppVqEMhtUurAwfbe-xgEsR5MZFjoIY3ONOx8zd4Q",
      "expires_in": 3600,
      "refresh_token": "1/8LrGRLdBaFJYHlOr0rEAyZcgC9yDl2PcZZyrbqoxc7c",
      "scope": "https://www.googleapis.com/auth/photoslibrary",
      "token_type": "Bearer"
    }
    
  • ACCESS_TOKENは1時間しか有効でないので、適宜REFRESH_TOKENを使って更新
    1
    2
    3
    4
    5
    
    $ REFRESH_TOKEN=1/8LrGRLdBaFJYHlOr0rEAyZcgC9yDl2PcZZyrbqoxc7c
    $ CLIENT_ID=952391557281-s8b8ditnocfu590fi0ntsfk76rbmkm80.apps.googleusercontent.com
    $ CLIENT_SECRET=k6XPLuryMWUtKDKmS1cYgW0r
    
    $ ACCESS_TOKEN=`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|jq .access_token -r`
    

REFRESH_TOKENを取得すれば、あとCLIENT_IDCLIENT_SECRETが分かればACCESS_TOKENは更新できます。

ALBUMの作成

  • 既存のAlbumの確認
    1
    
    $ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://photoslibrary.googleapis.com/v1/albums?pageSize=50
    
  • 既存のAlbumの確認(nextPageTokenがある場合)
    1
    
    $ curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://photoslibrary.googleapis.com/v1/albums?pageSize=50&pageToken=...
    
  • 新規Albumの作成
    1
    2
    3
    4
    5
    6
    7
    8
    
    $ DIR=20190428
    $ curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "album": { "title":"'$DIR'" } }' https://photoslibrary.googleapis.com/v1/albums
    {
      "id": "ADIlBkAOcfB64a_Opnwdjgxeq6jhQv4GQ1pZQ-wse2o2hiBIofuhefmFycfTtIcLAG0inLt0FlZn",
      "title": "20190428",
      "productUrl": "https://photos.google.com/lr/album/ADIlBkAOcfB64a_Opnwdjgxeq6jhQv4GQ1pZQ-wse2o2hiBIofuhefmFycfTtIcLAG0inLt0FlZn",
      "isWriteable": true
    }
    

UPLOAD and adding to Album

2段階になっていて、

  1. binary fileをuploadしてUPLOAD_TOKENを得る
  2. UPLOAD_TOKENを元にmediaItems:batchCreateする(ALBUM名はここで渡す。batch処理なので複数のUPLOAD_TOKENを渡せる)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FILENAME=20190428/img_0699.jpg

$ curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-type: application/octet-stream' -H 'X-Goog-Upload-Protocol: raw' -H "X-Goog-Upload-File-Name: $FILENAME" --data-binary "@$FILENAME" https://photoslibrary.googleapis.com/v1/uploads
CAIS+QIASsy4zFbu3IKGgbDXA5XshOGvHPOTLuqbqTN9MQxKRVCsxp3YHbus+qsDgA0GCjuqXdmGpv1uWFxKvf8GYa/8VJQ1S6FUcmGWgw6Hdj14QNYtBRVbXU/cdq/Jkx3ZblG5co3hnY6+yMxih26kB0vTWfWp9GwIE904y5yXEE1pm/V0bFduzA/CZvdlAU9EvWfqKnNO7c3nozWUalm5WUZHHatVQZT+H5+jD0Bq3YwMUdfC5KF048AxFa9auW1HpQGdboalYyXBCJksfzteWtU53wZ8rFnZgHwrui9uA2ptnTuDlin2m+WXU+HqaVRuKX1ou5BzalI4P0gVfWql41Af6nuvvEdMNZ39tEvK2EARUX0CUd8veDznZiWjtPcRqpJnvjDRCxaSgr/cn+JXf9k7SnD0DYVWOdM64lngcAuXxsKk6RJJOVxQBUi6XAG04dHnKxDndqjl+fcH9qWAmpXejPx8Kgn6GX7TgatiKHEG4ybvWjStWg1JPg

$ UPLOAD_TOKEN=CAIS+...
$ ALBUM_ID=ADIlBkAOcfB64a_Opnwdjgxeq6jhQv4GQ1pZQ-wse2o2hiBIofuhefmFycfTtIcLAG0inLt0FlZn
$ curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "albumId": "'$ALBUM_ID'", "newMediaItems":[ { "simpleMediaItem": { "uploadToken": "'$UPLOAD_TOKEN'" }} ] }' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate
{
  "newMediaItemResults": [
    {
      "uploadToken": "CAIS+...",
      "status": {
        "message": "OK"
      },
      "mediaItem": {
        "id": "ADIl...",
        "productUrl": "https://photos.google.com/lr/album/ADIl...",
        "mimeType": "image/jpeg",
        "mediaMetadata": {
          "creationTime": "2019-04-28T02:40:35Z",
          "width": "5184",
          "height": "3456"
        },
        "filename": "20190428/img_0699.jpg"
      }
    }
  ]
}

folderまるっとupload

  • 事前準備
    1
    2
    3
    4
    
    REFRESH_TOKEN=...
    CLIENT_ID=...
    CLIENT_SECRET=...
    ACCESS_TOKEN=`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|jq .access_token -r`
    
  • Album作成
    1
    2
    
    DIR=...
    ALBUM_ID=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"album":{"title":"'$DIR'"}}' https://photoslibrary.googleapis.com/v1/albums|jq -r .id`
    
  • ~/photo/$DIR以下のimg_*.jpg filesのuploadとalbum登録(約100 files毎にACCESS_TOKENのrefresh)
1
for i in ~/photo/$DIR/img_*.jpg; do if [ ! ${i##*00.jpg} ];then ACCESS_TOKEN=`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|jq .access_token -r`;fi;UPLOAD_TOKEN=`FILENAME=$DIR/${i##*/}; curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-type: application/octet-stream' -H 'X-Goog-Upload-Protocol: raw' -H "X-Goog-Upload-File-Name: $FILENAME" --data-binary "@$i" https://photoslibrary.googleapis.com/v1/uploads`;curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"albumId":"'$ALBUM_ID'","newMediaItems":[{"simpleMediaItem":{"uploadToken":"'$UPLOAD_TOKEN'"}}]}' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate|tee -a /tmp/upload.log|grep -q error&&echo $i;done|tee /tmp/upload_failed.log

uploadに失敗したfile namesが標準出力と/tmp/upload_failed.logに出てくるので、後刻それらをretry。

1
for i in `cat /tmp/upload_failed.log`; do if [ ! ${i##*00.jpg} ];then ACCESS_TOKEN=`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|jq .access_token -r`;fi;UPLOAD_TOKEN=`FILENAME=$DIR/${i##*/}; curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-type: application/octet-stream' -H 'X-Goog-Upload-Protocol: raw' -H "X-Goog-Upload-File-Name: $FILENAME" --data-binary "@$i" https://photoslibrary.googleapis.com/v1/uploads`;curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"albumId":"'$ALBUM_ID'","newMediaItems":[{"simpleMediaItem":{"uploadToken":"'$UPLOAD_TOKEN'"}}]}' https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate|tee -a /tmp/upload.log|grep -q error&&echo $i;done

これではbatch処理を活かしていない(複数のUPLOAD_TOKENをbatchCreateしていない)のですが、Googleだけに割とすぐ終わること、error handlingがあまりにも複雑になることから、都度batchCreateすることにしました。

私の場合、1000 filesで約3GB弱、を目処に分割してuploadしています。 uploadしたfilesは全て「元のサイズ」で保存されてしまい、Google Driveの容量を消費してしまうので、設定から「容量を解放」しなければなりません。これが「1日1回」となっているものの、だからといって24時間後に再度実行しても「ファイルを圧縮できませんでした。ストレージを復元できるのは 1 日 1 回だけです。」と言われて出来ず、困っています。実際に再度実行できるまでには1.5日〜2日かかるようで、これが最大のneckになっています。

新規Albumへの既存files追加

これはダメでした。 何度試してもダメだったので、調べてみると、公式Documentに、 Note that you can only add media items that have been uploaded by your application to albums that your application has created.とあります。 なんでやねん! 何で既存の画像とAPI経由の画像とを区別するのか、わけわかりません。 それじゃぁ、っていうんで、既にGoogle Photos上にある写真も改めてuploadしてalbumにaddしたら、それは出来ました。しかし、「元のサイズ」になってしまって容量を食ってしまいます。これについても「容量を解放」しなければなりません。 全く七面倒臭いものです。

ちなみに、以下のようにやりました。paginationが発生しない程度のAlbum限定で、

1
2
3
4
5
6
7
8
9
10
11
12
13
$ DIR=20171005
$ ALBUM_ID=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{ "album": { "title":"'$DIR'" } }' https://photoslibrary.googleapis.com/v1/albums|jq -r .id`

$ MEDIA_ITEMS=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"pageSize":"100","filters":{"dateFilter":{"dates":[{"year":2017,"month":10,"day":5}]}}}' https://photoslibrary.googleapis.com/v1/mediaItems:search|jq .mediaItems[].id|sed -z 's/\n/,/g'`

$ curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"mediaItemIds":['$MEDIA_ITEMS']}' https://photoslibrary.googleapis.com/v1/albums/${ALBUM_ID}:batchAddMediaItems
{
  "error": {
    "code": 400,
    "message": "Request contains an invalid media item id.",
    "status": "INVALID_ARGUMENT"
  }
}
なら既存のものは手でやればいいではないか? やってみたのですが、微妙に手元のfilesと数が合わなかったりするので、困難です。手元に3282枚あってGoogle PhotosのAlbumに3281枚あった時、どうやって差分をあぶり出したらいいのですか?! 全downloadはなしで。もう一つでは、手元に5221枚、Google Photosに5230枚と増えてます! Manuallyでは限界を感じました。

Album中の全file名取得

自己解決しました。 NEXT_PAGE_TOKENあると面倒くさいんですけど、これで何とか。

Album探し

最初のpageに目的のalbumがあるかをこれ↓で探す

1
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" 'https://photoslibrary.googleapis.com/v1/albums?pageSize=50'|jq -r .albums[].title,.nextPageToken
なければ次のpageへ。

1
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" 'https://photoslibrary.googleapis.com/v1/albums?pageSize=50&pageToken=Ck...'|jq -r .albums[].title,.nextPageToken

見付かれば、ALBUM_IDを同定。

1
2
3
4
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" 'https://photoslibrary.googleapis.com/v1/albums?pageSize=50&pageToken=Ck...'|grep -1 20080318
      "id": "ADI...",
      "title": "20080318",
      "productUrl": "https://photos.google.com/lr/album/ADI...",

そうしてから徐に、

1
2
ALBUM_ID=...
NEXT_PAGE_TOKEN=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"pageSize":"100","albumId":"'$ALBUM_ID'"}' https://photoslibrary.googleapis.com/v1/mediaItems:search|jq -r '.mediaItems[].filename,.nextPageToken'|tee /tmp/files.txt|tail -1`;while [ "$NEXT_PAGE_TOKEN" != null ];do NEXT_PAGE_TOKEN=`curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-type: application/json" -d '{"pageSize":"100","albumId":"'$ALBUM_ID'","pageToken":"'$NEXT_PAGE_TOKEN'"}' https://photoslibrary.googleapis.com/v1/mediaItems:search|jq -r '.mediaItems[].filename,.nextPageToken'|tee -a /tmp/files.txt|tail -1`;done

その後、

1
grep -vE -e '.{300,}' -e null /tmp/files.txt

で取り出せます。

それで比較(diff -y --suppress-common-lines <(cd ~/photo;ls .../img_*|sort) <(grep -vE -e '.{300,}' -e null /tmp/files.txt|sort))したところ、足りないものはわかりました。 ですが、何故かAPIで取ると3282個なのに 「コンテンツ 3283個」と表示されていたり... よく精査すると、なるほど、Google Photosが勝手に?作った、 アシスタントにある MOVIE.mp4(ムービー)や...-EFFECTS.jpg(スタイルを適用した写真)、 ...-PANO.jpg(パノラマ)が含まれているから? いや、一つ(5230個と表示)はそれが原因で9個多く数が表示されていたのですが、 もう一つ、「コンテンツ 3283個」は、APIで取得するといくら見ても 3282個しかない、自動生成物もない、です。謎です。

それと、EnrichmentとかいってTextやLocationとMapを入れられるんですけど、 それらを取得する術がなく、Textに書いたことを検索するとかも出来ず。 GUIから入れてみましたが、要素を追加する度にいちいち先頭に戻される、 移動すると他の要素もどこかに行ってしまうことがある、 等何だかなぁ、というものでした。 Googleならこんなもんじゃないだろー!